@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.
Files changed (132) hide show
  1. package/README.md +51 -1
  2. package/dist/artifacts.d.ts +1 -1
  3. package/dist/artifacts.js +43 -14
  4. package/dist/artifacts.js.map +1 -1
  5. package/dist/{chunk-ROIRTM37.js → chunk-5UQPNLIO.js} +6 -6
  6. package/dist/{chunk-JR3QFY6B.js → chunk-6KLX2RH5.js} +49 -2
  7. package/dist/chunk-6KLX2RH5.js.map +1 -0
  8. package/dist/{chunk-FWS3KC3Z.js → chunk-B6WPZSTT.js} +104 -104
  9. package/dist/{chunk-FWS3KC3Z.js.map → chunk-B6WPZSTT.js.map} +1 -1
  10. package/dist/{chunk-HDBG7XGO.js → chunk-C5ZPSSS5.js} +50 -1
  11. package/dist/chunk-C5ZPSSS5.js.map +1 -0
  12. package/dist/{chunk-X4S56Y2H.js → chunk-HA46CYQK.js} +135 -11
  13. package/dist/chunk-HA46CYQK.js.map +1 -0
  14. package/dist/{chunk-724BW4TW.js → chunk-IGC6A2ZI.js} +2 -2
  15. package/dist/chunk-LLOLUHVG.js +5937 -0
  16. package/dist/chunk-LLOLUHVG.js.map +1 -0
  17. package/dist/{chunk-WYA65Y3F.js → chunk-MKPIAGVL.js} +13 -3
  18. package/dist/chunk-MKPIAGVL.js.map +1 -0
  19. package/dist/chunk-R3AZOAB5.js +933 -0
  20. package/dist/chunk-R3AZOAB5.js.map +1 -0
  21. package/dist/{chunk-23EJ676W.js → chunk-XBZWOM2K.js} +9 -4
  22. package/dist/chunk-XBZWOM2K.js.map +1 -0
  23. package/dist/{chunk-JAGDUCZQ.js → chunk-Y7OEDUJ5.js} +17 -13
  24. package/dist/chunk-Y7OEDUJ5.js.map +1 -0
  25. package/dist/client.d.ts +6 -4
  26. package/dist/components/artifacts/index.d.ts +1 -0
  27. package/dist/components/artifacts/published-html-artifact-frame.d.ts +14 -0
  28. package/dist/components/browser-input.d.ts +7 -0
  29. package/dist/components/browser-viewer.d.ts +5 -4
  30. package/dist/components/computer-viewer.d.ts +2 -1
  31. package/dist/components/desktop-viewer.d.ts +26 -2
  32. package/dist/components/interaction-intervention-banner.d.ts +11 -0
  33. package/dist/components/sandbox-terminal.d.ts +4 -1
  34. package/dist/components/sandbox-workspace.d.ts +3 -0
  35. package/dist/composer.js +6 -6
  36. package/dist/hooks/use-auth-runs.d.ts +32 -0
  37. package/dist/hooks/use-browser-downloads.d.ts +25 -0
  38. package/dist/hooks/use-browser-identities.d.ts +5 -1
  39. package/dist/hooks/use-browser-session.d.ts +2 -1
  40. package/dist/hooks/use-browser-sessions.d.ts +1 -1
  41. package/dist/hooks/use-channels.d.ts +26 -0
  42. package/dist/hooks/use-computer-frame-stream.d.ts +1 -1
  43. package/dist/hooks/use-computer-session.d.ts +2 -1
  44. package/dist/hooks/use-computer-sessions.d.ts +1 -1
  45. package/dist/hooks/use-desktop-stream.d.ts +4 -0
  46. package/dist/hooks/use-interaction-interventions.d.ts +27 -0
  47. package/dist/hooks/use-interaction-invalidation.d.ts +19 -0
  48. package/dist/hooks/use-network-routes.d.ts +25 -0
  49. package/dist/hooks/use-site-auth-connections.d.ts +25 -0
  50. package/dist/hooks/use-supergrok-accounts.d.ts +26 -0
  51. package/dist/hooks/use-terminal-stream.d.ts +3 -0
  52. package/dist/index.d.ts +4 -0
  53. package/dist/index.js +1220 -1545
  54. package/dist/index.js.map +1 -1
  55. package/dist/interaction.d.ts +13 -3
  56. package/dist/interaction.js +520 -6
  57. package/dist/interaction.js.map +1 -1
  58. package/dist/interactive-workbench-viewers-TOHBXJEE.js +15 -0
  59. package/dist/lib/host-platform.d.ts +5 -0
  60. package/dist/lib/relay-wire.d.ts +20 -1
  61. package/dist/machines.js +3 -3
  62. package/dist/model-policy.d.ts +1 -1
  63. package/dist/model-policy.js +1 -1
  64. package/dist/provider.d.ts +3 -2
  65. package/dist/realtime/realtime-control.d.ts +6 -2
  66. package/dist/realtime.js +138 -24
  67. package/dist/realtime.js.map +1 -1
  68. package/dist/session-context.d.ts +10 -2
  69. package/dist/session-ui.js +3 -3
  70. package/dist/session.js +11 -11
  71. package/dist/timeline/types.d.ts +5 -0
  72. package/package.json +2 -2
  73. package/src/artifacts.ts +3 -0
  74. package/src/client.ts +73 -3
  75. package/src/components/artifacts/index.ts +5 -0
  76. package/src/components/artifacts/published-html-artifact-frame.tsx +38 -0
  77. package/src/components/browser-input.ts +65 -0
  78. package/src/components/browser-viewer.tsx +1287 -177
  79. package/src/components/computer-viewer.tsx +611 -114
  80. package/src/components/desktop-viewer.tsx +114 -1
  81. package/src/components/interaction-intervention-banner.tsx +99 -0
  82. package/src/components/machines/machine-detail.tsx +1 -1
  83. package/src/components/model-policy-picker.tsx +15 -0
  84. package/src/components/sandbox-terminal.tsx +24 -7
  85. package/src/components/sandbox-workspace.tsx +42 -4
  86. package/src/components/workspace-dock.tsx +59 -5
  87. package/src/hooks/use-attached-browsers.ts +13 -11
  88. package/src/hooks/use-auth-runs.ts +278 -0
  89. package/src/hooks/use-browser-downloads.ts +218 -0
  90. package/src/hooks/use-browser-frame-stream.ts +168 -59
  91. package/src/hooks/use-browser-identities.ts +47 -6
  92. package/src/hooks/use-browser-session.ts +94 -27
  93. package/src/hooks/use-browser-sessions.ts +19 -14
  94. package/src/hooks/use-channels.ts +99 -0
  95. package/src/hooks/use-computer-frame-stream.ts +184 -61
  96. package/src/hooks/use-computer-session.ts +22 -2
  97. package/src/hooks/use-computer-sessions.ts +19 -13
  98. package/src/hooks/use-desktop-stream.ts +39 -7
  99. package/src/hooks/use-interaction-interventions.ts +242 -0
  100. package/src/hooks/use-interaction-invalidation.ts +66 -0
  101. package/src/hooks/use-machine-chip.ts +13 -3
  102. package/src/hooks/use-network-routes.ts +214 -0
  103. package/src/hooks/use-site-auth-connections.ts +226 -0
  104. package/src/hooks/use-supergrok-accounts.ts +81 -0
  105. package/src/hooks/use-terminal-stream.ts +288 -55
  106. package/src/index.ts +8 -0
  107. package/src/interaction.ts +30 -0
  108. package/src/lib/host-platform.ts +10 -0
  109. package/src/lib/relay-wire.ts +43 -4
  110. package/src/lib/terminal-capability.ts +1 -1
  111. package/src/model-policy.ts +19 -4
  112. package/src/provider.tsx +133 -1
  113. package/src/realtime/realtime-control.tsx +181 -26
  114. package/src/session-context.ts +66 -1
  115. package/src/timeline/activity-rail.tsx +6 -0
  116. package/src/timeline/projection.ts +58 -0
  117. package/src/timeline/types.ts +2 -0
  118. package/styles/compiled.css +64 -6
  119. package/dist/chunk-23EJ676W.js.map +0 -1
  120. package/dist/chunk-6BBWWOOU.js +0 -82
  121. package/dist/chunk-6BBWWOOU.js.map +0 -1
  122. package/dist/chunk-7I2KB3OK.js +0 -3895
  123. package/dist/chunk-7I2KB3OK.js.map +0 -1
  124. package/dist/chunk-HDBG7XGO.js.map +0 -1
  125. package/dist/chunk-JAGDUCZQ.js.map +0 -1
  126. package/dist/chunk-JR3QFY6B.js.map +0 -1
  127. package/dist/chunk-WYA65Y3F.js.map +0 -1
  128. package/dist/chunk-X4S56Y2H.js.map +0 -1
  129. package/dist/interactive-workbench-viewers-KHNOHWQE.js +0 -13
  130. /package/dist/{chunk-ROIRTM37.js.map → chunk-5UQPNLIO.js.map} +0 -0
  131. /package/dist/{chunk-724BW4TW.js.map → chunk-IGC6A2ZI.js.map} +0 -0
  132. /package/dist/{interactive-workbench-viewers-KHNOHWQE.js.map → interactive-workbench-viewers-TOHBXJEE.js.map} +0 -0
@@ -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