@opengeni/react 1.0.1 → 2.3.0-canary.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -10
- package/dist/artifacts-spreadsheet.js +1 -1
- package/dist/artifacts.js +1 -1
- package/dist/browser-viewer-GXUICIAG.js +14 -0
- package/dist/{chunk-R3AZOAB5.js → chunk-4MDLEHXL.js} +9 -115
- package/dist/chunk-4MDLEHXL.js.map +1 -0
- package/dist/chunk-5YK2ZDIG.js +116 -0
- package/dist/chunk-5YK2ZDIG.js.map +1 -0
- package/dist/chunk-H5YGYNXS.js +329 -0
- package/dist/chunk-H5YGYNXS.js.map +1 -0
- package/dist/{chunk-7WCLAEFV.js → chunk-ICA6R6TW.js} +2 -2
- package/dist/{chunk-7WCLAEFV.js.map → chunk-ICA6R6TW.js.map} +1 -1
- package/dist/{chunk-IGC6A2ZI.js → chunk-JOAVA2TW.js} +589 -217
- package/dist/chunk-JOAVA2TW.js.map +1 -0
- package/dist/{chunk-Y7OEDUJ5.js → chunk-LS4POUTY.js} +1057 -426
- package/dist/chunk-LS4POUTY.js.map +1 -0
- package/dist/{chunk-5UQPNLIO.js → chunk-LTCHJ4MS.js} +9 -9
- package/dist/chunk-LTCHJ4MS.js.map +1 -0
- package/dist/{chunk-C5ZPSSS5.js → chunk-NEXLLTP4.js} +2 -1
- package/dist/{chunk-C5ZPSSS5.js.map → chunk-NEXLLTP4.js.map} +1 -1
- package/dist/chunk-QGBJ6GHG.js +2300 -0
- package/dist/chunk-QGBJ6GHG.js.map +1 -0
- package/dist/{chunk-6KLX2RH5.js → chunk-RSXJN73N.js} +365 -21
- package/dist/chunk-RSXJN73N.js.map +1 -0
- package/dist/{chunk-B6WPZSTT.js → chunk-SE4W4WQ3.js} +78 -16
- package/dist/chunk-SE4W4WQ3.js.map +1 -0
- package/dist/{chunk-MKPIAGVL.js → chunk-W6NFJXSA.js} +8 -19
- package/dist/chunk-W6NFJXSA.js.map +1 -0
- package/dist/{chunk-XBZWOM2K.js → chunk-X4A66EWZ.js} +8 -3
- package/dist/chunk-X4A66EWZ.js.map +1 -0
- package/dist/chunk-X4JAFD5R.js +3907 -0
- package/dist/chunk-X4JAFD5R.js.map +1 -0
- package/dist/{chunk-HA46CYQK.js → chunk-XXWH2JLC.js} +792 -206
- package/dist/chunk-XXWH2JLC.js.map +1 -0
- package/dist/client.d.ts +2 -2
- package/dist/components/browser-viewer.d.ts +7 -1
- package/dist/components/computer-viewer.d.ts +4 -1
- package/dist/components/enrollment-consent.d.ts +6 -3
- package/dist/components/file-browser.d.ts +7 -1
- package/dist/components/human-input-form.d.ts +1 -1
- package/dist/components/human-input-surface.d.ts +1 -1
- package/dist/components/machine-card.d.ts +4 -1
- package/dist/components/machines/machine-detail.d.ts +4 -1
- package/dist/components/machines/machine-operation-policy.d.ts +11 -0
- package/dist/components/machines-dashboard.d.ts +3 -1
- package/dist/components/markdown.d.ts +20 -1
- package/dist/components/message-timeline.d.ts +1 -1
- package/dist/components/model-policy-picker.d.ts +2 -0
- package/dist/components/pierre-file.d.ts +1 -1
- package/dist/components/sandbox-files.d.ts +8 -2
- package/dist/components/sandbox-workspace.d.ts +33 -3
- package/dist/components/session-chrome.d.ts +15 -3
- package/dist/components/workspace-dock.d.ts +6 -2
- package/dist/composer.js +5 -5
- package/dist/computer-viewer-ZOP4Q7A7.js +16 -0
- package/dist/computer-viewer-ZOP4Q7A7.js.map +1 -0
- package/dist/hooks/channel-cache.d.ts +23 -0
- package/dist/hooks/shared-machines-list.d.ts +14 -0
- package/dist/hooks/use-attached-browsers.d.ts +2 -1
- package/dist/hooks/use-channels.d.ts +1 -0
- package/dist/hooks/use-composer.d.ts +37 -9
- package/dist/hooks/use-machine-chip.d.ts +3 -3
- package/dist/hooks/use-machines.d.ts +19 -1
- package/dist/hooks/use-sandbox-files.d.ts +1 -0
- package/dist/hooks/use-session-background-commands.d.ts +14 -0
- package/dist/hooks/use-turn-queue.d.ts +12 -0
- package/dist/hooks/use-workspace-sessions.d.ts +1 -0
- package/dist/index.d.ts +7 -5
- package/dist/index.js +1096 -623
- package/dist/index.js.map +1 -1
- package/dist/interaction.js +29 -181
- package/dist/interaction.js.map +1 -1
- package/dist/lib/interaction-errors.d.ts +3 -0
- package/dist/machines.d.ts +4 -2
- package/dist/machines.js +10 -2
- package/dist/model-policy.d.ts +1 -1
- package/dist/model-policy.js +1 -1
- package/dist/platform-activity-row-BJHUHXAT.js +169 -0
- package/dist/platform-activity-row-BJHUHXAT.js.map +1 -0
- package/dist/{queue-surface-implementation-2KVJTYXH.js → queue-surface-implementation-BSKUWHRI.js} +76 -16
- package/dist/queue-surface-implementation-BSKUWHRI.js.map +1 -0
- package/dist/realtime/realtime-control.d.ts +6 -3
- package/dist/realtime.js +36 -14
- package/dist/realtime.js.map +1 -1
- package/dist/session-ui.d.ts +1 -1
- package/dist/session-ui.js +6 -2
- package/dist/session.d.ts +2 -2
- package/dist/session.js +4 -4
- package/dist/timeline/index.d.ts +1 -1
- package/dist/timeline/platform-activity-row.d.ts +15 -0
- package/dist/timeline/screenshot-lightbox.d.ts +1 -1
- package/dist/timeline/shared.d.ts +4 -2
- package/dist/timeline/types.d.ts +50 -5
- package/dist/types/machines.d.ts +2 -2
- package/package.json +3 -3
- package/src/client.ts +12 -1
- package/src/components/artifacts/editable-spreadsheet.tsx +1 -1
- package/src/components/browser-viewer.tsx +361 -58
- package/src/components/chat-composer.tsx +1 -1
- package/src/components/composer.tsx +6 -6
- package/src/components/computer-viewer.tsx +368 -115
- package/src/components/enrollment-consent.tsx +64 -6
- package/src/components/file-browser.tsx +117 -2
- package/src/components/human-input-form.tsx +103 -59
- package/src/components/human-input-surface.tsx +1 -1
- package/src/components/machine-card.tsx +111 -0
- package/src/components/machine-input-display.ts +15 -0
- package/src/components/machines/machine-detail.tsx +47 -0
- package/src/components/machines/machine-operation-policy.tsx +179 -0
- package/src/components/machines-dashboard.tsx +6 -0
- package/src/components/markdown.tsx +174 -20
- package/src/components/message-timeline.tsx +215 -18
- package/src/components/model-policy-picker.tsx +9 -17
- package/src/components/pierre-file.tsx +1 -1
- package/src/components/queue-surface-implementation.tsx +82 -16
- package/src/components/sandbox-files.tsx +135 -19
- package/src/components/sandbox-workspace.tsx +204 -92
- package/src/components/session-chrome.tsx +415 -208
- package/src/components/session-status.tsx +1 -1
- package/src/components/user-message-body.tsx +168 -48
- package/src/components/workspace-dock.tsx +141 -175
- package/src/hooks/channel-cache.ts +104 -0
- package/src/hooks/shared-machines-list.ts +239 -0
- package/src/hooks/use-attached-browsers.ts +5 -1
- package/src/hooks/use-browser-frame-stream.ts +6 -1
- package/src/hooks/use-browser-session.ts +6 -4
- package/src/hooks/use-browser-sessions.ts +7 -1
- package/src/hooks/use-channels.ts +32 -10
- package/src/hooks/use-composer.ts +760 -291
- package/src/hooks/use-computer-frame-stream.ts +14 -9
- package/src/hooks/use-computer-session.ts +16 -14
- package/src/hooks/use-computer-sessions.ts +8 -1
- package/src/hooks/use-last-started-turn-policy.ts +1 -1
- package/src/hooks/use-machine-chip.ts +23 -6
- package/src/hooks/use-machines.ts +104 -6
- package/src/hooks/use-sandbox-files.ts +195 -140
- package/src/hooks/use-sandbox-git.ts +58 -29
- package/src/hooks/use-session-background-commands.ts +56 -0
- package/src/hooks/use-session-capabilities.ts +18 -0
- package/src/hooks/use-turn-queue.ts +111 -12
- package/src/hooks/use-workspace-sessions.ts +5 -1
- package/src/index.ts +26 -4
- package/src/lib/interaction-errors.ts +18 -0
- package/src/machines.ts +9 -1
- package/src/model-policy.ts +10 -2
- package/src/realtime/realtime-control.tsx +53 -17
- package/src/session-context.ts +1 -0
- package/src/session-ui.ts +6 -1
- package/src/session.ts +9 -1
- package/src/timeline/activity-rail.tsx +20 -75
- package/src/timeline/index.ts +4 -0
- package/src/timeline/platform-activity-row.tsx +201 -0
- package/src/timeline/projection.ts +529 -25
- package/src/timeline/screenshot-lightbox.tsx +48 -14
- package/src/timeline/shared.tsx +8 -2
- package/src/timeline/tool-renderers.tsx +104 -6
- package/src/timeline/turn-summary.tsx +6 -1
- package/src/timeline/types.ts +77 -2
- package/src/types/machines.ts +10 -0
- package/styles/compiled.css +115 -104
- package/dist/chunk-5UQPNLIO.js.map +0 -1
- package/dist/chunk-6KLX2RH5.js.map +0 -1
- package/dist/chunk-B6WPZSTT.js.map +0 -1
- package/dist/chunk-HA46CYQK.js.map +0 -1
- package/dist/chunk-IGC6A2ZI.js.map +0 -1
- package/dist/chunk-LLOLUHVG.js +0 -5937
- package/dist/chunk-LLOLUHVG.js.map +0 -1
- package/dist/chunk-MKPIAGVL.js.map +0 -1
- package/dist/chunk-R3AZOAB5.js.map +0 -1
- package/dist/chunk-XBZWOM2K.js.map +0 -1
- package/dist/chunk-Y7OEDUJ5.js.map +0 -1
- package/dist/interactive-workbench-viewers-TOHBXJEE.js +0 -15
- package/dist/queue-surface-implementation-2KVJTYXH.js.map +0 -1
- /package/dist/{interactive-workbench-viewers-TOHBXJEE.js.map → browser-viewer-GXUICIAG.js.map} +0 -0
|
@@ -211,10 +211,10 @@ export function useComputerFrameStream(
|
|
|
211
211
|
const frame = await decodeComputerFrameMessage(bytes);
|
|
212
212
|
if (disposed || source !== socket) continue;
|
|
213
213
|
if (frame.computerSessionId !== computerSessionId || frame.targetId !== targetId) {
|
|
214
|
-
throw new Error("
|
|
214
|
+
throw new Error("desktop frame belongs to another resource");
|
|
215
215
|
}
|
|
216
216
|
if (!controllerGeneration || frame.controllerGeneration !== controllerGeneration) {
|
|
217
|
-
throw new Error("
|
|
217
|
+
throw new Error("desktop frame belongs to a stale controller");
|
|
218
218
|
}
|
|
219
219
|
const key = `${computerSessionId}:${targetId}:${frame.controllerGeneration}:${frame.targetGeneration}`;
|
|
220
220
|
if (latestRef.current.key === key && frame.sequence <= latestRef.current.sequence)
|
|
@@ -246,7 +246,7 @@ export function useComputerFrameStream(
|
|
|
246
246
|
if (disposed || source !== socket) return;
|
|
247
247
|
let frameBytes = bytes;
|
|
248
248
|
if (activeStream?.kind === "relay") {
|
|
249
|
-
if (bytes.length < 1) throw new Error("
|
|
249
|
+
if (bytes.length < 1) throw new Error("desktop relay returned an empty message");
|
|
250
250
|
const tag = bytes[0];
|
|
251
251
|
const body = bytes.subarray(1);
|
|
252
252
|
if (tag === RELAY_TAG_OPEN_ACK) {
|
|
@@ -255,7 +255,7 @@ export function useComputerFrameStream(
|
|
|
255
255
|
activeAttachment = null;
|
|
256
256
|
activeStream = null;
|
|
257
257
|
attachmentExpiresAt = 0;
|
|
258
|
-
throw new Error(ack.error?.message ?? "
|
|
258
|
+
throw new Error(ack.error?.message ?? "desktop stream was rejected by the relay");
|
|
259
259
|
}
|
|
260
260
|
relayAccepted = true;
|
|
261
261
|
return;
|
|
@@ -269,7 +269,7 @@ export function useComputerFrameStream(
|
|
|
269
269
|
attachmentExpiresAt = 0;
|
|
270
270
|
lastRelaySequence = null;
|
|
271
271
|
relayAccepted = false;
|
|
272
|
-
fail(new Error("
|
|
272
|
+
fail(new Error("Desktop frame source ended."), false);
|
|
273
273
|
return;
|
|
274
274
|
}
|
|
275
275
|
if (tag !== RELAY_TAG_FRAME || !relayAccepted) return;
|
|
@@ -290,7 +290,7 @@ export function useComputerFrameStream(
|
|
|
290
290
|
|
|
291
291
|
const onError = (source: ComputerFrameWebSocket) => {
|
|
292
292
|
if (source !== socket) return;
|
|
293
|
-
fail(new Error("
|
|
293
|
+
fail(new Error("Desktop view lost connection."));
|
|
294
294
|
};
|
|
295
295
|
|
|
296
296
|
const onClose = (source: ComputerFrameWebSocket) => {
|
|
@@ -363,7 +363,7 @@ export function useComputerFrameStream(
|
|
|
363
363
|
attachment.computerSessionId !== computerSessionId ||
|
|
364
364
|
attachment.targetId !== targetId
|
|
365
365
|
) {
|
|
366
|
-
throw new Error("
|
|
366
|
+
throw new Error("desktop attachment does not match the requested resource");
|
|
367
367
|
}
|
|
368
368
|
controllerGeneration = attachment.controllerGeneration;
|
|
369
369
|
activeAttachment = attachment;
|
|
@@ -397,7 +397,7 @@ export function useComputerFrameStream(
|
|
|
397
397
|
connectSocket();
|
|
398
398
|
} catch (cause) {
|
|
399
399
|
if (attachmentAbort.signal.aborted || disposed) return;
|
|
400
|
-
fail(cause);
|
|
400
|
+
fail(cause, !isPlacementGenerationLossError(cause));
|
|
401
401
|
}
|
|
402
402
|
};
|
|
403
403
|
|
|
@@ -430,5 +430,10 @@ async function messageBytes(value: unknown): Promise<Uint8Array> {
|
|
|
430
430
|
if (typeof Blob !== "undefined" && value instanceof Blob) {
|
|
431
431
|
return new Uint8Array(await value.arrayBuffer());
|
|
432
432
|
}
|
|
433
|
-
throw new Error("
|
|
433
|
+
throw new Error("desktop frame stream returned a non-binary message");
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function isPlacementGenerationLossError(cause: unknown): boolean {
|
|
437
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
438
|
+
return /placement instance changed|controller authority changed/i.test(message);
|
|
434
439
|
}
|
|
@@ -8,6 +8,7 @@ import type {
|
|
|
8
8
|
ComputerTarget,
|
|
9
9
|
} from "@opengeni/sdk/interaction";
|
|
10
10
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
11
|
+
import { isNonRetryableInteractionError } from "../lib/interaction-errors";
|
|
11
12
|
import {
|
|
12
13
|
type EmbeddedComputerInteractionClientOverride,
|
|
13
14
|
useEmbeddedComputerInteraction,
|
|
@@ -55,6 +56,7 @@ export function useComputerSession(options: UseComputerSessionOptions): UseCompu
|
|
|
55
56
|
);
|
|
56
57
|
const visible =
|
|
57
58
|
state.computerSessionId === computerSessionId ? state : emptyState(computerSessionId, enabled);
|
|
59
|
+
const refreshBlocked = isNonRetryableInteractionError(visible.error);
|
|
58
60
|
const selectedTargetIdRef = useRef<string | null>(visible.selectedTargetId);
|
|
59
61
|
selectedTargetIdRef.current = visible.selectedTargetId;
|
|
60
62
|
const targetsRef = useRef<{
|
|
@@ -173,15 +175,15 @@ export function useComputerSession(options: UseComputerSessionOptions): UseCompu
|
|
|
173
175
|
}, [computerSessionId, enabled, invalidateRefresh, refresh]);
|
|
174
176
|
|
|
175
177
|
useEffect(() => {
|
|
176
|
-
if (!enabled || !pageLive || visible.mutating) return;
|
|
178
|
+
if (!enabled || !pageLive || visible.mutating || refreshBlocked) return;
|
|
177
179
|
const timer = setInterval(() => {
|
|
178
180
|
if (!requestRef.current.controller) void refresh();
|
|
179
181
|
}, pollIntervalMs);
|
|
180
182
|
return () => clearInterval(timer);
|
|
181
|
-
}, [enabled, pageLive, pollIntervalMs, refresh, visible.mutating]);
|
|
183
|
+
}, [enabled, pageLive, pollIntervalMs, refresh, refreshBlocked, visible.mutating]);
|
|
182
184
|
|
|
183
185
|
useEffect(() => {
|
|
184
|
-
if (!enabled || !computerSessionId || !pageLive) return;
|
|
186
|
+
if (!enabled || !computerSessionId || !pageLive || refreshBlocked) return;
|
|
185
187
|
let disposed = false;
|
|
186
188
|
const timer = setInterval(() => {
|
|
187
189
|
void client.heartbeatComputerSession(workspaceId, computerSessionId).catch(() => {
|
|
@@ -192,7 +194,7 @@ export function useComputerSession(options: UseComputerSessionOptions): UseCompu
|
|
|
192
194
|
disposed = true;
|
|
193
195
|
clearInterval(timer);
|
|
194
196
|
};
|
|
195
|
-
}, [client, computerSessionId, enabled, pageLive, refresh, workspaceId]);
|
|
197
|
+
}, [client, computerSessionId, enabled, pageLive, refresh, refreshBlocked, workspaceId]);
|
|
196
198
|
|
|
197
199
|
const runMutation = useCallback(
|
|
198
200
|
async <T>(scopeComputerSessionId: string, operation: () => Promise<T>): Promise<T> => {
|
|
@@ -234,9 +236,9 @@ export function useComputerSession(options: UseComputerSessionOptions): UseCompu
|
|
|
234
236
|
|
|
235
237
|
const selectTarget = useCallback(
|
|
236
238
|
async (targetId: string): Promise<ComputerTarget> => {
|
|
237
|
-
if (!computerSessionId) throw new Error("No
|
|
239
|
+
if (!computerSessionId) throw new Error("No desktop is selected.");
|
|
238
240
|
const target = targetsRef.current.targets.find((candidate) => candidate.id === targetId);
|
|
239
|
-
if (!target) throw new Error("The selected
|
|
241
|
+
if (!target) throw new Error("The selected desktop target is no longer available.");
|
|
240
242
|
invalidateRefresh();
|
|
241
243
|
selectedTargetIdRef.current = target.id;
|
|
242
244
|
observationRef.current = { computerSessionId, observation: null };
|
|
@@ -284,9 +286,9 @@ export function useComputerSession(options: UseComputerSessionOptions): UseCompu
|
|
|
284
286
|
operationId: string,
|
|
285
287
|
frame: ComputerFrame | null,
|
|
286
288
|
): Promise<ComputerActionReceipt> => {
|
|
287
|
-
if (!computerSessionId) throw new Error("No
|
|
289
|
+
if (!computerSessionId) throw new Error("No desktop is selected.");
|
|
288
290
|
if (frame && frame.computerSessionId !== computerSessionId) {
|
|
289
|
-
throw new Error("The displayed
|
|
291
|
+
throw new Error("The displayed desktop frame belongs to another desktop session.");
|
|
290
292
|
}
|
|
291
293
|
const currentObservation =
|
|
292
294
|
observationRef.current.computerSessionId === computerSessionId
|
|
@@ -302,18 +304,18 @@ export function useComputerSession(options: UseComputerSessionOptions): UseCompu
|
|
|
302
304
|
(currentObservation?.target.id === frame.targetId ? currentObservation.target : null))
|
|
303
305
|
: null;
|
|
304
306
|
const target = frameTarget ?? focusTarget ?? currentObservation?.target ?? null;
|
|
305
|
-
if (!target) throw new Error("The
|
|
307
|
+
if (!target) throw new Error("The desktop target is not ready for input.");
|
|
306
308
|
if (frame && frame.targetId !== selectedTargetIdRef.current) {
|
|
307
|
-
throw new Error("The displayed
|
|
309
|
+
throw new Error("The displayed desktop frame is no longer selected.");
|
|
308
310
|
}
|
|
309
311
|
if (action.type === "pointer") {
|
|
310
312
|
const expectedFrameId = frame?.frameId ?? currentObservation?.frameId ?? null;
|
|
311
313
|
if (!expectedFrameId || action.frameId !== expectedFrameId) {
|
|
312
|
-
throw new Error("Pointer input must reference the exact displayed
|
|
314
|
+
throw new Error("Pointer input must reference the exact displayed desktop frame.");
|
|
313
315
|
}
|
|
314
316
|
}
|
|
315
317
|
if (action.type === "semantic" && !currentObservation) {
|
|
316
|
-
throw new Error("The
|
|
318
|
+
throw new Error("The desktop accessibility tree is not ready for input.");
|
|
317
319
|
}
|
|
318
320
|
|
|
319
321
|
return await runMutation(computerSessionId, async () => {
|
|
@@ -367,7 +369,7 @@ export function useComputerSession(options: UseComputerSessionOptions): UseCompu
|
|
|
367
369
|
);
|
|
368
370
|
|
|
369
371
|
const readClipboard = useCallback(async (): Promise<ComputerClipboard> => {
|
|
370
|
-
if (!computerSessionId) throw new Error("No
|
|
372
|
+
if (!computerSessionId) throw new Error("No desktop is selected.");
|
|
371
373
|
return await client.readComputerClipboard(workspaceId, computerSessionId);
|
|
372
374
|
}, [client, computerSessionId, workspaceId]);
|
|
373
375
|
|
|
@@ -432,9 +434,9 @@ function chooseTarget(
|
|
|
432
434
|
return (
|
|
433
435
|
targets.find((target) => target.focused && target.kind === "window") ??
|
|
434
436
|
targets.find((target) => target.kind === "window") ??
|
|
437
|
+
targets.find((target) => target.kind === "screen") ??
|
|
435
438
|
targets.find((target) => target.focused && target.kind === "app") ??
|
|
436
439
|
targets.find((target) => target.kind === "app") ??
|
|
437
|
-
targets.find((target) => target.kind === "screen") ??
|
|
438
440
|
targets[0] ??
|
|
439
441
|
null
|
|
440
442
|
);
|
|
@@ -74,6 +74,7 @@ export function useComputerSessions(
|
|
|
74
74
|
requestRef.current = { id, controller };
|
|
75
75
|
setState((current) => ({
|
|
76
76
|
...(current.workspaceId === workspaceId ? current : emptyState(workspaceId, true)),
|
|
77
|
+
active: true,
|
|
77
78
|
loading: foreground,
|
|
78
79
|
refreshing: !foreground,
|
|
79
80
|
error: foreground || current.workspaceId !== workspaceId ? null : current.error,
|
|
@@ -85,6 +86,7 @@ export function useComputerSessions(
|
|
|
85
86
|
if (!mountedRef.current || requestRef.current.id !== id) return;
|
|
86
87
|
setState({
|
|
87
88
|
workspaceId,
|
|
89
|
+
active: true,
|
|
88
90
|
revision: response.revision,
|
|
89
91
|
sessions: sortComputerSessions(response.sessions),
|
|
90
92
|
loading: false,
|
|
@@ -187,7 +189,10 @@ export function useComputerSessions(
|
|
|
187
189
|
revision: visible.revision,
|
|
188
190
|
sessions: visible.sessions,
|
|
189
191
|
relevantSessions,
|
|
190
|
-
|
|
192
|
+
// A disabled registry is deliberately empty. When its surface becomes
|
|
193
|
+
// visible again, expose that transition as loading in the same render so
|
|
194
|
+
// consumers cannot mistake the stale empty snapshot for a completed list.
|
|
195
|
+
loading: enabled ? !visible.active || visible.loading : false,
|
|
191
196
|
refreshing: visible.refreshing,
|
|
192
197
|
error: visible.error,
|
|
193
198
|
refresh,
|
|
@@ -198,6 +203,7 @@ export function useComputerSessions(
|
|
|
198
203
|
|
|
199
204
|
type ComputerRegistryState = {
|
|
200
205
|
workspaceId: string;
|
|
206
|
+
active: boolean;
|
|
201
207
|
revision: number;
|
|
202
208
|
sessions: ComputerSession[];
|
|
203
209
|
loading: boolean;
|
|
@@ -208,6 +214,7 @@ type ComputerRegistryState = {
|
|
|
208
214
|
function emptyState(workspaceId: string, loading: boolean): ComputerRegistryState {
|
|
209
215
|
return {
|
|
210
216
|
workspaceId,
|
|
217
|
+
active: loading,
|
|
211
218
|
revision: 0,
|
|
212
219
|
sessions: [],
|
|
213
220
|
loading,
|
|
@@ -33,7 +33,7 @@ function policyFromTurn(turn: SessionTurn | null | undefined): LastStartedTurnPo
|
|
|
33
33
|
return {
|
|
34
34
|
model: turn.model,
|
|
35
35
|
reasoningEffort: turn.reasoningEffort,
|
|
36
|
-
latencyMode: turn.latencyMode
|
|
36
|
+
latencyMode: turn.latencyMode,
|
|
37
37
|
turnId: turn.id,
|
|
38
38
|
};
|
|
39
39
|
}
|
|
@@ -4,13 +4,13 @@ import type { SessionCapabilitiesState } from "./use-session-capabilities";
|
|
|
4
4
|
import { sandboxAcceptsLiveIo } from "../lib/sandbox-liveness";
|
|
5
5
|
import { connectionStatusForState } from "../types/machines";
|
|
6
6
|
|
|
7
|
-
/** The one truthful machine indicator.
|
|
8
|
-
*
|
|
7
|
+
/** The one truthful machine indicator. Managed cold compute is sleeping; a
|
|
8
|
+
* Connected Machine that is genuinely unreachable remains offline. */
|
|
9
9
|
export type MachineChipState = "live" | "waking" | "offline";
|
|
10
10
|
|
|
11
11
|
export type MachineChip = {
|
|
12
12
|
state: MachineChipState;
|
|
13
|
-
/** A ready-to-render label ("Live" / "Waking…" / "
|
|
13
|
+
/** A ready-to-render label ("Live" / "Waking…" / "Sleeping · saved 3m ago"). */
|
|
14
14
|
label: string;
|
|
15
15
|
/** The capture time backing an offline/stale label (ISO), or null. M4 may
|
|
16
16
|
* reformat this; `label` already embeds a relative form. */
|
|
@@ -64,7 +64,8 @@ export function formatAsOf(capturedAt: string, now: number): string {
|
|
|
64
64
|
export function deriveMachineChip(input: DeriveMachineChipInput): MachineChip {
|
|
65
65
|
const now = input.now ?? Date.now();
|
|
66
66
|
const asOf = input.capturedAt ?? null;
|
|
67
|
-
const
|
|
67
|
+
const managedRestingLabel = asOf ? `Sleeping · saved ${formatAsOf(asOf, now)}` : "Sleeping";
|
|
68
|
+
const offlineLabel = "Offline";
|
|
68
69
|
|
|
69
70
|
// A connected machine has no Modal group lease, so capability negotiation
|
|
70
71
|
// legitimately reports `cold` until a stream holder is minted. Its fleet
|
|
@@ -86,6 +87,16 @@ export function deriveMachineChip(input: DeriveMachineChipInput): MachineChip {
|
|
|
86
87
|
const selfhostedOffline =
|
|
87
88
|
input.activeIsSelfhosted === true && input.activeMachineState === "offline";
|
|
88
89
|
|
|
90
|
+
// Capability negotiation has definitively failed. A retained warm intent
|
|
91
|
+
// must not turn that error into the optimistic Waking state.
|
|
92
|
+
if (input.capabilitiesState === "error") {
|
|
93
|
+
return {
|
|
94
|
+
state: "offline",
|
|
95
|
+
label: input.activeIsSelfhosted === true ? offlineLabel : managedRestingLabel,
|
|
96
|
+
asOf,
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
89
100
|
// 2. Actively coming up: negotiation in flight, an edit/terminal is warming, or
|
|
90
101
|
// the active machine is (re)connecting. Never when self-hosted is hard-offline.
|
|
91
102
|
const machineWarming =
|
|
@@ -97,8 +108,14 @@ export function deriveMachineChip(input: DeriveMachineChipInput): MachineChip {
|
|
|
97
108
|
return { state: "waking", label: "Waking…", asOf };
|
|
98
109
|
}
|
|
99
110
|
|
|
100
|
-
// 3.
|
|
101
|
-
|
|
111
|
+
// 3. Managed cold compute is sleeping with saved-workspace freshness. A
|
|
112
|
+
// Connected Machine is only described as Offline when its reachability is
|
|
113
|
+
// genuinely unavailable; capture time is not a heartbeat.
|
|
114
|
+
return {
|
|
115
|
+
state: "offline",
|
|
116
|
+
label: input.activeIsSelfhosted === true ? offlineLabel : managedRestingLabel,
|
|
117
|
+
asOf,
|
|
118
|
+
};
|
|
102
119
|
}
|
|
103
120
|
|
|
104
121
|
export type UseMachineChipOptions = DeriveMachineChipInput;
|
|
@@ -2,12 +2,34 @@ import { useCallback, useRef, useState } from "react";
|
|
|
2
2
|
import type {
|
|
3
3
|
RemoveEnrollmentRequest,
|
|
4
4
|
RemoveEnrollmentResponse,
|
|
5
|
+
MachineOperationPolicy,
|
|
5
6
|
SwapActiveSandboxResponse,
|
|
7
|
+
UpdateMachineOperationPolicyRequest,
|
|
8
|
+
UpdateMachineAgentResponse,
|
|
6
9
|
} from "@opengeni/sdk";
|
|
7
10
|
import { useOpenGeni, type ClientOverride } from "../provider";
|
|
8
|
-
import { useMutationRunner
|
|
11
|
+
import { useMutationRunner } from "./internal";
|
|
12
|
+
import { EMPTY_MACHINES, useSharedMachinesList } from "./shared-machines-list";
|
|
9
13
|
import type { MachinesResponse, MachineView, MetricSample } from "../types/machines";
|
|
10
14
|
|
|
15
|
+
/** Dashboard list: cheap heartbeat read, keep the pill reasonably fresh. */
|
|
16
|
+
export const MACHINES_DASHBOARD_POLL_MS = 10_000;
|
|
17
|
+
/** Session surfaces share one poll for the same workspace+session. */
|
|
18
|
+
export const MACHINES_SESSION_POLL_MS = 15_000;
|
|
19
|
+
/** Composer polls after the first load shows a machine, or to retry a real error. */
|
|
20
|
+
export const MACHINES_COMPOSER_POLL_MS = 30_000;
|
|
21
|
+
|
|
22
|
+
const machinesClientIds = new WeakMap<object, number>();
|
|
23
|
+
let nextMachinesClientId = 1;
|
|
24
|
+
|
|
25
|
+
function machinesClientId(client: object): number {
|
|
26
|
+
const existing = machinesClientIds.get(client);
|
|
27
|
+
if (existing !== undefined) return existing;
|
|
28
|
+
const id = nextMachinesClientId++;
|
|
29
|
+
machinesClientIds.set(client, id);
|
|
30
|
+
return id;
|
|
31
|
+
}
|
|
32
|
+
|
|
11
33
|
/**
|
|
12
34
|
* The slice of the SDK client the Machines surface needs. The method NAMES +
|
|
13
35
|
* SIGNATURES match M10's `OpenGeniClient` (`listMachines`, `machineMetricsSeries`)
|
|
@@ -42,6 +64,17 @@ export type MachinesClientLike = {
|
|
|
42
64
|
enrollmentId: string,
|
|
43
65
|
request?: RemoveEnrollmentRequest,
|
|
44
66
|
) => Promise<RemoveEnrollmentResponse>;
|
|
67
|
+
/** POST .../machines/:enrollmentId/update — signed, generation-fenced update. */
|
|
68
|
+
updateMachineAgent?: (
|
|
69
|
+
workspaceId: string,
|
|
70
|
+
enrollmentId: string,
|
|
71
|
+
) => Promise<UpdateMachineAgentResponse>;
|
|
72
|
+
/** PATCH .../machines/:enrollmentId/operation-policy — revision-fenced policy. */
|
|
73
|
+
updateMachineOperationPolicy?: (
|
|
74
|
+
workspaceId: string,
|
|
75
|
+
enrollmentId: string,
|
|
76
|
+
request: UpdateMachineOperationPolicyRequest,
|
|
77
|
+
) => Promise<MachineOperationPolicy>;
|
|
45
78
|
/**
|
|
46
79
|
* POST .../sessions/:sessionId/active-sandbox — swap the session's active
|
|
47
80
|
* sandbox to a machine. The default swap path; the real SDK client provides it.
|
|
@@ -87,6 +120,17 @@ export type UseMachinesResult = {
|
|
|
87
120
|
) => Promise<RemoveEnrollmentResponse | null>;
|
|
88
121
|
canRemove: boolean;
|
|
89
122
|
removingEnrollmentId: string | null;
|
|
123
|
+
/** Start/retry the signed self-update for one Connected Machine. */
|
|
124
|
+
updateAgent: (enrollmentId: string) => Promise<UpdateMachineAgentResponse | null>;
|
|
125
|
+
canUpdateAgent: boolean;
|
|
126
|
+
updatingEnrollmentId: string | null;
|
|
127
|
+
/** Update the optional per-enrollment command memory policy. */
|
|
128
|
+
updateOperationPolicy: (
|
|
129
|
+
enrollmentId: string,
|
|
130
|
+
request: UpdateMachineOperationPolicyRequest,
|
|
131
|
+
) => Promise<MachineOperationPolicy | null>;
|
|
132
|
+
canUpdateOperationPolicy: boolean;
|
|
133
|
+
updatingOperationPolicyEnrollmentId: string | null;
|
|
90
134
|
/** Whether the host wired an attach/swap path (drives the card affordance). */
|
|
91
135
|
canAttach: boolean;
|
|
92
136
|
/** Fetch a downsampled metric series for one enrolled machine. */
|
|
@@ -101,8 +145,6 @@ export type UseMachinesResult = {
|
|
|
101
145
|
clearMutationError: () => void;
|
|
102
146
|
};
|
|
103
147
|
|
|
104
|
-
const EMPTY: MachinesResponse = { activeSandboxId: null, activeEpoch: 0, machines: [] };
|
|
105
|
-
|
|
106
148
|
function swapFailureMessage(result: SwapActiveSandboxResponse): string {
|
|
107
149
|
if (result.reason?.trim()) return result.reason;
|
|
108
150
|
switch (result.code) {
|
|
@@ -134,7 +176,7 @@ export function useMachines(options: UseMachinesOptions = {}): UseMachinesResult
|
|
|
134
176
|
const machinesClient = (options.machinesClient ??
|
|
135
177
|
(client as unknown as MachinesClientLike)) satisfies MachinesClientLike;
|
|
136
178
|
const sessionId = options.sessionId;
|
|
137
|
-
const identityKey = `${workspaceId}\u0000${sessionId ?? ""}`;
|
|
179
|
+
const identityKey = `${workspaceId}\u0000${sessionId ?? ""}\u0000${machinesClientId(machinesClient)}`;
|
|
138
180
|
const identityRef = useRef(identityKey);
|
|
139
181
|
identityRef.current = identityKey;
|
|
140
182
|
|
|
@@ -153,7 +195,7 @@ export function useMachines(options: UseMachinesOptions = {}): UseMachinesResult
|
|
|
153
195
|
loading,
|
|
154
196
|
error,
|
|
155
197
|
refresh,
|
|
156
|
-
} =
|
|
198
|
+
} = useSharedMachinesList(identityKey, load, {
|
|
157
199
|
pollIntervalMs: options.pollIntervalMs,
|
|
158
200
|
enabled: options.enabled,
|
|
159
201
|
});
|
|
@@ -170,8 +212,20 @@ export function useMachines(options: UseMachinesOptions = {}): UseMachinesResult
|
|
|
170
212
|
}>(() => ({ identity: identityKey, enrollmentId: null }));
|
|
171
213
|
const removingEnrollmentId =
|
|
172
214
|
removeState.identity === identityKey ? removeState.enrollmentId : null;
|
|
215
|
+
const [updateState, setUpdateState] = useState<{
|
|
216
|
+
identity: string;
|
|
217
|
+
enrollmentId: string | null;
|
|
218
|
+
}>(() => ({ identity: identityKey, enrollmentId: null }));
|
|
219
|
+
const updatingEnrollmentId =
|
|
220
|
+
updateState.identity === identityKey ? updateState.enrollmentId : null;
|
|
221
|
+
const [operationPolicyState, setOperationPolicyState] = useState<{
|
|
222
|
+
identity: string;
|
|
223
|
+
enrollmentId: string | null;
|
|
224
|
+
}>(() => ({ identity: identityKey, enrollmentId: null }));
|
|
225
|
+
const updatingOperationPolicyEnrollmentId =
|
|
226
|
+
operationPolicyState.identity === identityKey ? operationPolicyState.enrollmentId : null;
|
|
173
227
|
|
|
174
|
-
const data = loadedData ??
|
|
228
|
+
const data = loadedData ?? EMPTY_MACHINES;
|
|
175
229
|
// The swap is session-scoped: a host adapter (`attachMachine`) wins; otherwise
|
|
176
230
|
// the default `swapActiveSandbox` path is wired whenever a sessionId is in
|
|
177
231
|
// scope. Either way attach needs a sessionId to point at.
|
|
@@ -180,6 +234,9 @@ export function useMachines(options: UseMachinesOptions = {}): UseMachinesResult
|
|
|
180
234
|
(typeof machinesClient.attachMachine === "function" ||
|
|
181
235
|
typeof machinesClient.swapActiveSandbox === "function");
|
|
182
236
|
const canRemove = typeof machinesClient.removeEnrollment === "function";
|
|
237
|
+
const canUpdateAgent = typeof machinesClient.updateMachineAgent === "function";
|
|
238
|
+
const canUpdateOperationPolicy =
|
|
239
|
+
typeof machinesClient.updateMachineOperationPolicy === "function";
|
|
183
240
|
|
|
184
241
|
const attach = useCallback(
|
|
185
242
|
async (sandboxId: string): Promise<boolean> => {
|
|
@@ -246,6 +303,41 @@ export function useMachines(options: UseMachinesOptions = {}): UseMachinesResult
|
|
|
246
303
|
[machinesClient, workspaceId, identityKey, run, refresh],
|
|
247
304
|
);
|
|
248
305
|
|
|
306
|
+
const updateAgent = useCallback(
|
|
307
|
+
async (enrollmentId: string): Promise<UpdateMachineAgentResponse | null> => {
|
|
308
|
+
if (!machinesClient.updateMachineAgent) return null;
|
|
309
|
+
const ownedIdentity = identityKey;
|
|
310
|
+
setUpdateState({ identity: ownedIdentity, enrollmentId });
|
|
311
|
+
const result = await run(() => machinesClient.updateMachineAgent!(workspaceId, enrollmentId));
|
|
312
|
+
if (identityRef.current === ownedIdentity) {
|
|
313
|
+
setUpdateState({ identity: ownedIdentity, enrollmentId: null });
|
|
314
|
+
await refresh();
|
|
315
|
+
}
|
|
316
|
+
return result;
|
|
317
|
+
},
|
|
318
|
+
[machinesClient, workspaceId, identityKey, run, refresh],
|
|
319
|
+
);
|
|
320
|
+
|
|
321
|
+
const updateOperationPolicy = useCallback(
|
|
322
|
+
async (
|
|
323
|
+
enrollmentId: string,
|
|
324
|
+
request: UpdateMachineOperationPolicyRequest,
|
|
325
|
+
): Promise<MachineOperationPolicy | null> => {
|
|
326
|
+
if (!machinesClient.updateMachineOperationPolicy) return null;
|
|
327
|
+
const ownedIdentity = identityKey;
|
|
328
|
+
setOperationPolicyState({ identity: ownedIdentity, enrollmentId });
|
|
329
|
+
const result = await run(() =>
|
|
330
|
+
machinesClient.updateMachineOperationPolicy!(workspaceId, enrollmentId, request),
|
|
331
|
+
);
|
|
332
|
+
if (identityRef.current === ownedIdentity) {
|
|
333
|
+
setOperationPolicyState({ identity: ownedIdentity, enrollmentId: null });
|
|
334
|
+
if (result) await refresh();
|
|
335
|
+
}
|
|
336
|
+
return result;
|
|
337
|
+
},
|
|
338
|
+
[machinesClient, workspaceId, identityKey, run, refresh],
|
|
339
|
+
);
|
|
340
|
+
|
|
249
341
|
return {
|
|
250
342
|
machines: data.machines,
|
|
251
343
|
activeSandboxId: data.activeSandboxId,
|
|
@@ -261,6 +353,12 @@ export function useMachines(options: UseMachinesOptions = {}): UseMachinesResult
|
|
|
261
353
|
remove,
|
|
262
354
|
canRemove,
|
|
263
355
|
removingEnrollmentId,
|
|
356
|
+
updateAgent,
|
|
357
|
+
canUpdateAgent,
|
|
358
|
+
updatingEnrollmentId,
|
|
359
|
+
updateOperationPolicy,
|
|
360
|
+
canUpdateOperationPolicy,
|
|
361
|
+
updatingOperationPolicyEnrollmentId,
|
|
264
362
|
mutationError,
|
|
265
363
|
clearMutationError,
|
|
266
364
|
};
|