@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
|
@@ -233,12 +233,14 @@ export function useSandboxGit(
|
|
|
233
233
|
// survive renders, so they must be fenced/reset when that identity changes.
|
|
234
234
|
const refreshGenerationRef = useRef(0);
|
|
235
235
|
const refreshAbortRef = useRef<AbortController | null>(null);
|
|
236
|
+
const refreshInFlightRef = useRef<Promise<void> | null>(null);
|
|
237
|
+
const refreshPendingRef = useRef(false);
|
|
238
|
+
const refreshWorkerEpochRef = useRef(0);
|
|
236
239
|
const lastChangeRef = useRef(0);
|
|
237
|
-
const
|
|
240
|
+
const eventRefreshTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
238
241
|
|
|
239
|
-
const
|
|
242
|
+
const performRefresh = useCallback(async () => {
|
|
240
243
|
if (!sessionId) return;
|
|
241
|
-
refreshAbortRef.current?.abort();
|
|
242
244
|
const refreshAbort = new AbortController();
|
|
243
245
|
refreshAbortRef.current = refreshAbort;
|
|
244
246
|
const generation = (refreshGenerationRef.current += 1);
|
|
@@ -377,6 +379,32 @@ export function useSandboxGit(
|
|
|
377
379
|
comparison,
|
|
378
380
|
]);
|
|
379
381
|
|
|
382
|
+
// Provider reads cannot be undispatched after they reach a remote sandbox.
|
|
383
|
+
// Keep exactly one refresh in flight and collapse any burst of invalidations
|
|
384
|
+
// into one trailing read. Aborting and replacing every tool-output refresh
|
|
385
|
+
// used to leave several Modal reads contending for the same provider lock.
|
|
386
|
+
const performRefreshRef = useRef(performRefresh);
|
|
387
|
+
performRefreshRef.current = performRefresh;
|
|
388
|
+
const refresh = useCallback(async () => {
|
|
389
|
+
refreshPendingRef.current = true;
|
|
390
|
+
const existing = refreshInFlightRef.current;
|
|
391
|
+
if (existing) return await existing;
|
|
392
|
+
|
|
393
|
+
const workerEpoch = refreshWorkerEpochRef.current;
|
|
394
|
+
const run = (async () => {
|
|
395
|
+
while (refreshPendingRef.current && refreshWorkerEpochRef.current === workerEpoch) {
|
|
396
|
+
refreshPendingRef.current = false;
|
|
397
|
+
await performRefreshRef.current();
|
|
398
|
+
}
|
|
399
|
+
})();
|
|
400
|
+
refreshInFlightRef.current = run;
|
|
401
|
+
try {
|
|
402
|
+
await run;
|
|
403
|
+
} finally {
|
|
404
|
+
if (refreshInFlightRef.current === run) refreshInFlightRef.current = null;
|
|
405
|
+
}
|
|
406
|
+
}, []);
|
|
407
|
+
|
|
380
408
|
// Serve the requested review scope from a capture when that scope exists.
|
|
381
409
|
const seedFromCapture = useCallback(
|
|
382
410
|
(manifest: WorkspaceCaptureManifest) => {
|
|
@@ -432,9 +460,17 @@ export function useSandboxGit(
|
|
|
432
460
|
// true data-identity change clears rendered state and the event high-water mark.
|
|
433
461
|
refreshAbortRef.current?.abort();
|
|
434
462
|
refreshAbortRef.current = null;
|
|
463
|
+
refreshPendingRef.current = false;
|
|
435
464
|
refreshGenerationRef.current += 1;
|
|
436
465
|
const identityChanged = previousIdentityRef.current !== identityKey;
|
|
437
466
|
previousIdentityRef.current = identityKey;
|
|
467
|
+
if (identityChanged) {
|
|
468
|
+
// Different sessions target different provider identities. Fence the old
|
|
469
|
+
// worker and let the new session load immediately instead of waiting for a
|
|
470
|
+
// stuck request that has already been aborted.
|
|
471
|
+
refreshWorkerEpochRef.current += 1;
|
|
472
|
+
refreshInFlightRef.current = null;
|
|
473
|
+
}
|
|
438
474
|
if (identityChanged || !enabled) {
|
|
439
475
|
lastChangeRef.current = 0;
|
|
440
476
|
setDiff([]);
|
|
@@ -466,6 +502,7 @@ export function useSandboxGit(
|
|
|
466
502
|
return () => {
|
|
467
503
|
refreshAbortRef.current?.abort();
|
|
468
504
|
refreshAbortRef.current = null;
|
|
505
|
+
refreshPendingRef.current = false;
|
|
469
506
|
refreshGenerationRef.current += 1;
|
|
470
507
|
};
|
|
471
508
|
}, [
|
|
@@ -479,15 +516,15 @@ export function useSandboxGit(
|
|
|
479
516
|
seedFromCapture,
|
|
480
517
|
]);
|
|
481
518
|
|
|
482
|
-
//
|
|
483
|
-
//
|
|
484
|
-
//
|
|
519
|
+
// Structured Git changes are debounced into one live reconcile. Generic
|
|
520
|
+
// command/tool output performs no provider reads: the canonical turn-end
|
|
521
|
+
// capture advances `captureRevision`, which refreshes after the burst settles.
|
|
485
522
|
const events = options.events;
|
|
486
523
|
useEffect(() => {
|
|
487
524
|
if (!enabled || !events) {
|
|
488
|
-
if (
|
|
489
|
-
clearTimeout(
|
|
490
|
-
|
|
525
|
+
if (eventRefreshTimerRef.current) {
|
|
526
|
+
clearTimeout(eventRefreshTimerRef.current);
|
|
527
|
+
eventRefreshTimerRef.current = null;
|
|
491
528
|
}
|
|
492
529
|
return;
|
|
493
530
|
}
|
|
@@ -498,43 +535,35 @@ export function useSandboxGit(
|
|
|
498
535
|
for (const event of events) {
|
|
499
536
|
if (event.sequence > lastChangeRef.current) lastChangeRef.current = event.sequence;
|
|
500
537
|
}
|
|
501
|
-
if (
|
|
502
|
-
clearTimeout(
|
|
503
|
-
|
|
538
|
+
if (eventRefreshTimerRef.current) {
|
|
539
|
+
clearTimeout(eventRefreshTimerRef.current);
|
|
540
|
+
eventRefreshTimerRef.current = null;
|
|
504
541
|
}
|
|
505
542
|
return;
|
|
506
543
|
}
|
|
507
544
|
let latest = lastChangeRef.current;
|
|
508
|
-
let
|
|
509
|
-
let commandDelta = false;
|
|
545
|
+
let changed = false;
|
|
510
546
|
for (const event of events) {
|
|
511
547
|
if (event.sequence <= latest) continue;
|
|
512
|
-
if (event.type === "git.changed"
|
|
513
|
-
latest = event.sequence;
|
|
514
|
-
immediate = true;
|
|
515
|
-
} else if (event.type === "sandbox.command.output.delta") {
|
|
548
|
+
if (event.type === "git.changed") {
|
|
516
549
|
latest = event.sequence;
|
|
517
|
-
|
|
550
|
+
changed = true;
|
|
518
551
|
}
|
|
519
552
|
}
|
|
520
553
|
if (latest > lastChangeRef.current) {
|
|
521
554
|
lastChangeRef.current = latest;
|
|
522
|
-
if (
|
|
523
|
-
|
|
524
|
-
|
|
555
|
+
if (!changed) return;
|
|
556
|
+
if (eventRefreshTimerRef.current) clearTimeout(eventRefreshTimerRef.current);
|
|
557
|
+
eventRefreshTimerRef.current = setTimeout(() => {
|
|
558
|
+
eventRefreshTimerRef.current = null;
|
|
525
559
|
if (acceptsEventReadsRef.current) void refresh();
|
|
526
|
-
}
|
|
527
|
-
commandRefreshTimerRef.current = setTimeout(() => {
|
|
528
|
-
commandRefreshTimerRef.current = null;
|
|
529
|
-
if (acceptsEventReadsRef.current) void refresh();
|
|
530
|
-
}, 1_000);
|
|
531
|
-
}
|
|
560
|
+
}, 150);
|
|
532
561
|
}
|
|
533
562
|
}, [enabled, active, acceptsEventReads, events, refresh]);
|
|
534
563
|
|
|
535
564
|
useEffect(
|
|
536
565
|
() => () => {
|
|
537
|
-
if (
|
|
566
|
+
if (eventRefreshTimerRef.current) clearTimeout(eventRefreshTimerRef.current);
|
|
538
567
|
},
|
|
539
568
|
[],
|
|
540
569
|
);
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { SessionBackgroundCommand } from "@opengeni/sdk";
|
|
2
|
+
import { useCallback } from "react";
|
|
3
|
+
|
|
4
|
+
import { useOpenGeni, type ClientOverride } from "../provider";
|
|
5
|
+
import { usePolledValue } from "./internal";
|
|
6
|
+
|
|
7
|
+
export type UseSessionBackgroundCommandsOptions = ClientOverride & {
|
|
8
|
+
pollIntervalMs?: number | undefined;
|
|
9
|
+
enabled?: boolean | undefined;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type UseSessionBackgroundCommandsResult = {
|
|
13
|
+
commands: SessionBackgroundCommand[];
|
|
14
|
+
loading: boolean;
|
|
15
|
+
error: Error | null;
|
|
16
|
+
refresh: () => Promise<void>;
|
|
17
|
+
cancel: (commandId: string) => Promise<void>;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function useSessionBackgroundCommands(
|
|
21
|
+
sessionId: string | null | undefined,
|
|
22
|
+
options: UseSessionBackgroundCommandsOptions = {},
|
|
23
|
+
): UseSessionBackgroundCommandsResult {
|
|
24
|
+
const { client, workspaceId } = useOpenGeni(options);
|
|
25
|
+
const enabled = (options.enabled ?? true) && Boolean(sessionId);
|
|
26
|
+
const load = useCallback(async () => {
|
|
27
|
+
if (!sessionId) return { commands: [] };
|
|
28
|
+
if (!client.listSessionBackgroundCommands) {
|
|
29
|
+
throw new Error("The configured OpenGeni client does not support background commands");
|
|
30
|
+
}
|
|
31
|
+
return await client.listSessionBackgroundCommands(workspaceId, sessionId);
|
|
32
|
+
}, [client, workspaceId, sessionId]);
|
|
33
|
+
const state = usePolledValue(load, {
|
|
34
|
+
enabled,
|
|
35
|
+
pollIntervalMs: options.pollIntervalMs,
|
|
36
|
+
});
|
|
37
|
+
const refresh = state.refresh;
|
|
38
|
+
const cancel = useCallback(
|
|
39
|
+
async (commandId: string): Promise<void> => {
|
|
40
|
+
if (!sessionId) return;
|
|
41
|
+
if (!client.cancelSessionBackgroundCommand) {
|
|
42
|
+
throw new Error("The configured OpenGeni client does not support background commands");
|
|
43
|
+
}
|
|
44
|
+
await client.cancelSessionBackgroundCommand(workspaceId, sessionId, commandId);
|
|
45
|
+
await refresh();
|
|
46
|
+
},
|
|
47
|
+
[client, workspaceId, sessionId, refresh],
|
|
48
|
+
);
|
|
49
|
+
return {
|
|
50
|
+
commands: state.data?.commands ?? [],
|
|
51
|
+
loading: state.loading,
|
|
52
|
+
error: state.error,
|
|
53
|
+
refresh,
|
|
54
|
+
cancel,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -283,6 +283,11 @@ export function useSessionCapabilities(
|
|
|
283
283
|
})
|
|
284
284
|
.catch((cause) => {
|
|
285
285
|
if (!cancelled) {
|
|
286
|
+
if (isSandboxOwnershipDisabledError(cause)) {
|
|
287
|
+
setState("on-demand");
|
|
288
|
+
setError(null);
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
286
291
|
setState("error");
|
|
287
292
|
setError(cause instanceof Error ? cause : new Error(String(cause)));
|
|
288
293
|
}
|
|
@@ -453,6 +458,11 @@ export function useSessionCapabilities(
|
|
|
453
458
|
}
|
|
454
459
|
} catch (cause) {
|
|
455
460
|
if (cancelled) return;
|
|
461
|
+
if (isSandboxOwnershipDisabledError(cause)) {
|
|
462
|
+
setState("on-demand");
|
|
463
|
+
setError(null);
|
|
464
|
+
return;
|
|
465
|
+
}
|
|
456
466
|
if (cause instanceof OpenGeniApiError && cause.status === 403) {
|
|
457
467
|
setState("error");
|
|
458
468
|
setError(new Error("not permitted to view this session's sandbox"));
|
|
@@ -524,3 +534,11 @@ export function useSessionCapabilities(
|
|
|
524
534
|
renegotiate,
|
|
525
535
|
};
|
|
526
536
|
}
|
|
537
|
+
|
|
538
|
+
function isSandboxOwnershipDisabledError(error: unknown): boolean {
|
|
539
|
+
return (
|
|
540
|
+
error instanceof OpenGeniApiError &&
|
|
541
|
+
error.status === 404 &&
|
|
542
|
+
error.message.includes("sandbox ownership is not enabled")
|
|
543
|
+
);
|
|
544
|
+
}
|
|
@@ -42,6 +42,17 @@ function queueSnapshotCovers(
|
|
|
42
42
|
export type QueueMutationKind = "move" | "edit" | "steer" | "delete";
|
|
43
43
|
const EMPTY_PENDING_BY_TURN: Readonly<Record<string, QueueMutationKind>> = {};
|
|
44
44
|
|
|
45
|
+
export type AcceptedQueueSteer = {
|
|
46
|
+
turnId: string;
|
|
47
|
+
triggerEventId: string;
|
|
48
|
+
text: string;
|
|
49
|
+
annotations: SessionTurn["annotations"];
|
|
50
|
+
resources: SessionTurn["resources"];
|
|
51
|
+
tools: SessionTurn["tools"];
|
|
52
|
+
occurredAt: string;
|
|
53
|
+
state: "sending" | "queued";
|
|
54
|
+
};
|
|
55
|
+
|
|
45
56
|
export type UseTurnQueueOptions = EmbeddedSessionClientOverride &
|
|
46
57
|
SessionEventFeedOptions & {
|
|
47
58
|
pollIntervalMs?: number | undefined;
|
|
@@ -77,6 +88,8 @@ export type UseTurnQueueResult = {
|
|
|
77
88
|
mutating: boolean;
|
|
78
89
|
mutationError: Error | null;
|
|
79
90
|
clearMutationError: () => void;
|
|
91
|
+
/** Immediate chat bridge while the durable Steer event catches up over SSE. */
|
|
92
|
+
acceptedSteers?: AcceptedQueueSteer[] | undefined;
|
|
80
93
|
};
|
|
81
94
|
|
|
82
95
|
/**
|
|
@@ -99,6 +112,7 @@ export function useTurnQueue(
|
|
|
99
112
|
const [error, setError] = useState<Error | null>(null);
|
|
100
113
|
const [mutationError, setMutationError] = useState<Error | null>(null);
|
|
101
114
|
const [pendingByTurn, setPendingByTurn] = useState<Record<string, QueueMutationKind>>({});
|
|
115
|
+
const [acceptedSteers, setAcceptedSteers] = useState<AcceptedQueueSteer[]>([]);
|
|
102
116
|
const pendingRef = useRef<Record<string, QueueMutationKind>>({});
|
|
103
117
|
const readGeneration = useRef(0);
|
|
104
118
|
const targetKeyRef = useRef(targetKey);
|
|
@@ -118,8 +132,26 @@ export function useTurnQueue(
|
|
|
118
132
|
setError(null);
|
|
119
133
|
setMutationError(null);
|
|
120
134
|
setPendingByTurn({});
|
|
135
|
+
setAcceptedSteers([]);
|
|
121
136
|
}, [enabled, targetKey]);
|
|
122
137
|
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
if (acceptedSteers.length === 0 || options.events === undefined) return;
|
|
140
|
+
const durablyStartedTurnIds = new Set(
|
|
141
|
+
options.events.flatMap((event) =>
|
|
142
|
+
event.type === "turn.started" && event.turnId ? [event.turnId] : [],
|
|
143
|
+
),
|
|
144
|
+
);
|
|
145
|
+
if (durablyStartedTurnIds.size === 0) return;
|
|
146
|
+
// steer_requested can reach the browser before the timeline projection
|
|
147
|
+
// that moves the queued user.message into chat. Keep this bridge through
|
|
148
|
+
// physical cancellation; turn.started is the first boundary at which the
|
|
149
|
+
// replacement timeline must already be visible.
|
|
150
|
+
setAcceptedSteers((current) =>
|
|
151
|
+
current.filter((steer) => !durablyStartedTurnIds.has(steer.turnId)),
|
|
152
|
+
);
|
|
153
|
+
}, [acceptedSteers.length, options.events]);
|
|
154
|
+
|
|
123
155
|
const acceptSnapshot = useCallback(
|
|
124
156
|
(ownedTargetKey: string, next: SessionQueueSnapshot | null): boolean => {
|
|
125
157
|
if (targetKeyRef.current !== ownedTargetKey) return false;
|
|
@@ -234,6 +266,7 @@ export function useTurnQueue(
|
|
|
234
266
|
command: (
|
|
235
267
|
current: SessionQueueSnapshot,
|
|
236
268
|
turn: SessionTurn,
|
|
269
|
+
clientEventId: string,
|
|
237
270
|
) => Promise<SessionQueueMutationResponse>,
|
|
238
271
|
): Promise<SessionQueueMutationResponse | null> => {
|
|
239
272
|
const ownedTargetKey = targetKey;
|
|
@@ -247,8 +280,17 @@ export function useTurnQueue(
|
|
|
247
280
|
setStateTargetKey(ownedTargetKey);
|
|
248
281
|
setPendingByTurn(pendingRef.current);
|
|
249
282
|
setMutationError(null);
|
|
283
|
+
const clientEventId = operationKey();
|
|
250
284
|
try {
|
|
251
|
-
|
|
285
|
+
let result: SessionQueueMutationResponse;
|
|
286
|
+
try {
|
|
287
|
+
result = await command(current, turn, clientEventId);
|
|
288
|
+
} catch (cause) {
|
|
289
|
+
if (!isOutcomeUnknownError(cause)) throw cause;
|
|
290
|
+
// Same-key replay is a fact check against the durable receipt. It
|
|
291
|
+
// cannot duplicate the command even if the first response was lost.
|
|
292
|
+
result = await command(current, turn, clientEventId);
|
|
293
|
+
}
|
|
252
294
|
if (targetKeyRef.current !== ownedTargetKey) return null;
|
|
253
295
|
acceptSnapshot(ownedTargetKey, result.snapshot);
|
|
254
296
|
return result;
|
|
@@ -272,9 +314,9 @@ export function useTurnQueue(
|
|
|
272
314
|
|
|
273
315
|
const moveTurn = useCallback(
|
|
274
316
|
async (turnId: string, beforeTurnId: string | null): Promise<boolean> => {
|
|
275
|
-
const result = await mutate(turnId, "move", (current) =>
|
|
317
|
+
const result = await mutate(turnId, "move", (current, _turn, clientEventId) =>
|
|
276
318
|
client.moveQueueItem(workspaceId, sessionId!, turnId, {
|
|
277
|
-
clientEventId
|
|
319
|
+
clientEventId,
|
|
278
320
|
expectedQueueVersion: current.version,
|
|
279
321
|
beforeTurnId,
|
|
280
322
|
}),
|
|
@@ -289,9 +331,9 @@ export function useTurnQueue(
|
|
|
289
331
|
turnId: string,
|
|
290
332
|
edit: { expectedDraftRevision: number; replaceDraft: boolean },
|
|
291
333
|
): Promise<ComposerDraft | null> => {
|
|
292
|
-
const result = await mutate(turnId, "edit", (_current, turn) =>
|
|
334
|
+
const result = await mutate(turnId, "edit", (_current, turn, clientEventId) =>
|
|
293
335
|
client.editQueueItem(workspaceId, sessionId!, turnId, {
|
|
294
|
-
clientEventId
|
|
336
|
+
clientEventId,
|
|
295
337
|
expectedTurnVersion: turn.version,
|
|
296
338
|
expectedDraftRevision: edit.expectedDraftRevision,
|
|
297
339
|
replaceDraft: edit.replaceDraft,
|
|
@@ -304,23 +346,71 @@ export function useTurnQueue(
|
|
|
304
346
|
|
|
305
347
|
const steerTurn = useCallback(
|
|
306
348
|
async (turnId: string): Promise<boolean> => {
|
|
307
|
-
const
|
|
349
|
+
const ownedTargetKey = targetKey;
|
|
350
|
+
const turn = snapshotRef.current?.items.find((candidate) => candidate.id === turnId);
|
|
351
|
+
if (targetKeyRef.current !== ownedTargetKey) return false;
|
|
352
|
+
if (!turn) {
|
|
353
|
+
setMutationError(new Error("That queued prompt changed before Steer could be sent."));
|
|
354
|
+
await load();
|
|
355
|
+
return false;
|
|
356
|
+
}
|
|
357
|
+
const optimistic: AcceptedQueueSteer = {
|
|
358
|
+
turnId: turn.id,
|
|
359
|
+
triggerEventId: turn.triggerEventId,
|
|
360
|
+
text: turn.prompt,
|
|
361
|
+
annotations: turn.annotations ?? [],
|
|
362
|
+
resources: turn.resources,
|
|
363
|
+
tools: turn.tools,
|
|
364
|
+
occurredAt: new Date().toISOString(),
|
|
365
|
+
state: "sending",
|
|
366
|
+
};
|
|
367
|
+
setAcceptedSteers((current) => [
|
|
368
|
+
...current.filter((steer) => steer.turnId !== turnId),
|
|
369
|
+
optimistic,
|
|
370
|
+
]);
|
|
371
|
+
const result = await mutate(turnId, "steer", (current, queuedTurn, clientEventId) =>
|
|
308
372
|
client.steerQueueItem(workspaceId, sessionId!, turnId, {
|
|
309
|
-
clientEventId
|
|
310
|
-
expectedTurnVersion:
|
|
373
|
+
clientEventId,
|
|
374
|
+
expectedTurnVersion: queuedTurn.version,
|
|
311
375
|
controlEtag: current.effectiveControl.controlEtag,
|
|
312
376
|
}),
|
|
313
377
|
);
|
|
314
|
-
|
|
378
|
+
if (targetKeyRef.current !== ownedTargetKey) return false;
|
|
379
|
+
// The active turn can finish while the operator clicks Steer. In that
|
|
380
|
+
// race the queued prompt is atomically claimed before the steer command,
|
|
381
|
+
// so the command conflicts but the user's prompt already advanced into
|
|
382
|
+
// chat. Reconciled absence is success, not a scary unknown-outcome lie.
|
|
383
|
+
const advancedWithoutSteer =
|
|
384
|
+
result === null &&
|
|
385
|
+
snapshotRef.current !== null &&
|
|
386
|
+
!snapshotRef.current.items.some((candidate) => candidate.id === turnId);
|
|
387
|
+
if (advancedWithoutSteer) {
|
|
388
|
+
setMutationError(null);
|
|
389
|
+
setAcceptedSteers((current) => current.filter((steer) => steer.turnId !== turnId));
|
|
390
|
+
return true;
|
|
391
|
+
}
|
|
392
|
+
const accepted =
|
|
393
|
+
result !== null ||
|
|
394
|
+
snapshotRef.current?.items.some(
|
|
395
|
+
(candidate) => candidate.id === turnId && candidate.metadata.delivery === "steer",
|
|
396
|
+
) === true;
|
|
397
|
+
setAcceptedSteers((current) =>
|
|
398
|
+
accepted
|
|
399
|
+
? current.map((steer) =>
|
|
400
|
+
steer.turnId === turnId ? { ...steer, state: "queued" as const } : steer,
|
|
401
|
+
)
|
|
402
|
+
: current.filter((steer) => steer.turnId !== turnId),
|
|
403
|
+
);
|
|
404
|
+
return accepted;
|
|
315
405
|
},
|
|
316
|
-
[client, mutate, sessionId, workspaceId],
|
|
406
|
+
[client, load, mutate, sessionId, targetKey, workspaceId],
|
|
317
407
|
);
|
|
318
408
|
|
|
319
409
|
const removeTurn = useCallback(
|
|
320
410
|
async (turnId: string): Promise<boolean> => {
|
|
321
|
-
const result = await mutate(turnId, "delete", (_current, turn) =>
|
|
411
|
+
const result = await mutate(turnId, "delete", (_current, turn, clientEventId) =>
|
|
322
412
|
client.deleteQueueItem(workspaceId, sessionId!, turnId, {
|
|
323
|
-
clientEventId
|
|
413
|
+
clientEventId,
|
|
324
414
|
expectedTurnVersion: turn.version,
|
|
325
415
|
reason: "Deleted from the prompt queue",
|
|
326
416
|
}),
|
|
@@ -364,6 +454,7 @@ export function useTurnQueue(
|
|
|
364
454
|
clearMutationError: useCallback(() => {
|
|
365
455
|
if (targetKeyRef.current === targetKey) setMutationError(null);
|
|
366
456
|
}, [targetKey]),
|
|
457
|
+
acceptedSteers: identityMatches ? acceptedSteers : [],
|
|
367
458
|
};
|
|
368
459
|
}
|
|
369
460
|
|
|
@@ -374,3 +465,11 @@ function operationKey(): string {
|
|
|
374
465
|
function asError(cause: unknown): Error {
|
|
375
466
|
return cause instanceof Error ? cause : new Error(String(cause));
|
|
376
467
|
}
|
|
468
|
+
|
|
469
|
+
function isOutcomeUnknownError(cause: unknown): boolean {
|
|
470
|
+
return (
|
|
471
|
+
typeof cause === "object" &&
|
|
472
|
+
cause !== null &&
|
|
473
|
+
(cause as { outcomeUnknown?: unknown }).outcomeUnknown === true
|
|
474
|
+
);
|
|
475
|
+
}
|
|
@@ -10,6 +10,7 @@ export type UseWorkspaceSessionsOptions = ClientOverride & {
|
|
|
10
10
|
search?: string | undefined;
|
|
11
11
|
/** Return only the complete personal pinned projection. */
|
|
12
12
|
pinsOnly?: boolean | undefined;
|
|
13
|
+
archivedOnly?: boolean | undefined;
|
|
13
14
|
/** Refresh interval (ms) for fleet/manager views. Off by default. */
|
|
14
15
|
pollIntervalMs?: number | undefined;
|
|
15
16
|
enabled?: boolean | undefined;
|
|
@@ -41,6 +42,7 @@ export function useWorkspaceSessions(
|
|
|
41
42
|
const cursor = options.cursor;
|
|
42
43
|
const search = options.search;
|
|
43
44
|
const pinsOnly = options.pinsOnly;
|
|
45
|
+
const archivedOnly = options.archivedOnly;
|
|
44
46
|
const enabled = options.enabled ?? true;
|
|
45
47
|
const queryKey = [
|
|
46
48
|
workspaceId,
|
|
@@ -49,6 +51,7 @@ export function useWorkspaceSessions(
|
|
|
49
51
|
cursor ?? "",
|
|
50
52
|
search ?? "",
|
|
51
53
|
pinsOnly ? "1" : "",
|
|
54
|
+
archivedOnly ? "archived" : "active",
|
|
52
55
|
].join("\u0000");
|
|
53
56
|
const previousQueryKey = useRef(queryKey);
|
|
54
57
|
const queryKeyTransition = previousQueryKey.current !== queryKey;
|
|
@@ -64,9 +67,10 @@ export function useWorkspaceSessions(
|
|
|
64
67
|
...(cursor !== undefined ? { cursor } : {}),
|
|
65
68
|
...(search !== undefined ? { search } : {}),
|
|
66
69
|
...(pinsOnly ? { pinsOnly: true } : {}),
|
|
70
|
+
...(archivedOnly ? { archivedOnly: true } : {}),
|
|
67
71
|
}),
|
|
68
72
|
}),
|
|
69
|
-
[client, workspaceId, limit, parentSessionId, cursor, search, pinsOnly, queryKey],
|
|
73
|
+
[client, workspaceId, limit, parentSessionId, cursor, search, pinsOnly, archivedOnly, queryKey],
|
|
70
74
|
);
|
|
71
75
|
const state = usePolledValue(load, {
|
|
72
76
|
pollIntervalMs: options.pollIntervalMs,
|
package/src/index.ts
CHANGED
|
@@ -54,7 +54,13 @@ export {
|
|
|
54
54
|
shouldSubmitOnKey,
|
|
55
55
|
FILE_ONLY_MESSAGE_TEXT,
|
|
56
56
|
} from "./hooks/use-composer";
|
|
57
|
-
export type {
|
|
57
|
+
export type {
|
|
58
|
+
ComposerControllerState,
|
|
59
|
+
ComposerPolicy,
|
|
60
|
+
ComposerSendExtras,
|
|
61
|
+
ComposerState,
|
|
62
|
+
UseComposerOptions,
|
|
63
|
+
} from "./hooks/use-composer";
|
|
58
64
|
export {
|
|
59
65
|
VOICE_RECORDING_CLIENT_MAX_DURATION_SECONDS,
|
|
60
66
|
VOICE_RECORDING_OWNER_HEARTBEAT_MILLISECONDS,
|
|
@@ -129,7 +135,12 @@ export type {
|
|
|
129
135
|
} from "./hooks/use-last-started-turn-policy";
|
|
130
136
|
export { QueueSurface } from "./components/queue-surface";
|
|
131
137
|
export type { QueueSurfaceProps } from "./components/queue-surface";
|
|
132
|
-
export {
|
|
138
|
+
export {
|
|
139
|
+
SessionChrome,
|
|
140
|
+
sessionChromeGoalPillExplanation,
|
|
141
|
+
sessionChromeGoalPillLabel,
|
|
142
|
+
sessionChromeGoalPillState,
|
|
143
|
+
} from "./components/session-chrome";
|
|
133
144
|
export type {
|
|
134
145
|
SessionChromeAgentsSignal,
|
|
135
146
|
SessionChromeProps,
|
|
@@ -163,6 +174,11 @@ export type {
|
|
|
163
174
|
UseWorkspaceSessionsOptions,
|
|
164
175
|
UseWorkspaceSessionsResult,
|
|
165
176
|
} from "./hooks/use-workspace-sessions";
|
|
177
|
+
export { useSessionBackgroundCommands } from "./hooks/use-session-background-commands";
|
|
178
|
+
export type {
|
|
179
|
+
UseSessionBackgroundCommandsOptions,
|
|
180
|
+
UseSessionBackgroundCommandsResult,
|
|
181
|
+
} from "./hooks/use-session-background-commands";
|
|
166
182
|
export { useSessionLineage, isLineageRefreshEvent } from "./hooks/use-session-lineage";
|
|
167
183
|
export type {
|
|
168
184
|
UseSessionLineageOptions,
|
|
@@ -315,6 +331,8 @@ export type {
|
|
|
315
331
|
AuthNeededItem,
|
|
316
332
|
ContextCompactionItem,
|
|
317
333
|
GoalItem,
|
|
334
|
+
HumanInputAnswerSummary,
|
|
335
|
+
HumanInputItem,
|
|
318
336
|
MachineInputBatchItem,
|
|
319
337
|
MachineInputMember,
|
|
320
338
|
NoticeItem,
|
|
@@ -478,10 +496,14 @@ export type { GeneratedVideoPlayerProps } from "./components/generated-video-pla
|
|
|
478
496
|
export type { MessageTimelineProps } from "./components/message-timeline";
|
|
479
497
|
export { UserMessageBody, userMessageLikelyNeedsDisclosure } from "./components/user-message-body";
|
|
480
498
|
export type { UserMessageBodyProps } from "./components/user-message-body";
|
|
481
|
-
export {
|
|
499
|
+
export {
|
|
500
|
+
Markdown,
|
|
501
|
+
sandboxFileLocationFromHref,
|
|
502
|
+
sandboxFilePathFromHref,
|
|
503
|
+
} from "./components/markdown";
|
|
504
|
+
export type { MarkdownProps, SandboxFileLocation } from "./components/markdown";
|
|
482
505
|
export { CopyButton, CopyHoverFrame } from "./components/copy-button";
|
|
483
506
|
export { copyTextToClipboard, tableElementToTsv } from "./lib/clipboard";
|
|
484
|
-
export type { MarkdownProps } from "./components/markdown";
|
|
485
507
|
export { SessionStatus, StatusDot, SESSION_STATUS_META } from "./components/session-status";
|
|
486
508
|
export type {
|
|
487
509
|
SessionStatusProps,
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { OpenGeniApiError } from "@opengeni/sdk";
|
|
2
|
+
|
|
3
|
+
export function isNonRetryableInteractionError(error: unknown): error is OpenGeniApiError {
|
|
4
|
+
return error instanceof OpenGeniApiError && error.retryable === false;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function isSourcePlacementChangedError(
|
|
8
|
+
error: unknown,
|
|
9
|
+
resource: "browser_session" | "computer_session",
|
|
10
|
+
): error is OpenGeniApiError {
|
|
11
|
+
return (
|
|
12
|
+
isNonRetryableInteractionError(error) &&
|
|
13
|
+
error.status === 409 &&
|
|
14
|
+
error.details?.interactionResource === resource &&
|
|
15
|
+
error.details?.interactionFailureCode === "source_placement_changed" &&
|
|
16
|
+
error.details?.interactionLifecycle === "lost"
|
|
17
|
+
);
|
|
18
|
+
}
|
package/src/machines.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
export type {
|
|
10
10
|
ConnectionStatus,
|
|
11
11
|
MachineKind,
|
|
12
|
+
MachineOperationPolicy,
|
|
12
13
|
MachineMetricsSeriesResponse,
|
|
13
14
|
MachineState,
|
|
14
15
|
MachineView,
|
|
@@ -48,6 +49,8 @@ export type {
|
|
|
48
49
|
} from "./components/machines/metric-history-chart";
|
|
49
50
|
export { MachineDetail } from "./components/machines/machine-detail";
|
|
50
51
|
export type { MachineDetailProps } from "./components/machines/machine-detail";
|
|
52
|
+
export { MachineOperationPolicyEditor } from "./components/machines/machine-operation-policy";
|
|
53
|
+
export type { MachineOperationPolicyEditorProps } from "./components/machines/machine-operation-policy";
|
|
51
54
|
export { METRICS, METRIC_WINDOWS, WINDOW_LABEL, pointsFor } from "./components/machines/series";
|
|
52
55
|
export type { MetricDef, MetricKey, MetricWindow } from "./components/machines/series";
|
|
53
56
|
export { MachinesDashboard } from "./components/machines-dashboard";
|
|
@@ -68,7 +71,12 @@ export type {
|
|
|
68
71
|
EnrollmentConsentPhase,
|
|
69
72
|
EnrollmentConsentProps,
|
|
70
73
|
} from "./components/enrollment-consent";
|
|
71
|
-
export {
|
|
74
|
+
export {
|
|
75
|
+
useMachines,
|
|
76
|
+
MACHINES_COMPOSER_POLL_MS,
|
|
77
|
+
MACHINES_DASHBOARD_POLL_MS,
|
|
78
|
+
MACHINES_SESSION_POLL_MS,
|
|
79
|
+
} from "./hooks/use-machines";
|
|
72
80
|
export type {
|
|
73
81
|
MachinesClientLike,
|
|
74
82
|
UseMachinesOptions,
|
package/src/model-policy.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { ClientModel, ReasoningEffort, WorkspaceModelCatalogModel } from "@
|
|
|
2
2
|
|
|
3
3
|
export type PickerBillingClass =
|
|
4
4
|
| "opengeni_credits"
|
|
5
|
+
| "external"
|
|
5
6
|
| "codex_subscription"
|
|
6
7
|
| "supergrok_subscription"
|
|
7
8
|
| "byok";
|
|
@@ -24,6 +25,7 @@ export type LatencyModeId = "standard" | "priority" | "fast";
|
|
|
24
25
|
|
|
25
26
|
const BILLING_CLASS_ORDER: PickerBillingClass[] = [
|
|
26
27
|
"opengeni_credits",
|
|
28
|
+
"external",
|
|
27
29
|
"codex_subscription",
|
|
28
30
|
"supergrok_subscription",
|
|
29
31
|
"byok",
|
|
@@ -31,6 +33,7 @@ const BILLING_CLASS_ORDER: PickerBillingClass[] = [
|
|
|
31
33
|
|
|
32
34
|
const BILLING_CLASS_LABELS: Record<PickerBillingClass, string> = {
|
|
33
35
|
opengeni_credits: "OpenGeni",
|
|
36
|
+
external: "External",
|
|
34
37
|
codex_subscription: "Codex",
|
|
35
38
|
supergrok_subscription: "SuperGrok",
|
|
36
39
|
byok: "Your Gateway",
|
|
@@ -47,6 +50,9 @@ const AVAILABILITY_REASON_LABELS: Record<string, string> = {
|
|
|
47
50
|
};
|
|
48
51
|
|
|
49
52
|
export function billingClassForModel(model: ClientModel): PickerBillingClass {
|
|
53
|
+
if (model.billing?.metering === "external" && model.billing.upstreamPayer === "deployment") {
|
|
54
|
+
return "external";
|
|
55
|
+
}
|
|
50
56
|
if (model.source === "supergrok") {
|
|
51
57
|
return "supergrok_subscription";
|
|
52
58
|
}
|
|
@@ -153,13 +159,15 @@ export function payerSummaryForModel(model: ClientModel): string {
|
|
|
153
159
|
if (billing.upstreamPayer === "workspace") {
|
|
154
160
|
return "Billed to your AI Gateway";
|
|
155
161
|
}
|
|
156
|
-
return "
|
|
162
|
+
return "External provider · no OpenGeni credits";
|
|
157
163
|
}
|
|
158
164
|
|
|
159
165
|
export function advancedSourceSummary(model: ClientModel): string | null {
|
|
160
166
|
const source = model.credentialSource;
|
|
161
167
|
if (!source) {
|
|
162
|
-
return
|
|
168
|
+
return model.billing?.metering === "external" && model.billing.upstreamPayer === "deployment"
|
|
169
|
+
? "Deployment route · no authentication"
|
|
170
|
+
: null;
|
|
163
171
|
}
|
|
164
172
|
if (source.kind === "connected_subscription") {
|
|
165
173
|
return source.provider === "xai"
|