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