@opengeni/react 0.52.1 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +51 -1
- package/dist/artifacts.d.ts +1 -1
- package/dist/artifacts.js +43 -14
- package/dist/artifacts.js.map +1 -1
- package/dist/{chunk-ROIRTM37.js → chunk-5UQPNLIO.js} +6 -6
- package/dist/{chunk-JR3QFY6B.js → chunk-6KLX2RH5.js} +49 -2
- package/dist/chunk-6KLX2RH5.js.map +1 -0
- package/dist/{chunk-FWS3KC3Z.js → chunk-B6WPZSTT.js} +104 -104
- package/dist/{chunk-FWS3KC3Z.js.map → chunk-B6WPZSTT.js.map} +1 -1
- package/dist/{chunk-HDBG7XGO.js → chunk-C5ZPSSS5.js} +50 -1
- package/dist/chunk-C5ZPSSS5.js.map +1 -0
- package/dist/{chunk-X4S56Y2H.js → chunk-HA46CYQK.js} +135 -11
- package/dist/chunk-HA46CYQK.js.map +1 -0
- package/dist/{chunk-724BW4TW.js → chunk-IGC6A2ZI.js} +2 -2
- package/dist/chunk-LLOLUHVG.js +5937 -0
- package/dist/chunk-LLOLUHVG.js.map +1 -0
- package/dist/{chunk-WYA65Y3F.js → chunk-MKPIAGVL.js} +13 -3
- package/dist/chunk-MKPIAGVL.js.map +1 -0
- package/dist/chunk-R3AZOAB5.js +933 -0
- package/dist/chunk-R3AZOAB5.js.map +1 -0
- package/dist/{chunk-23EJ676W.js → chunk-XBZWOM2K.js} +9 -4
- package/dist/chunk-XBZWOM2K.js.map +1 -0
- package/dist/{chunk-JAGDUCZQ.js → chunk-Y7OEDUJ5.js} +17 -13
- package/dist/chunk-Y7OEDUJ5.js.map +1 -0
- package/dist/client.d.ts +6 -4
- package/dist/components/artifacts/index.d.ts +1 -0
- package/dist/components/artifacts/published-html-artifact-frame.d.ts +14 -0
- package/dist/components/browser-input.d.ts +7 -0
- package/dist/components/browser-viewer.d.ts +5 -4
- package/dist/components/computer-viewer.d.ts +2 -1
- package/dist/components/desktop-viewer.d.ts +26 -2
- package/dist/components/interaction-intervention-banner.d.ts +11 -0
- package/dist/components/sandbox-terminal.d.ts +4 -1
- package/dist/components/sandbox-workspace.d.ts +3 -0
- package/dist/composer.js +6 -6
- package/dist/hooks/use-auth-runs.d.ts +32 -0
- package/dist/hooks/use-browser-downloads.d.ts +25 -0
- package/dist/hooks/use-browser-identities.d.ts +5 -1
- package/dist/hooks/use-browser-session.d.ts +2 -1
- package/dist/hooks/use-browser-sessions.d.ts +1 -1
- package/dist/hooks/use-channels.d.ts +26 -0
- package/dist/hooks/use-computer-frame-stream.d.ts +1 -1
- package/dist/hooks/use-computer-session.d.ts +2 -1
- package/dist/hooks/use-computer-sessions.d.ts +1 -1
- package/dist/hooks/use-desktop-stream.d.ts +4 -0
- package/dist/hooks/use-interaction-interventions.d.ts +27 -0
- package/dist/hooks/use-interaction-invalidation.d.ts +19 -0
- package/dist/hooks/use-network-routes.d.ts +25 -0
- package/dist/hooks/use-site-auth-connections.d.ts +25 -0
- package/dist/hooks/use-supergrok-accounts.d.ts +26 -0
- package/dist/hooks/use-terminal-stream.d.ts +3 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +1220 -1545
- package/dist/index.js.map +1 -1
- package/dist/interaction.d.ts +13 -3
- package/dist/interaction.js +520 -6
- package/dist/interaction.js.map +1 -1
- package/dist/interactive-workbench-viewers-TOHBXJEE.js +15 -0
- package/dist/lib/host-platform.d.ts +5 -0
- package/dist/lib/relay-wire.d.ts +20 -1
- package/dist/machines.js +3 -3
- package/dist/model-policy.d.ts +1 -1
- package/dist/model-policy.js +1 -1
- package/dist/provider.d.ts +3 -2
- package/dist/realtime/realtime-control.d.ts +6 -2
- package/dist/realtime.js +138 -24
- package/dist/realtime.js.map +1 -1
- package/dist/session-context.d.ts +10 -2
- package/dist/session-ui.js +3 -3
- package/dist/session.js +11 -11
- package/dist/timeline/types.d.ts +5 -0
- package/package.json +2 -2
- package/src/artifacts.ts +3 -0
- package/src/client.ts +73 -3
- package/src/components/artifacts/index.ts +5 -0
- package/src/components/artifacts/published-html-artifact-frame.tsx +38 -0
- package/src/components/browser-input.ts +65 -0
- package/src/components/browser-viewer.tsx +1287 -177
- package/src/components/computer-viewer.tsx +611 -114
- package/src/components/desktop-viewer.tsx +114 -1
- package/src/components/interaction-intervention-banner.tsx +99 -0
- package/src/components/machines/machine-detail.tsx +1 -1
- package/src/components/model-policy-picker.tsx +15 -0
- package/src/components/sandbox-terminal.tsx +24 -7
- package/src/components/sandbox-workspace.tsx +42 -4
- package/src/components/workspace-dock.tsx +59 -5
- package/src/hooks/use-attached-browsers.ts +13 -11
- package/src/hooks/use-auth-runs.ts +278 -0
- package/src/hooks/use-browser-downloads.ts +218 -0
- package/src/hooks/use-browser-frame-stream.ts +168 -59
- package/src/hooks/use-browser-identities.ts +47 -6
- package/src/hooks/use-browser-session.ts +94 -27
- package/src/hooks/use-browser-sessions.ts +19 -14
- package/src/hooks/use-channels.ts +99 -0
- package/src/hooks/use-computer-frame-stream.ts +184 -61
- package/src/hooks/use-computer-session.ts +22 -2
- package/src/hooks/use-computer-sessions.ts +19 -13
- package/src/hooks/use-desktop-stream.ts +39 -7
- package/src/hooks/use-interaction-interventions.ts +242 -0
- package/src/hooks/use-interaction-invalidation.ts +66 -0
- package/src/hooks/use-machine-chip.ts +13 -3
- package/src/hooks/use-network-routes.ts +214 -0
- package/src/hooks/use-site-auth-connections.ts +226 -0
- package/src/hooks/use-supergrok-accounts.ts +81 -0
- package/src/hooks/use-terminal-stream.ts +288 -55
- package/src/index.ts +8 -0
- package/src/interaction.ts +30 -0
- package/src/lib/host-platform.ts +10 -0
- package/src/lib/relay-wire.ts +43 -4
- package/src/lib/terminal-capability.ts +1 -1
- package/src/model-policy.ts +19 -4
- package/src/provider.tsx +133 -1
- package/src/realtime/realtime-control.tsx +181 -26
- package/src/session-context.ts +66 -1
- package/src/timeline/activity-rail.tsx +6 -0
- package/src/timeline/projection.ts +58 -0
- package/src/timeline/types.ts +2 -0
- package/styles/compiled.css +64 -6
- package/dist/chunk-23EJ676W.js.map +0 -1
- package/dist/chunk-6BBWWOOU.js +0 -82
- package/dist/chunk-6BBWWOOU.js.map +0 -1
- package/dist/chunk-7I2KB3OK.js +0 -3895
- package/dist/chunk-7I2KB3OK.js.map +0 -1
- package/dist/chunk-HDBG7XGO.js.map +0 -1
- package/dist/chunk-JAGDUCZQ.js.map +0 -1
- package/dist/chunk-JR3QFY6B.js.map +0 -1
- package/dist/chunk-WYA65Y3F.js.map +0 -1
- package/dist/chunk-X4S56Y2H.js.map +0 -1
- package/dist/interactive-workbench-viewers-KHNOHWQE.js +0 -13
- /package/dist/{chunk-ROIRTM37.js.map → chunk-5UQPNLIO.js.map} +0 -0
- /package/dist/{chunk-724BW4TW.js.map → chunk-IGC6A2ZI.js.map} +0 -0
- /package/dist/{interactive-workbench-viewers-KHNOHWQE.js.map → interactive-workbench-viewers-TOHBXJEE.js.map} +0 -0
|
@@ -0,0 +1,933 @@
|
|
|
1
|
+
import {
|
|
2
|
+
cn
|
|
3
|
+
} from "./chunk-22KP6LR5.js";
|
|
4
|
+
|
|
5
|
+
// src/lib/relay-wire.ts
|
|
6
|
+
import { BinaryReader, BinaryWriter } from "@bufbuild/protobuf/wire";
|
|
7
|
+
var STREAM_KIND_DESKTOP = 2;
|
|
8
|
+
var STREAM_KIND_PTY = 1;
|
|
9
|
+
var STREAM_KIND_BROWSER = 3;
|
|
10
|
+
var STREAM_KIND_COMPUTER = 4;
|
|
11
|
+
var STREAM_ROLE_CLIENT = 2;
|
|
12
|
+
var STREAM_CLOSE_REASON_NORMAL = 1;
|
|
13
|
+
function encodeChannel(message, writer) {
|
|
14
|
+
if (message.channelId !== "") writer.uint32(10).string(message.channelId);
|
|
15
|
+
if (message.workspaceId !== "") writer.uint32(18).string(message.workspaceId);
|
|
16
|
+
if (message.agentId !== "") writer.uint32(26).string(message.agentId);
|
|
17
|
+
if (message.kind !== 0) writer.uint32(32).int32(message.kind);
|
|
18
|
+
if (message.port !== 0) writer.uint32(40).uint32(message.port);
|
|
19
|
+
return writer;
|
|
20
|
+
}
|
|
21
|
+
function encodeStreamOpen(message) {
|
|
22
|
+
const writer = new BinaryWriter();
|
|
23
|
+
encodeChannel(message.channel, writer.uint32(10).fork()).join();
|
|
24
|
+
if (message.token !== "") writer.uint32(18).string(message.token);
|
|
25
|
+
if (message.role !== 0) writer.uint32(24).int32(message.role);
|
|
26
|
+
if (message.resumeFromSeq !== "0") writer.uint32(32).uint64(message.resumeFromSeq);
|
|
27
|
+
return writer.finish();
|
|
28
|
+
}
|
|
29
|
+
function encodeStreamClose(message) {
|
|
30
|
+
const writer = new BinaryWriter();
|
|
31
|
+
if (message.channelId !== "") writer.uint32(10).string(message.channelId);
|
|
32
|
+
if (message.reason !== 0) writer.uint32(16).int32(message.reason);
|
|
33
|
+
if (message.message !== "") writer.uint32(26).string(message.message);
|
|
34
|
+
return writer.finish();
|
|
35
|
+
}
|
|
36
|
+
function encodeStreamFrame(message) {
|
|
37
|
+
const writer = new BinaryWriter();
|
|
38
|
+
if (message.channelId !== "") writer.uint32(10).string(message.channelId);
|
|
39
|
+
if (message.seq !== "0") writer.uint32(16).uint64(message.seq);
|
|
40
|
+
if (message.data.length > 0) writer.uint32(26).bytes(message.data);
|
|
41
|
+
if (message.producedAtMs !== "0") writer.uint32(32).uint64(message.producedAtMs);
|
|
42
|
+
return writer.finish();
|
|
43
|
+
}
|
|
44
|
+
function decodeAgentErrorMessage(reader, length) {
|
|
45
|
+
const end = reader.pos + length;
|
|
46
|
+
let message;
|
|
47
|
+
while (reader.pos < end) {
|
|
48
|
+
const tag = reader.uint32();
|
|
49
|
+
if (tag >>> 3 === 2 && tag === 18) {
|
|
50
|
+
message = reader.string();
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
if ((tag & 7) === 4 || tag === 0) break;
|
|
54
|
+
reader.skip(tag & 7);
|
|
55
|
+
}
|
|
56
|
+
return message !== void 0 ? { message } : {};
|
|
57
|
+
}
|
|
58
|
+
function decodeStreamOpenAck(bytes) {
|
|
59
|
+
const reader = new BinaryReader(bytes);
|
|
60
|
+
const end = reader.len;
|
|
61
|
+
let accepted = false;
|
|
62
|
+
let error;
|
|
63
|
+
while (reader.pos < end) {
|
|
64
|
+
const tag = reader.uint32();
|
|
65
|
+
if (tag >>> 3 === 1 && tag === 8) {
|
|
66
|
+
accepted = reader.bool();
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (tag >>> 3 === 2 && tag === 18) {
|
|
70
|
+
error = decodeAgentErrorMessage(reader, reader.uint32());
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if ((tag & 7) === 4 || tag === 0) break;
|
|
74
|
+
reader.skip(tag & 7);
|
|
75
|
+
}
|
|
76
|
+
return error !== void 0 ? { accepted, error } : { accepted };
|
|
77
|
+
}
|
|
78
|
+
function decodeStreamFrame(bytes) {
|
|
79
|
+
const reader = new BinaryReader(bytes);
|
|
80
|
+
const end = reader.len;
|
|
81
|
+
let seq = "0";
|
|
82
|
+
let data = new Uint8Array(0);
|
|
83
|
+
while (reader.pos < end) {
|
|
84
|
+
const tag = reader.uint32();
|
|
85
|
+
if (tag >>> 3 === 2 && tag === 16) {
|
|
86
|
+
seq = reader.uint64().toString();
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (tag >>> 3 === 3 && tag === 26) {
|
|
90
|
+
data = new Uint8Array(reader.bytes());
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
if ((tag & 7) === 4 || tag === 0) break;
|
|
94
|
+
reader.skip(tag & 7);
|
|
95
|
+
}
|
|
96
|
+
return { seq, data };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// src/hooks/use-relay-frame-stream.ts
|
|
100
|
+
import { useEffect, useRef, useState } from "react";
|
|
101
|
+
var TAG_OPEN = 1;
|
|
102
|
+
var TAG_OPENACK = 2;
|
|
103
|
+
var TAG_FRAME = 3;
|
|
104
|
+
function datagram(tag, body) {
|
|
105
|
+
const out = new Uint8Array(body.length + 1);
|
|
106
|
+
out[0] = tag;
|
|
107
|
+
out.set(body, 1);
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
function defaultWebSocketFactory(url) {
|
|
111
|
+
return new WebSocket(url);
|
|
112
|
+
}
|
|
113
|
+
function useRelayFrameStream(options) {
|
|
114
|
+
const { capability, containerRef, webSocketFactory } = options;
|
|
115
|
+
const [state, setState] = useState("idle");
|
|
116
|
+
const [error, setError] = useState(null);
|
|
117
|
+
const [nonce, setNonce] = useState(0);
|
|
118
|
+
const stateRef = useRef("idle");
|
|
119
|
+
const setBoth = (next) => {
|
|
120
|
+
stateRef.current = next;
|
|
121
|
+
setState(next);
|
|
122
|
+
};
|
|
123
|
+
const reconnect = () => setNonce((n) => n + 1);
|
|
124
|
+
const transport = capability?.transport ?? null;
|
|
125
|
+
const url = capability?.url ?? null;
|
|
126
|
+
const token = capability?.token ?? null;
|
|
127
|
+
const factoryRef = useRef(webSocketFactory);
|
|
128
|
+
factoryRef.current = webSocketFactory;
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
if (typeof window === "undefined") return;
|
|
131
|
+
if (transport !== "relay-frames" || !url) {
|
|
132
|
+
setBoth("idle");
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const container = containerRef.current;
|
|
136
|
+
if (!container) {
|
|
137
|
+
setBoth("idle");
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
let channel;
|
|
141
|
+
try {
|
|
142
|
+
const u = new URL(url);
|
|
143
|
+
channel = {
|
|
144
|
+
channelId: u.searchParams.get("channel") ?? "",
|
|
145
|
+
workspaceId: u.searchParams.get("ws") ?? "",
|
|
146
|
+
agentId: u.searchParams.get("agent") ?? "",
|
|
147
|
+
kind: STREAM_KIND_DESKTOP,
|
|
148
|
+
port: Number(u.searchParams.get("port") ?? "0")
|
|
149
|
+
};
|
|
150
|
+
} catch (cause) {
|
|
151
|
+
setError(cause instanceof Error ? cause : new Error(String(cause)));
|
|
152
|
+
setBoth("error");
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
setError(null);
|
|
156
|
+
setBoth("connecting");
|
|
157
|
+
const canvas = document.createElement("canvas");
|
|
158
|
+
canvas.setAttribute("data-opengeni-desktop-frames", "");
|
|
159
|
+
const style = canvas.style;
|
|
160
|
+
style.position = "absolute";
|
|
161
|
+
style.top = "0";
|
|
162
|
+
style.left = "0";
|
|
163
|
+
style.right = "0";
|
|
164
|
+
style.bottom = "0";
|
|
165
|
+
style.margin = "auto";
|
|
166
|
+
style.maxWidth = "100%";
|
|
167
|
+
style.maxHeight = "100%";
|
|
168
|
+
style.width = "auto";
|
|
169
|
+
style.height = "auto";
|
|
170
|
+
style.display = "block";
|
|
171
|
+
style.imageRendering = "auto";
|
|
172
|
+
container.appendChild(canvas);
|
|
173
|
+
const ctx = canvas.getContext("2d");
|
|
174
|
+
let disposed = false;
|
|
175
|
+
let acked = false;
|
|
176
|
+
let decoding = false;
|
|
177
|
+
let pending = null;
|
|
178
|
+
const drainLatest = async () => {
|
|
179
|
+
if (decoding) return;
|
|
180
|
+
decoding = true;
|
|
181
|
+
try {
|
|
182
|
+
while (pending) {
|
|
183
|
+
if (disposed) break;
|
|
184
|
+
const data = pending;
|
|
185
|
+
pending = null;
|
|
186
|
+
let bmp;
|
|
187
|
+
try {
|
|
188
|
+
bmp = await createImageBitmap(new Blob([new Uint8Array(data)], { type: "image/png" }));
|
|
189
|
+
} catch {
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
if (disposed) {
|
|
193
|
+
bmp.close();
|
|
194
|
+
break;
|
|
195
|
+
}
|
|
196
|
+
if (canvas.width !== bmp.width || canvas.height !== bmp.height) {
|
|
197
|
+
canvas.width = bmp.width;
|
|
198
|
+
canvas.height = bmp.height;
|
|
199
|
+
}
|
|
200
|
+
ctx?.drawImage(bmp, 0, 0, canvas.width, canvas.height);
|
|
201
|
+
bmp.close();
|
|
202
|
+
if (!disposed && stateRef.current !== "connected") setBoth("connected");
|
|
203
|
+
}
|
|
204
|
+
} finally {
|
|
205
|
+
decoding = false;
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
let ws;
|
|
209
|
+
try {
|
|
210
|
+
ws = (factoryRef.current ?? defaultWebSocketFactory)(url);
|
|
211
|
+
} catch (cause) {
|
|
212
|
+
setError(cause instanceof Error ? cause : new Error(String(cause)));
|
|
213
|
+
setBoth("error");
|
|
214
|
+
canvas.remove();
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
ws.binaryType = "arraybuffer";
|
|
218
|
+
const onOpen = () => {
|
|
219
|
+
if (disposed) return;
|
|
220
|
+
const body = encodeStreamOpen({
|
|
221
|
+
channel,
|
|
222
|
+
token: token ?? "",
|
|
223
|
+
role: STREAM_ROLE_CLIENT,
|
|
224
|
+
resumeFromSeq: "0"
|
|
225
|
+
});
|
|
226
|
+
try {
|
|
227
|
+
ws.send(datagram(TAG_OPEN, body).buffer);
|
|
228
|
+
} catch {
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
const onMessage = (ev) => {
|
|
232
|
+
if (disposed) return;
|
|
233
|
+
const data = ev.data;
|
|
234
|
+
if (!(data instanceof ArrayBuffer)) return;
|
|
235
|
+
const buf = new Uint8Array(data);
|
|
236
|
+
if (buf.length === 0) return;
|
|
237
|
+
const tag = buf[0];
|
|
238
|
+
const rest = buf.subarray(1);
|
|
239
|
+
if (tag === TAG_OPENACK) {
|
|
240
|
+
let ack;
|
|
241
|
+
try {
|
|
242
|
+
ack = decodeStreamOpenAck(rest);
|
|
243
|
+
} catch {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
if (!ack.accepted) {
|
|
247
|
+
setError(
|
|
248
|
+
new Error(
|
|
249
|
+
ack.error?.message ? `desktop stream rejected: ${ack.error.message}` : "desktop stream rejected by the relay"
|
|
250
|
+
)
|
|
251
|
+
);
|
|
252
|
+
setBoth("error");
|
|
253
|
+
try {
|
|
254
|
+
ws.close();
|
|
255
|
+
} catch {
|
|
256
|
+
}
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
acked = true;
|
|
260
|
+
} else if (tag === TAG_FRAME) {
|
|
261
|
+
let fr;
|
|
262
|
+
try {
|
|
263
|
+
fr = decodeStreamFrame(rest);
|
|
264
|
+
} catch {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (fr.data && fr.data.length > 0) {
|
|
268
|
+
pending = fr.data;
|
|
269
|
+
void drainLatest();
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
const onError = () => {
|
|
274
|
+
if (disposed) return;
|
|
275
|
+
setError((prev) => prev ?? new Error("desktop stream connection error"));
|
|
276
|
+
setBoth("error");
|
|
277
|
+
};
|
|
278
|
+
const onClose = () => {
|
|
279
|
+
if (disposed) return;
|
|
280
|
+
setError(
|
|
281
|
+
(prev) => prev ?? new Error(acked ? "desktop stream closed" : "desktop stream closed before it opened")
|
|
282
|
+
);
|
|
283
|
+
setBoth("error");
|
|
284
|
+
};
|
|
285
|
+
ws.addEventListener("open", onOpen);
|
|
286
|
+
ws.addEventListener("message", onMessage);
|
|
287
|
+
ws.addEventListener("error", onError);
|
|
288
|
+
ws.addEventListener("close", onClose);
|
|
289
|
+
return () => {
|
|
290
|
+
disposed = true;
|
|
291
|
+
pending = null;
|
|
292
|
+
ws.removeEventListener("open", onOpen);
|
|
293
|
+
ws.removeEventListener("message", onMessage);
|
|
294
|
+
ws.removeEventListener("error", onError);
|
|
295
|
+
ws.removeEventListener("close", onClose);
|
|
296
|
+
try {
|
|
297
|
+
ws.close();
|
|
298
|
+
} catch {
|
|
299
|
+
}
|
|
300
|
+
canvas.remove();
|
|
301
|
+
};
|
|
302
|
+
}, [url, token, transport, nonce]);
|
|
303
|
+
return { state, error, reconnect };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// src/hooks/use-desktop-stream.ts
|
|
307
|
+
import {
|
|
308
|
+
desktopSocketUrl,
|
|
309
|
+
nextDesktopState
|
|
310
|
+
} from "@opengeni/sdk";
|
|
311
|
+
import { useCallback, useEffect as useEffect2, useRef as useRef2, useState as useState2 } from "react";
|
|
312
|
+
async function defaultRfbFactory() {
|
|
313
|
+
const mod = await import("@novnc/novnc");
|
|
314
|
+
const RFB = mod.default;
|
|
315
|
+
return (target, url, opts) => new RFB(target, url, opts);
|
|
316
|
+
}
|
|
317
|
+
function useDesktopStream(options) {
|
|
318
|
+
const {
|
|
319
|
+
capability,
|
|
320
|
+
containerRef,
|
|
321
|
+
interactive,
|
|
322
|
+
scaleViewport,
|
|
323
|
+
rfbFactory,
|
|
324
|
+
webSocketProtocols,
|
|
325
|
+
webSocketFactory
|
|
326
|
+
} = options;
|
|
327
|
+
const frameStream = useRelayFrameStream({
|
|
328
|
+
capability,
|
|
329
|
+
containerRef,
|
|
330
|
+
...webSocketFactory ? { webSocketFactory } : {}
|
|
331
|
+
});
|
|
332
|
+
const [state, setState] = useState2("idle");
|
|
333
|
+
const [error, setError] = useState2(null);
|
|
334
|
+
const [nonce, setNonce] = useState2(0);
|
|
335
|
+
const stateRef = useRef2("idle");
|
|
336
|
+
const setBoth = (next) => {
|
|
337
|
+
stateRef.current = next;
|
|
338
|
+
setState(next);
|
|
339
|
+
};
|
|
340
|
+
const reconnect = () => setNonce((n) => n + 1);
|
|
341
|
+
const sendKey = useCallback((keysym, code, down) => {
|
|
342
|
+
const rfb = rfbRef.current;
|
|
343
|
+
if (!rfb?.sendKey || rfb.viewOnly || stateRef.current !== "connected") return false;
|
|
344
|
+
rfb.sendKey(keysym, code, down);
|
|
345
|
+
return true;
|
|
346
|
+
}, []);
|
|
347
|
+
const url = capability?.url ?? null;
|
|
348
|
+
const token = capability?.token ?? null;
|
|
349
|
+
const transport = capability?.transport ?? null;
|
|
350
|
+
const mode = capability?.mode ?? "read-only";
|
|
351
|
+
const webSocketProtocolsKey = webSocketProtocols?.join("\0") ?? "";
|
|
352
|
+
const rfbRef = useRef2(null);
|
|
353
|
+
const interactiveRef = useRef2(interactive);
|
|
354
|
+
const scaleViewportRef = useRef2(scaleViewport);
|
|
355
|
+
const modeRef = useRef2(mode);
|
|
356
|
+
const rfbFactoryRef = useRef2(rfbFactory);
|
|
357
|
+
const webSocketProtocolsRef = useRef2(webSocketProtocols);
|
|
358
|
+
interactiveRef.current = interactive;
|
|
359
|
+
scaleViewportRef.current = scaleViewport;
|
|
360
|
+
modeRef.current = mode;
|
|
361
|
+
rfbFactoryRef.current = rfbFactory;
|
|
362
|
+
webSocketProtocolsRef.current = webSocketProtocols;
|
|
363
|
+
const viewOnlyFor = (m, want) => m === "read-only" || !want;
|
|
364
|
+
useEffect2(() => {
|
|
365
|
+
if (typeof window === "undefined") return;
|
|
366
|
+
if (transport !== "vnc-ws" || !url) {
|
|
367
|
+
setBoth("idle");
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
const container = containerRef.current;
|
|
371
|
+
if (!container) {
|
|
372
|
+
setBoth("idle");
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
let rfb = null;
|
|
376
|
+
let disposed = false;
|
|
377
|
+
setError(null);
|
|
378
|
+
setBoth("negotiating");
|
|
379
|
+
const onConnect = () => {
|
|
380
|
+
if (!disposed) setBoth(nextDesktopState(stateRef.current, { type: "connected" }));
|
|
381
|
+
};
|
|
382
|
+
const onDisconnect = () => {
|
|
383
|
+
if (!disposed) setBoth(nextDesktopState(stateRef.current, { type: "disconnected" }));
|
|
384
|
+
};
|
|
385
|
+
const onSecurityFailure = () => {
|
|
386
|
+
if (disposed) return;
|
|
387
|
+
setError(new Error("desktop authentication failed (token expired or revoked)"));
|
|
388
|
+
setBoth(nextDesktopState(stateRef.current, { type: "fail" }));
|
|
389
|
+
};
|
|
390
|
+
void (async () => {
|
|
391
|
+
try {
|
|
392
|
+
const factory = rfbFactoryRef.current ?? await defaultRfbFactory();
|
|
393
|
+
if (disposed) return;
|
|
394
|
+
const socketUrl = desktopSocketUrl({ url });
|
|
395
|
+
setBoth(nextDesktopState(stateRef.current, { type: "negotiated" }));
|
|
396
|
+
rfb = factory(container, socketUrl, {
|
|
397
|
+
credentials: token ? { password: token } : void 0,
|
|
398
|
+
...webSocketProtocolsRef.current ? { wsProtocols: webSocketProtocolsRef.current } : {}
|
|
399
|
+
});
|
|
400
|
+
rfb.viewOnly = viewOnlyFor(modeRef.current, interactiveRef.current);
|
|
401
|
+
rfb.clipViewport = false;
|
|
402
|
+
rfb.scaleViewport = scaleViewportRef.current ?? true;
|
|
403
|
+
rfb.addEventListener("connect", onConnect);
|
|
404
|
+
rfb.addEventListener("disconnect", onDisconnect);
|
|
405
|
+
rfb.addEventListener("securityfailure", onSecurityFailure);
|
|
406
|
+
rfbRef.current = rfb;
|
|
407
|
+
} catch (cause) {
|
|
408
|
+
if (!disposed) {
|
|
409
|
+
setError(cause instanceof Error ? cause : new Error(String(cause)));
|
|
410
|
+
setBoth("error");
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
})();
|
|
414
|
+
return () => {
|
|
415
|
+
disposed = true;
|
|
416
|
+
if (rfbRef.current === rfb) rfbRef.current = null;
|
|
417
|
+
if (rfb) {
|
|
418
|
+
rfb.removeEventListener?.("connect", onConnect);
|
|
419
|
+
rfb.removeEventListener?.("disconnect", onDisconnect);
|
|
420
|
+
rfb.removeEventListener?.("securityfailure", onSecurityFailure);
|
|
421
|
+
try {
|
|
422
|
+
rfb.disconnect();
|
|
423
|
+
} catch {
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
}, [url, token, transport, webSocketProtocolsKey, nonce]);
|
|
428
|
+
useEffect2(() => {
|
|
429
|
+
const rfb = rfbRef.current;
|
|
430
|
+
if (rfb) rfb.viewOnly = viewOnlyFor(mode, interactive);
|
|
431
|
+
}, [interactive, mode, state]);
|
|
432
|
+
useEffect2(() => {
|
|
433
|
+
const rfb = rfbRef.current;
|
|
434
|
+
if (rfb) {
|
|
435
|
+
rfb.clipViewport = false;
|
|
436
|
+
rfb.scaleViewport = scaleViewport ?? true;
|
|
437
|
+
}
|
|
438
|
+
}, [scaleViewport, state]);
|
|
439
|
+
if (transport === "relay-frames") return frameStream;
|
|
440
|
+
return { state, error, reconnect, sendKey };
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// src/components/desktop-viewer.tsx
|
|
444
|
+
import { LoaderCircleIcon, MonitorIcon, MousePointerClickIcon, WifiOffIcon } from "lucide-react";
|
|
445
|
+
import {
|
|
446
|
+
useEffect as useEffect3,
|
|
447
|
+
useRef as useRef3,
|
|
448
|
+
useState as useState3
|
|
449
|
+
} from "react";
|
|
450
|
+
|
|
451
|
+
// src/lib/host-platform.ts
|
|
452
|
+
function interactionHostPlatform() {
|
|
453
|
+
if (typeof navigator === "undefined") return "other";
|
|
454
|
+
const platform = navigator.platform || navigator.userAgent;
|
|
455
|
+
return /Mac|iPhone|iPad|iPod/u.test(platform) ? "mac" : "other";
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// src/components/desktop-viewer.tsx
|
|
459
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
460
|
+
var DESKTOP_MEDIA_BACKGROUND = "#000";
|
|
461
|
+
function isHardUnavailable(reason) {
|
|
462
|
+
switch (reason) {
|
|
463
|
+
case "backend_unsupported":
|
|
464
|
+
case "os_unsupported":
|
|
465
|
+
case "not_provisioned":
|
|
466
|
+
case "disabled_by_policy":
|
|
467
|
+
case "tier_headless":
|
|
468
|
+
return true;
|
|
469
|
+
default:
|
|
470
|
+
return false;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
function DesktopViewer({
|
|
474
|
+
capability,
|
|
475
|
+
interactive,
|
|
476
|
+
showControlToggle = true,
|
|
477
|
+
scaleViewport,
|
|
478
|
+
rfbFactory,
|
|
479
|
+
webSocketProtocols,
|
|
480
|
+
webSocketFactory,
|
|
481
|
+
onPasteText,
|
|
482
|
+
targetPlatform,
|
|
483
|
+
onActivate,
|
|
484
|
+
onAcknowledge,
|
|
485
|
+
watching,
|
|
486
|
+
onWarm,
|
|
487
|
+
renderUnavailable,
|
|
488
|
+
renderWarming,
|
|
489
|
+
renderViewerCap,
|
|
490
|
+
viewerCapReached,
|
|
491
|
+
connectTimeoutMs = 13e3,
|
|
492
|
+
className
|
|
493
|
+
}) {
|
|
494
|
+
const containerRef = useRef3(null);
|
|
495
|
+
const onActivateRef = useRef3(onActivate);
|
|
496
|
+
const onAcknowledgeRef = useRef3(onAcknowledge);
|
|
497
|
+
onActivateRef.current = onActivate;
|
|
498
|
+
onAcknowledgeRef.current = onAcknowledge;
|
|
499
|
+
const acknowledgmentAttemptedRef = useRef3(false);
|
|
500
|
+
const [acknowledgmentError, setAcknowledgmentError] = useState3(null);
|
|
501
|
+
const [acknowledgmentRetryNonce, setAcknowledgmentRetryNonce] = useState3(0);
|
|
502
|
+
const [takeControl, setTakeControl] = useState3(interactive ?? false);
|
|
503
|
+
const externallyControlled = interactive !== void 0;
|
|
504
|
+
const serverAllowsControl = capability?.mode !== "read-only";
|
|
505
|
+
const wantControl = externallyControlled ? interactive : takeControl;
|
|
506
|
+
const inControl = Boolean(wantControl) && serverAllowsControl;
|
|
507
|
+
const isWatching = watching ?? true;
|
|
508
|
+
const transportNull = !capability || capability.transport === null;
|
|
509
|
+
const reason = capability?.reason ?? null;
|
|
510
|
+
const needsAcknowledgment = capability?.requiresAcknowledgment === true && capability.acknowledged !== true;
|
|
511
|
+
const noLiveAddress = Boolean(capability) && !transportNull && !capability.url;
|
|
512
|
+
const coldWarmable = transportNull && reason === "lease_cold" || !transportNull && noLiveAddress;
|
|
513
|
+
const hardUnavailable = transportNull && isHardUnavailable(reason);
|
|
514
|
+
useEffect3(() => {
|
|
515
|
+
onActivateRef.current?.();
|
|
516
|
+
}, []);
|
|
517
|
+
useEffect3(() => {
|
|
518
|
+
if (!needsAcknowledgment) {
|
|
519
|
+
acknowledgmentAttemptedRef.current = false;
|
|
520
|
+
setAcknowledgmentError(null);
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
if (acknowledgmentAttemptedRef.current) return;
|
|
524
|
+
acknowledgmentAttemptedRef.current = true;
|
|
525
|
+
setAcknowledgmentError(null);
|
|
526
|
+
const acknowledge = onAcknowledgeRef.current;
|
|
527
|
+
if (!acknowledge) {
|
|
528
|
+
setAcknowledgmentError(new Error("Desktop acknowledgment is not configured."));
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
let active = true;
|
|
532
|
+
void Promise.resolve().then(() => acknowledge()).catch((cause) => {
|
|
533
|
+
if (!active) return;
|
|
534
|
+
setAcknowledgmentError(cause instanceof Error ? cause : new Error(String(cause)));
|
|
535
|
+
});
|
|
536
|
+
return () => {
|
|
537
|
+
active = false;
|
|
538
|
+
};
|
|
539
|
+
}, [needsAcknowledgment, acknowledgmentRetryNonce]);
|
|
540
|
+
const retryAcknowledgment = () => {
|
|
541
|
+
acknowledgmentAttemptedRef.current = false;
|
|
542
|
+
setAcknowledgmentError(null);
|
|
543
|
+
setAcknowledgmentRetryNonce((nonce) => nonce + 1);
|
|
544
|
+
};
|
|
545
|
+
useEffect3(() => {
|
|
546
|
+
if (!inControl || externallyControlled) return;
|
|
547
|
+
const onKey = (event) => {
|
|
548
|
+
if (event.ctrlKey && event.altKey && event.shiftKey && (event.key === "Control" || event.key === "Alt" || event.key === "Shift")) {
|
|
549
|
+
event.preventDefault();
|
|
550
|
+
setTakeControl(false);
|
|
551
|
+
}
|
|
552
|
+
};
|
|
553
|
+
window.addEventListener("keydown", onKey);
|
|
554
|
+
return () => {
|
|
555
|
+
window.removeEventListener("keydown", onKey);
|
|
556
|
+
};
|
|
557
|
+
}, [inControl, externallyControlled]);
|
|
558
|
+
const connectCapability = !transportNull && !needsAcknowledgment && !viewerCapReached ? capability : null;
|
|
559
|
+
const stream = useDesktopStream({
|
|
560
|
+
capability: connectCapability,
|
|
561
|
+
containerRef,
|
|
562
|
+
interactive: inControl,
|
|
563
|
+
...scaleViewport !== void 0 ? { scaleViewport } : {},
|
|
564
|
+
...rfbFactory ? { rfbFactory } : {},
|
|
565
|
+
...webSocketProtocols ? { webSocketProtocols } : {},
|
|
566
|
+
...webSocketFactory ? { webSocketFactory } : {}
|
|
567
|
+
});
|
|
568
|
+
const connected = stream.state === "connected";
|
|
569
|
+
const hasLiveUrl = Boolean(connectCapability?.url);
|
|
570
|
+
const streamStateRef = useRef3(stream.state);
|
|
571
|
+
streamStateRef.current = stream.state;
|
|
572
|
+
const warmKeyRef = useRef3(null);
|
|
573
|
+
useEffect3(() => {
|
|
574
|
+
if (!isWatching || !coldWarmable || needsAcknowledgment || viewerCapReached) {
|
|
575
|
+
if (!coldWarmable) warmKeyRef.current = null;
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
if (!onWarm) return;
|
|
579
|
+
const key = `${capability?.reason ?? ""}:${capability?.url ?? ""}:${capability?.expiresAt ?? ""}`;
|
|
580
|
+
if (warmKeyRef.current === key) return;
|
|
581
|
+
warmKeyRef.current = key;
|
|
582
|
+
onWarm();
|
|
583
|
+
}, [isWatching, coldWarmable, needsAcknowledgment, viewerCapReached, capability, onWarm]);
|
|
584
|
+
useEffect3(() => {
|
|
585
|
+
if (!hasLiveUrl || typeof document === "undefined") return;
|
|
586
|
+
const maybeReattach = () => {
|
|
587
|
+
if (document.visibilityState !== "visible") return;
|
|
588
|
+
if (streamStateRef.current === "idle") stream.reconnect();
|
|
589
|
+
};
|
|
590
|
+
document.addEventListener("visibilitychange", maybeReattach);
|
|
591
|
+
return () => document.removeEventListener("visibilitychange", maybeReattach);
|
|
592
|
+
}, [hasLiveUrl, connectCapability?.url]);
|
|
593
|
+
const [connectTimedOut, setConnectTimedOut] = useState3(false);
|
|
594
|
+
const [reconnectNonce, setReconnectNonce] = useState3(0);
|
|
595
|
+
useEffect3(() => {
|
|
596
|
+
setConnectTimedOut(false);
|
|
597
|
+
if (!hasLiveUrl || connected || stream.error || connectTimeoutMs <= 0) return;
|
|
598
|
+
const timer = setTimeout(() => setConnectTimedOut(true), connectTimeoutMs);
|
|
599
|
+
return () => clearTimeout(timer);
|
|
600
|
+
}, [
|
|
601
|
+
hasLiveUrl,
|
|
602
|
+
connected,
|
|
603
|
+
connectTimeoutMs,
|
|
604
|
+
connectCapability?.url,
|
|
605
|
+
stream.error,
|
|
606
|
+
reconnectNonce
|
|
607
|
+
]);
|
|
608
|
+
const reconnect = () => {
|
|
609
|
+
setConnectTimedOut(false);
|
|
610
|
+
setReconnectNonce((n) => n + 1);
|
|
611
|
+
stream.reconnect();
|
|
612
|
+
};
|
|
613
|
+
const paste = (event) => {
|
|
614
|
+
if (!inControl) return;
|
|
615
|
+
const text = event.clipboardData.getData("text/plain");
|
|
616
|
+
if (!text && !event.clipboardData.types.includes("text/plain")) return;
|
|
617
|
+
if (!onPasteText?.(text)) return;
|
|
618
|
+
event.preventDefault();
|
|
619
|
+
event.stopPropagation();
|
|
620
|
+
};
|
|
621
|
+
const primaryShortcut = (event) => {
|
|
622
|
+
if (!inControl || !targetPlatform) return;
|
|
623
|
+
const hostPlatform = interactionHostPlatform();
|
|
624
|
+
if (isHostPrimaryModifier(event, hostPlatform)) {
|
|
625
|
+
event.stopPropagation();
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
const chord = desktopPrimaryShortcut(event, hostPlatform, targetPlatform);
|
|
629
|
+
if (!chord) return;
|
|
630
|
+
if (chord === "clipboard") {
|
|
631
|
+
event.stopPropagation();
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
const sent = chord.every(({ keysym, code, down }) => stream.sendKey?.(keysym, code, down));
|
|
635
|
+
if (!sent) return;
|
|
636
|
+
event.preventDefault();
|
|
637
|
+
event.stopPropagation();
|
|
638
|
+
};
|
|
639
|
+
const primaryModifierUp = (event) => {
|
|
640
|
+
if (inControl && isHostPrimaryModifier(event, interactionHostPlatform())) {
|
|
641
|
+
event.stopPropagation();
|
|
642
|
+
}
|
|
643
|
+
};
|
|
644
|
+
let uiState;
|
|
645
|
+
if (viewerCapReached) uiState = "viewer_cap";
|
|
646
|
+
else if (hardUnavailable) uiState = "unavailable";
|
|
647
|
+
else if (acknowledgmentError) uiState = "acknowledgment_error";
|
|
648
|
+
else if (needsAcknowledgment) uiState = "opening";
|
|
649
|
+
else if (coldWarmable) uiState = "warming";
|
|
650
|
+
else if (connected) uiState = "connected";
|
|
651
|
+
else if (stream.error) uiState = "error";
|
|
652
|
+
else if (connectTimedOut) uiState = "connect_failed";
|
|
653
|
+
else uiState = "connecting";
|
|
654
|
+
const overlayShown = uiState !== "connected" && uiState !== "connecting" && uiState !== "opening";
|
|
655
|
+
const showToggle = showControlToggle && !overlayShown;
|
|
656
|
+
let overlay = null;
|
|
657
|
+
switch (uiState) {
|
|
658
|
+
case "viewer_cap":
|
|
659
|
+
overlay = renderViewerCap?.() ?? defaultNotice(
|
|
660
|
+
"Too many viewers",
|
|
661
|
+
"This session has reached its live-viewer limit. Try again shortly."
|
|
662
|
+
);
|
|
663
|
+
break;
|
|
664
|
+
case "unavailable":
|
|
665
|
+
overlay = renderUnavailable?.(reason) ?? defaultNotice("Desktop unavailable", unavailableCopy(reason));
|
|
666
|
+
break;
|
|
667
|
+
case "acknowledgment_error":
|
|
668
|
+
overlay = defaultNotice(
|
|
669
|
+
"Couldn\u2019t open desktop",
|
|
670
|
+
acknowledgmentError?.message ?? "The desktop acknowledgment failed.",
|
|
671
|
+
retryAcknowledgment
|
|
672
|
+
);
|
|
673
|
+
break;
|
|
674
|
+
case "warming":
|
|
675
|
+
overlay = renderWarming?.() ?? /* @__PURE__ */ jsx(WarmingNotice, {});
|
|
676
|
+
break;
|
|
677
|
+
case "connect_failed":
|
|
678
|
+
overlay = defaultNotice(
|
|
679
|
+
"Couldn\u2019t connect",
|
|
680
|
+
"The desktop is warm but the live stream didn\u2019t come up. This usually clears on a retry.",
|
|
681
|
+
reconnect
|
|
682
|
+
);
|
|
683
|
+
break;
|
|
684
|
+
case "error":
|
|
685
|
+
overlay = defaultNotice(
|
|
686
|
+
"Desktop disconnected",
|
|
687
|
+
stream.error?.message ?? "The stream dropped.",
|
|
688
|
+
reconnect
|
|
689
|
+
);
|
|
690
|
+
break;
|
|
691
|
+
case "opening":
|
|
692
|
+
case "connecting":
|
|
693
|
+
case "connected":
|
|
694
|
+
overlay = null;
|
|
695
|
+
break;
|
|
696
|
+
}
|
|
697
|
+
return /* @__PURE__ */ jsxs(
|
|
698
|
+
"div",
|
|
699
|
+
{
|
|
700
|
+
className: cn(
|
|
701
|
+
"relative h-full w-full overflow-hidden",
|
|
702
|
+
inControl && "ring-2 ring-inset ring-og-accent",
|
|
703
|
+
className
|
|
704
|
+
),
|
|
705
|
+
style: { backgroundColor: DESKTOP_MEDIA_BACKGROUND },
|
|
706
|
+
"data-opengeni-desktop": true,
|
|
707
|
+
"data-state": stream.state,
|
|
708
|
+
"data-ui-state": uiState,
|
|
709
|
+
"data-in-control": inControl ? "true" : void 0,
|
|
710
|
+
"data-target-platform": targetPlatform ?? void 0,
|
|
711
|
+
onKeyDownCapture: primaryShortcut,
|
|
712
|
+
onKeyUpCapture: primaryModifierUp,
|
|
713
|
+
onPasteCapture: paste,
|
|
714
|
+
children: [
|
|
715
|
+
/* @__PURE__ */ jsx(
|
|
716
|
+
"div",
|
|
717
|
+
{
|
|
718
|
+
ref: containerRef,
|
|
719
|
+
className: "absolute inset-0 overflow-hidden",
|
|
720
|
+
"data-opengeni-desktop-canvas": true,
|
|
721
|
+
"data-state": stream.state
|
|
722
|
+
}
|
|
723
|
+
),
|
|
724
|
+
(uiState === "opening" || uiState === "connecting") && /* @__PURE__ */ jsxs("div", { className: "pointer-events-none absolute inset-0 flex flex-col items-center justify-center gap-3 text-og-fg-subtle", children: [
|
|
725
|
+
/* @__PURE__ */ jsxs("span", { className: "relative flex items-center justify-center", children: [
|
|
726
|
+
/* @__PURE__ */ jsx(MonitorIcon, { className: "size-8 opacity-30", strokeWidth: 1.5 }),
|
|
727
|
+
/* @__PURE__ */ jsx(
|
|
728
|
+
LoaderCircleIcon,
|
|
729
|
+
{
|
|
730
|
+
className: "absolute size-12 animate-og-spin text-og-accent opacity-70",
|
|
731
|
+
strokeWidth: 1.25
|
|
732
|
+
}
|
|
733
|
+
)
|
|
734
|
+
] }),
|
|
735
|
+
/* @__PURE__ */ jsx("span", { className: "text-og-sm", children: uiState === "opening" ? "Opening the desktop\u2026" : "Connecting to the desktop\u2026" })
|
|
736
|
+
] }),
|
|
737
|
+
showToggle && !externallyControlled && inControl && /* @__PURE__ */ jsx(
|
|
738
|
+
InControlBar,
|
|
739
|
+
{
|
|
740
|
+
shared: capability?.shared ?? false,
|
|
741
|
+
onRelease: () => setTakeControl(false)
|
|
742
|
+
}
|
|
743
|
+
),
|
|
744
|
+
showToggle && !externallyControlled && !inControl && connected && /* @__PURE__ */ jsx(
|
|
745
|
+
TakeControlCallToAction,
|
|
746
|
+
{
|
|
747
|
+
disabled: !serverAllowsControl,
|
|
748
|
+
disabledReason: !serverAllowsControl ? capability?.client === "frames" ? "View-only \u2014 live control isn't available for this machine yet." : "This deployment streams the desktop read-only." : void 0,
|
|
749
|
+
onTakeControl: () => setTakeControl(true)
|
|
750
|
+
}
|
|
751
|
+
),
|
|
752
|
+
overlay && /* @__PURE__ */ jsx("div", { className: "absolute inset-0 flex items-center justify-center p-4", children: overlay })
|
|
753
|
+
]
|
|
754
|
+
}
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
function isHostPrimaryModifier(event, hostPlatform) {
|
|
758
|
+
return event.key === (hostPlatform === "mac" ? "Meta" : "Control");
|
|
759
|
+
}
|
|
760
|
+
function desktopPrimaryShortcut(event, hostPlatform, targetPlatform) {
|
|
761
|
+
const primaryHeld = hostPlatform === "mac" ? event.metaKey : event.ctrlKey;
|
|
762
|
+
if (!primaryHeld || [...event.key].length !== 1) return null;
|
|
763
|
+
const key = event.key.toLowerCase();
|
|
764
|
+
if (!event.altKey && (key === "c" || key === "v")) return "clipboard";
|
|
765
|
+
const primary = targetPlatform === "macos" ? { keysym: 65515, code: "MetaLeft" } : { keysym: 65507, code: "ControlLeft" };
|
|
766
|
+
const modifiers = [
|
|
767
|
+
primary,
|
|
768
|
+
...event.altKey ? [{ keysym: 65513, code: "AltLeft" }] : [],
|
|
769
|
+
...event.shiftKey ? [{ keysym: 65505, code: "ShiftLeft" }] : []
|
|
770
|
+
];
|
|
771
|
+
const keyEvent = {
|
|
772
|
+
keysym: key.codePointAt(0),
|
|
773
|
+
code: event.code || (/^[a-z]$/u.test(key) ? `Key${key.toUpperCase()}` : key)
|
|
774
|
+
};
|
|
775
|
+
return [
|
|
776
|
+
...modifiers.map((modifier) => ({ ...modifier, down: true })),
|
|
777
|
+
{ ...keyEvent, down: true },
|
|
778
|
+
{ ...keyEvent, down: false },
|
|
779
|
+
...modifiers.reverse().map((modifier) => ({ ...modifier, down: false }))
|
|
780
|
+
];
|
|
781
|
+
}
|
|
782
|
+
function TakeControlCallToAction({
|
|
783
|
+
disabled,
|
|
784
|
+
disabledReason,
|
|
785
|
+
onTakeControl
|
|
786
|
+
}) {
|
|
787
|
+
return (
|
|
788
|
+
// The wrapper spans the surface but is click-through (pointer-events-none); only
|
|
789
|
+
// the button itself is interactive, so watchers can still see the desktop.
|
|
790
|
+
/* @__PURE__ */ jsx("div", { className: "pointer-events-none absolute inset-0 flex items-end justify-center pb-[8%]", children: /* @__PURE__ */ jsxs(
|
|
791
|
+
"button",
|
|
792
|
+
{
|
|
793
|
+
type: "button",
|
|
794
|
+
disabled,
|
|
795
|
+
"aria-label": "Take control of the desktop",
|
|
796
|
+
title: disabled ? disabledReason : "Take control of the desktop",
|
|
797
|
+
onClick: onTakeControl,
|
|
798
|
+
className: cn(
|
|
799
|
+
"group pointer-events-auto flex items-center gap-3 rounded-og-lg border px-5 py-3",
|
|
800
|
+
"border-og-border",
|
|
801
|
+
"bg-og-bg/85 backdrop-blur-md",
|
|
802
|
+
"shadow-og-lg",
|
|
803
|
+
"outline-hidden transition-all duration-150 ease-out",
|
|
804
|
+
"focus-visible:ring-2 focus-visible:ring-og-accent focus-visible:ring-offset-2 focus-visible:ring-offset-og-bg",
|
|
805
|
+
disabled ? "cursor-not-allowed opacity-60" : cn(
|
|
806
|
+
"cursor-pointer opacity-90 hover:-translate-y-0.5 hover:opacity-100",
|
|
807
|
+
"hover:border-og-accent",
|
|
808
|
+
"hover:bg-og-accent/10"
|
|
809
|
+
)
|
|
810
|
+
),
|
|
811
|
+
children: [
|
|
812
|
+
/* @__PURE__ */ jsx(
|
|
813
|
+
"span",
|
|
814
|
+
{
|
|
815
|
+
className: cn(
|
|
816
|
+
"flex size-9 shrink-0 items-center justify-center rounded-full transition-colors",
|
|
817
|
+
"bg-og-accent",
|
|
818
|
+
"text-og-accent-fg",
|
|
819
|
+
disabled ? "" : "group-hover:scale-105"
|
|
820
|
+
),
|
|
821
|
+
children: /* @__PURE__ */ jsx(MousePointerClickIcon, { className: "size-5", strokeWidth: 2 })
|
|
822
|
+
}
|
|
823
|
+
),
|
|
824
|
+
/* @__PURE__ */ jsxs("span", { className: "flex flex-col items-start leading-tight", children: [
|
|
825
|
+
/* @__PURE__ */ jsx("span", { className: "text-og-base font-semibold text-og-fg", children: "Take control" }),
|
|
826
|
+
/* @__PURE__ */ jsx("span", { className: "text-og-xs text-og-fg-subtle", children: disabled && disabledReason ? disabledReason : "Drive the mouse & keyboard" })
|
|
827
|
+
] })
|
|
828
|
+
]
|
|
829
|
+
}
|
|
830
|
+
) })
|
|
831
|
+
);
|
|
832
|
+
}
|
|
833
|
+
function InControlBar({ shared, onRelease }) {
|
|
834
|
+
return /* @__PURE__ */ jsxs("div", { className: "pointer-events-none absolute inset-x-0 top-0 flex items-center justify-between gap-2 p-2", children: [
|
|
835
|
+
/* @__PURE__ */ jsxs("span", { className: "pointer-events-auto inline-flex items-center gap-2 rounded-og-sm bg-og-accent px-2.5 py-1 text-og-xs font-medium text-og-accent-fg shadow-og-md", children: [
|
|
836
|
+
/* @__PURE__ */ jsx("span", { className: "size-1.5 animate-pulse rounded-full bg-current", "aria-hidden": true }),
|
|
837
|
+
"You're in control",
|
|
838
|
+
/* @__PURE__ */ jsx("span", { className: "opacity-75", children: "\xB7 click Return control (or Ctrl+Alt+Shift)" })
|
|
839
|
+
] }),
|
|
840
|
+
/* @__PURE__ */ jsxs("div", { className: "pointer-events-auto flex items-center gap-1.5", children: [
|
|
841
|
+
shared && /* @__PURE__ */ jsx("span", { className: "rounded-og-sm bg-og-status-failed/85 px-2 py-0.5 text-og-xs text-og-accent-fg", children: "Shared sandbox \u2014 others are watching" }),
|
|
842
|
+
/* @__PURE__ */ jsxs(
|
|
843
|
+
"button",
|
|
844
|
+
{
|
|
845
|
+
type: "button",
|
|
846
|
+
onClick: onRelease,
|
|
847
|
+
title: "Return control (or press Ctrl+Alt+Shift)",
|
|
848
|
+
className: cn(
|
|
849
|
+
"inline-flex items-center gap-1.5 rounded-og-sm border px-2.5 py-1 text-og-xs font-semibold shadow-og-md transition-colors",
|
|
850
|
+
"border-og-accent bg-og-bg/90 text-og-fg backdrop-blur-sm",
|
|
851
|
+
"outline-hidden hover:bg-og-accent hover:text-og-accent-fg focus-visible:ring-2 focus-visible:ring-og-accent"
|
|
852
|
+
),
|
|
853
|
+
children: [
|
|
854
|
+
/* @__PURE__ */ jsx(MonitorIcon, { className: "size-3.5", strokeWidth: 2, "aria-hidden": true }),
|
|
855
|
+
"Return control"
|
|
856
|
+
]
|
|
857
|
+
}
|
|
858
|
+
)
|
|
859
|
+
] })
|
|
860
|
+
] });
|
|
861
|
+
}
|
|
862
|
+
function unavailableCopy(reason) {
|
|
863
|
+
switch (reason) {
|
|
864
|
+
case "backend_unsupported":
|
|
865
|
+
return "This sandbox backend cannot stream a desktop.";
|
|
866
|
+
case "tier_headless":
|
|
867
|
+
return "This deployment is headless \u2014 terminal, files, and diff only.";
|
|
868
|
+
case "os_unsupported":
|
|
869
|
+
return "The sandbox OS does not support a desktop stream.";
|
|
870
|
+
case "not_provisioned":
|
|
871
|
+
return "This sandbox doesn't have a desktop yet.";
|
|
872
|
+
case "disabled_by_policy":
|
|
873
|
+
return "Desktop streaming is disabled on this deployment.";
|
|
874
|
+
case "lease_cold":
|
|
875
|
+
return "Waiting for the sandbox to start\u2026";
|
|
876
|
+
default:
|
|
877
|
+
return "The desktop isn\u2019t available for this sandbox.";
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
function WarmingNotice({ onRetry }) {
|
|
881
|
+
return /* @__PURE__ */ jsxs("div", { className: "flex max-w-sm flex-col items-center gap-3 rounded-og-lg border border-og-border bg-og-bg/90 p-5 text-center text-og-base text-og-fg backdrop-blur-sm", children: [
|
|
882
|
+
/* @__PURE__ */ jsx(LoaderCircleIcon, { className: "size-7 animate-og-spin text-og-accent", strokeWidth: 1.5 }),
|
|
883
|
+
/* @__PURE__ */ jsxs("div", { className: "space-y-1", children: [
|
|
884
|
+
/* @__PURE__ */ jsx("div", { className: "font-medium", children: "Warming the sandbox\u2026" }),
|
|
885
|
+
/* @__PURE__ */ jsx("p", { className: "text-og-sm text-og-fg-subtle", children: "Spinning up the desktop \u2014 this takes a few seconds." })
|
|
886
|
+
] }),
|
|
887
|
+
onRetry && /* @__PURE__ */ jsx(
|
|
888
|
+
"button",
|
|
889
|
+
{
|
|
890
|
+
type: "button",
|
|
891
|
+
onClick: onRetry,
|
|
892
|
+
className: "rounded-og-sm border border-og-border px-3 py-1.5 text-og-sm text-og-fg-muted transition-colors hover:text-og-fg pointer-coarse:min-h-11",
|
|
893
|
+
children: "Taking too long? Retry"
|
|
894
|
+
}
|
|
895
|
+
)
|
|
896
|
+
] });
|
|
897
|
+
}
|
|
898
|
+
function defaultNotice(title, body, onRetry) {
|
|
899
|
+
return /* @__PURE__ */ jsxs("div", { className: "max-w-sm rounded-og-lg border border-og-border bg-og-bg p-4 text-center text-og-base text-og-fg", children: [
|
|
900
|
+
/* @__PURE__ */ jsxs("div", { className: "mb-1 flex items-center justify-center gap-1.5 font-medium", children: [
|
|
901
|
+
onRetry && /* @__PURE__ */ jsx(WifiOffIcon, { className: "size-4 opacity-70", strokeWidth: 1.75 }),
|
|
902
|
+
title
|
|
903
|
+
] }),
|
|
904
|
+
/* @__PURE__ */ jsx("p", { className: "text-og-sm text-og-fg-subtle", children: body }),
|
|
905
|
+
onRetry && /* @__PURE__ */ jsx(
|
|
906
|
+
"button",
|
|
907
|
+
{
|
|
908
|
+
type: "button",
|
|
909
|
+
onClick: onRetry,
|
|
910
|
+
className: "mt-3 rounded-og-sm border border-og-border px-3 py-1.5 text-og-sm transition-colors hover:border-og-accent pointer-coarse:min-h-11",
|
|
911
|
+
children: "Reconnect"
|
|
912
|
+
}
|
|
913
|
+
)
|
|
914
|
+
] });
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
export {
|
|
918
|
+
STREAM_KIND_PTY,
|
|
919
|
+
STREAM_KIND_BROWSER,
|
|
920
|
+
STREAM_KIND_COMPUTER,
|
|
921
|
+
STREAM_ROLE_CLIENT,
|
|
922
|
+
STREAM_CLOSE_REASON_NORMAL,
|
|
923
|
+
encodeStreamOpen,
|
|
924
|
+
encodeStreamClose,
|
|
925
|
+
encodeStreamFrame,
|
|
926
|
+
decodeStreamOpenAck,
|
|
927
|
+
decodeStreamFrame,
|
|
928
|
+
useRelayFrameStream,
|
|
929
|
+
useDesktopStream,
|
|
930
|
+
interactionHostPlatform,
|
|
931
|
+
DesktopViewer
|
|
932
|
+
};
|
|
933
|
+
//# sourceMappingURL=chunk-R3AZOAB5.js.map
|