@opengeni/react 0.48.0 → 0.57.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 +138 -2
- package/dist/artifacts-document.d.ts +3 -0
- package/dist/artifacts-document.js +17 -0
- package/dist/artifacts-document.js.map +1 -0
- package/dist/artifacts-presentation.d.ts +3 -0
- package/dist/artifacts-presentation.js +17 -0
- package/dist/artifacts-presentation.js.map +1 -0
- package/dist/artifacts-spreadsheet.d.ts +3 -0
- package/dist/artifacts-spreadsheet.js +17 -0
- package/dist/artifacts-spreadsheet.js.map +1 -0
- package/dist/artifacts.d.ts +2 -0
- package/dist/artifacts.js +199 -0
- package/dist/artifacts.js.map +1 -0
- package/dist/{chunk-NJ7M2B6Z.js → chunk-5UQPNLIO.js} +16 -8
- package/dist/{chunk-NJ7M2B6Z.js.map → chunk-5UQPNLIO.js.map} +1 -1
- package/dist/{chunk-LHLYWG5R.js → chunk-6KLX2RH5.js} +461 -38
- package/dist/chunk-6KLX2RH5.js.map +1 -0
- package/dist/chunk-7WCLAEFV.js +2828 -0
- package/dist/chunk-7WCLAEFV.js.map +1 -0
- package/dist/{chunk-I4MPFDRP.js → chunk-B6WPZSTT.js} +104 -104
- package/dist/{chunk-I4MPFDRP.js.map → chunk-B6WPZSTT.js.map} +1 -1
- package/dist/{chunk-EKZH6IDG.js → chunk-C5ZPSSS5.js} +111 -1
- package/dist/chunk-C5ZPSSS5.js.map +1 -0
- package/dist/{chunk-OL6QVXGF.js → chunk-HA46CYQK.js} +152 -8
- package/dist/chunk-HA46CYQK.js.map +1 -0
- package/dist/chunk-HUIKIDM5.js +2148 -0
- package/dist/chunk-HUIKIDM5.js.map +1 -0
- package/dist/{chunk-FWXGSNXT.js → chunk-IGC6A2ZI.js} +585 -34
- package/dist/chunk-IGC6A2ZI.js.map +1 -0
- package/dist/chunk-JVYQRRC4.js +53 -0
- package/dist/chunk-JVYQRRC4.js.map +1 -0
- package/dist/chunk-KCGXPP5V.js +157 -0
- package/dist/chunk-KCGXPP5V.js.map +1 -0
- package/dist/chunk-LLOLUHVG.js +5937 -0
- package/dist/chunk-LLOLUHVG.js.map +1 -0
- package/dist/{chunk-WYA65Y3F.js → chunk-MKPIAGVL.js} +13 -3
- package/dist/chunk-MKPIAGVL.js.map +1 -0
- package/dist/chunk-PEI6RANI.js +3234 -0
- package/dist/chunk-PEI6RANI.js.map +1 -0
- package/dist/chunk-R3AZOAB5.js +933 -0
- package/dist/chunk-R3AZOAB5.js.map +1 -0
- package/dist/{chunk-O4VXOBGF.js → chunk-WSK7G5LE.js} +70 -2
- package/dist/chunk-WSK7G5LE.js.map +1 -0
- package/dist/{chunk-23EJ676W.js → chunk-XBZWOM2K.js} +9 -4
- package/dist/chunk-XBZWOM2K.js.map +1 -0
- package/dist/{chunk-XCNTXWBB.js → chunk-Y7OEDUJ5.js} +856 -338
- package/dist/chunk-Y7OEDUJ5.js.map +1 -0
- package/dist/client.d.ts +10 -2
- package/dist/clipboard.d.ts +1 -0
- package/dist/clipboard.js +7 -0
- package/dist/clipboard.js.map +1 -0
- package/dist/commands/index.d.ts +3 -3
- package/dist/commands/registry.d.ts +1 -1
- package/dist/commands/types.d.ts +1 -1
- package/dist/components/approval-surface.d.ts +2 -2
- package/dist/components/artifacts/artifact-surface.d.ts +18 -0
- package/dist/components/artifacts/document-editor.d.ts +172 -0
- package/dist/components/artifacts/editable-artifact-ui.d.ts +13 -0
- package/dist/components/artifacts/editable-artifact-workbench.d.ts +49 -0
- package/dist/components/artifacts/editable-document.d.ts +24 -0
- package/dist/components/artifacts/editable-presentation.d.ts +24 -0
- package/dist/components/artifacts/editable-spreadsheet.d.ts +32 -0
- package/dist/components/artifacts/index.d.ts +9 -0
- package/dist/components/artifacts/presentation-editor.d.ts +297 -0
- package/dist/components/artifacts/published-html-artifact-frame.d.ts +14 -0
- package/dist/components/artifacts/spreadsheet-canvas.d.ts +120 -0
- package/dist/components/artifacts/spreadsheet-grid.d.ts +113 -0
- package/dist/components/browser-input.d.ts +7 -0
- package/dist/components/browser-viewer.d.ts +30 -0
- package/dist/components/chat-composer.d.ts +8 -8
- package/dist/components/code-editor.d.ts +25 -3
- package/dist/components/command-palette.d.ts +2 -2
- package/dist/components/composer-transcription-control.d.ts +2 -2
- package/dist/components/composer.d.ts +17 -17
- package/dist/components/computer-viewer.d.ts +24 -0
- package/dist/components/copy-button.d.ts +2 -2
- package/dist/components/desktop-viewer.d.ts +50 -22
- package/dist/components/diff-view.d.ts +1 -1
- package/dist/components/enrollment-consent.d.ts +1 -1
- package/dist/components/enrollment-device-flow.d.ts +1 -1
- package/dist/components/file-browser.d.ts +2 -2
- package/dist/components/fleet-tile.d.ts +1 -1
- package/dist/components/generated-video-player.d.ts +10 -0
- package/dist/components/human-input-form.d.ts +1 -1
- package/dist/components/human-input-surface.d.ts +2 -2
- package/dist/components/interaction-intervention-banner.d.ts +11 -0
- package/dist/components/interactive-workbench-viewers.d.ts +2 -0
- package/dist/components/machine-card.d.ts +2 -2
- package/dist/components/machine-dock-bar.d.ts +3 -3
- package/dist/components/machine-health-pill.d.ts +2 -2
- package/dist/components/machine-input-display.d.ts +1 -1
- package/dist/components/machine-metrics.d.ts +2 -2
- package/dist/components/machine-status-pill.d.ts +4 -4
- package/dist/components/machines/health.d.ts +1 -1
- package/dist/components/machines/machine-detail.d.ts +3 -3
- package/dist/components/machines/metric-history-chart.d.ts +1 -1
- package/dist/components/machines/metric-sparkline.d.ts +2 -2
- package/dist/components/machines/series.d.ts +2 -2
- package/dist/components/machines-dashboard.d.ts +2 -2
- package/dist/components/markdown.d.ts +1 -1
- package/dist/components/message-timeline.d.ts +12 -5
- package/dist/components/model-picker.d.ts +2 -2
- package/dist/components/model-policy-picker.d.ts +7 -7
- package/dist/components/pierre-diff.d.ts +1 -1
- package/dist/components/pierre-file.d.ts +1 -1
- package/dist/components/queue-draft-policy.d.ts +1 -1
- package/dist/components/queue-surface-implementation.d.ts +3 -3
- package/dist/components/queue-surface-state.d.ts +4 -4
- package/dist/components/queue-surface.d.ts +5 -5
- package/dist/components/sandbox-files.d.ts +3 -3
- package/dist/components/sandbox-terminal.d.ts +24 -4
- package/dist/components/sandbox-workspace.d.ts +28 -9
- package/dist/components/session-chrome.d.ts +4 -4
- package/dist/components/session-status.d.ts +2 -2
- package/dist/components/timeline-annotation-selection.d.ts +9 -0
- package/dist/components/timeline-annotations-dialog.d.ts +16 -0
- package/dist/components/timeline-annotations.d.ts +13 -0
- package/dist/components/tooltip.d.ts +4 -4
- package/dist/components/user-message-body.d.ts +2 -2
- package/dist/components/workbench-changes.d.ts +2 -2
- package/dist/components/workspace-dock.d.ts +1 -1
- package/dist/composer.d.ts +11 -11
- package/dist/composer.js +7 -7
- package/dist/hooks/internal.d.ts +1 -1
- package/dist/hooks/use-attached-browsers.d.ts +21 -0
- package/dist/hooks/use-auth-runs.d.ts +32 -0
- package/dist/hooks/use-available-models.d.ts +2 -2
- package/dist/hooks/use-billing-usage.d.ts +1 -1
- package/dist/hooks/use-browser-downloads.d.ts +25 -0
- package/dist/hooks/use-browser-frame-stream.d.ts +28 -0
- package/dist/hooks/use-browser-identities.d.ts +30 -0
- package/dist/hooks/use-browser-session.d.ts +29 -0
- package/dist/hooks/use-browser-sessions.d.ts +32 -0
- package/dist/hooks/use-channels.d.ts +26 -0
- package/dist/hooks/use-codex-accounts.d.ts +2 -2
- package/dist/hooks/use-composer.d.ts +36 -9
- package/dist/hooks/use-computer-frame-stream.d.ts +24 -0
- package/dist/hooks/use-computer-session.d.ts +29 -0
- package/dist/hooks/use-computer-sessions.d.ts +28 -0
- package/dist/hooks/use-desktop-stream.d.ts +5 -1
- package/dist/hooks/use-environments.d.ts +1 -1
- package/dist/hooks/use-file-attachments.d.ts +1 -1
- package/dist/hooks/use-goal.d.ts +2 -2
- package/dist/hooks/use-human-input.d.ts +2 -2
- package/dist/hooks/use-interaction-interventions.d.ts +27 -0
- package/dist/hooks/use-interaction-invalidation.d.ts +19 -0
- package/dist/hooks/use-last-started-turn-policy.d.ts +2 -2
- package/dist/hooks/use-machine-chip.d.ts +2 -2
- package/dist/hooks/use-machines.d.ts +4 -4
- package/dist/hooks/use-network-routes.d.ts +25 -0
- package/dist/hooks/use-packs.d.ts +7 -2
- package/dist/hooks/use-rigs.d.ts +1 -1
- package/dist/hooks/use-sandbox-files.d.ts +6 -4
- package/dist/hooks/use-sandbox-git.d.ts +6 -5
- package/dist/hooks/use-sandbox-terminal.d.ts +1 -1
- package/dist/hooks/use-scheduled-tasks.d.ts +1 -1
- package/dist/hooks/use-session-capabilities.d.ts +5 -4
- package/dist/hooks/use-session-control.d.ts +1 -1
- package/dist/hooks/use-session-events.d.ts +2 -2
- package/dist/hooks/use-session-lineage.d.ts +1 -1
- package/dist/hooks/use-session-mcp-approval-policy.d.ts +2 -2
- package/dist/hooks/use-session.d.ts +2 -2
- package/dist/hooks/use-site-auth-connections.d.ts +25 -0
- package/dist/hooks/use-slash-commands.d.ts +1 -1
- package/dist/hooks/use-supergrok-accounts.d.ts +26 -0
- package/dist/hooks/use-terminal-stream.d.ts +11 -2
- package/dist/hooks/use-turn-queue.d.ts +2 -2
- package/dist/hooks/use-video-artifact-playback.d.ts +9 -0
- package/dist/hooks/use-voice-input.d.ts +1 -1
- package/dist/hooks/use-workspace-capture.d.ts +1 -1
- package/dist/hooks/use-workspace-edit.d.ts +1 -1
- package/dist/hooks/use-workspace-sessions.d.ts +1 -1
- package/dist/hooks/use-workspaces.d.ts +1 -1
- package/dist/index.d.ts +162 -155
- package/dist/index.js +1822 -1703
- package/dist/index.js.map +1 -1
- package/dist/interaction.d.ts +33 -0
- package/dist/interaction.js +550 -0
- package/dist/interaction.js.map +1 -0
- package/dist/interactive-workbench-viewers-TOHBXJEE.js +15 -0
- package/dist/interactive-workbench-viewers-TOHBXJEE.js.map +1 -0
- package/dist/lib/host-platform.d.ts +5 -0
- package/dist/lib/relay-wire.d.ts +24 -1
- package/dist/lib/sandbox-liveness.d.ts +9 -0
- package/dist/lib/terminal-capability.d.ts +5 -0
- package/dist/lib/xterm-theme.d.ts +1 -1
- package/dist/machines.d.ts +30 -30
- package/dist/machines.js +2 -2
- package/dist/model-policy.d.ts +1 -1
- package/dist/model-policy.js +1 -1
- package/dist/provider.d.ts +6 -5
- package/dist/{queue-surface-implementation-U5JFG7HT.js → queue-surface-implementation-2KVJTYXH.js} +100 -48
- package/dist/queue-surface-implementation-2KVJTYXH.js.map +1 -0
- package/dist/realtime/dropdown-menu.d.ts +8 -8
- package/dist/realtime/realtime-control.d.ts +8 -8
- package/dist/realtime.d.ts +4 -4
- package/dist/realtime.js +38 -11
- package/dist/realtime.js.map +1 -1
- package/dist/session-context.d.ts +28 -2
- package/dist/session-ui.d.ts +14 -14
- package/dist/session-ui.js +5 -4
- package/dist/session.d.ts +28 -28
- package/dist/session.js +10 -10
- package/dist/timeline/activity-rail.d.ts +5 -3
- package/dist/timeline/disclosure-context.d.ts +1 -1
- package/dist/timeline/fleet-decision-projection.d.ts +1 -1
- package/dist/timeline/fleet-decision-row.d.ts +3 -3
- package/dist/timeline/index.d.ts +19 -19
- package/dist/timeline/parsers.d.ts +4 -1
- package/dist/timeline/projection.d.ts +3 -3
- package/dist/timeline/registry.d.ts +8 -2
- package/dist/timeline/screenshot-lightbox.d.ts +2 -2
- package/dist/timeline/shared.d.ts +21 -10
- package/dist/timeline/tool-diff.d.ts +2 -2
- package/dist/timeline/tool-renderers.d.ts +1 -1
- package/dist/timeline/turn-summary.d.ts +3 -3
- package/dist/timeline/types.d.ts +44 -3
- package/dist/timeline-annotation-selection-3L7LHGG2.js +187 -0
- package/dist/timeline-annotation-selection-3L7LHGG2.js.map +1 -0
- package/dist/timeline-annotations-dialog-U7EVLR75.js +196 -0
- package/dist/timeline-annotations-dialog-U7EVLR75.js.map +1 -0
- package/package.json +36 -7
- package/src/artifacts-document.ts +31 -0
- package/src/artifacts-presentation.ts +36 -0
- package/src/artifacts-spreadsheet.ts +21 -0
- package/src/artifacts.ts +89 -0
- package/src/client.ts +157 -1
- package/src/clipboard.ts +2 -0
- package/src/components/artifacts/artifact-surface.tsx +93 -0
- package/src/components/artifacts/document-editor.tsx +2255 -0
- package/src/components/artifacts/editable-artifact-ui.tsx +89 -0
- package/src/components/artifacts/editable-artifact-workbench.tsx +203 -0
- package/src/components/artifacts/editable-document.tsx +657 -0
- package/src/components/artifacts/editable-presentation.tsx +1020 -0
- package/src/components/artifacts/editable-spreadsheet.tsx +733 -0
- package/src/components/artifacts/index.ts +104 -0
- package/src/components/artifacts/presentation-editor.tsx +3148 -0
- package/src/components/artifacts/published-html-artifact-frame.tsx +38 -0
- package/src/components/artifacts/spreadsheet-canvas.ts +799 -0
- package/src/components/artifacts/spreadsheet-grid.tsx +2129 -0
- package/src/components/browser-input.ts +65 -0
- package/src/components/browser-viewer.tsx +2702 -0
- package/src/components/chat-composer.tsx +13 -0
- package/src/components/code-editor.tsx +89 -30
- package/src/components/composer-transcription-control.tsx +10 -6
- package/src/components/computer-viewer.tsx +1636 -0
- package/src/components/desktop-viewer.tsx +215 -74
- package/src/components/generated-video-player.tsx +89 -0
- package/src/components/interaction-intervention-banner.tsx +99 -0
- package/src/components/interactive-workbench-viewers.ts +2 -0
- package/src/components/machine-input-display.ts +3 -0
- package/src/components/machines/machine-detail.tsx +1 -1
- package/src/components/message-timeline.tsx +289 -32
- package/src/components/model-policy-picker.tsx +15 -0
- package/src/components/queue-surface-implementation.tsx +116 -60
- package/src/components/sandbox-files.tsx +2 -2
- package/src/components/sandbox-terminal.tsx +128 -46
- package/src/components/sandbox-workspace.tsx +256 -90
- package/src/components/session-chrome.tsx +53 -13
- package/src/components/timeline-annotation-selection.tsx +221 -0
- package/src/components/timeline-annotations-dialog.tsx +213 -0
- package/src/components/timeline-annotations.tsx +84 -0
- package/src/components/workbench-changes.tsx +188 -31
- package/src/components/workspace-dock.tsx +59 -5
- package/src/hooks/use-attached-browsers.ts +154 -0
- package/src/hooks/use-auth-runs.ts +278 -0
- package/src/hooks/use-browser-downloads.ts +218 -0
- package/src/hooks/use-browser-frame-stream.ts +425 -0
- package/src/hooks/use-browser-identities.ts +266 -0
- package/src/hooks/use-browser-session.ts +543 -0
- package/src/hooks/use-browser-sessions.ts +286 -0
- package/src/hooks/use-channels.ts +99 -0
- package/src/hooks/use-composer.ts +769 -24
- package/src/hooks/use-computer-frame-stream.ts +434 -0
- package/src/hooks/use-computer-session.ts +462 -0
- package/src/hooks/use-computer-sessions.ts +251 -0
- package/src/hooks/use-desktop-stream.ts +39 -7
- package/src/hooks/use-interaction-interventions.ts +242 -0
- package/src/hooks/use-interaction-invalidation.ts +66 -0
- package/src/hooks/use-machine-chip.ts +15 -3
- package/src/hooks/use-machines.ts +33 -3
- package/src/hooks/use-network-routes.ts +214 -0
- package/src/hooks/use-packs.ts +51 -0
- package/src/hooks/use-sandbox-files.ts +98 -55
- package/src/hooks/use-sandbox-git.ts +93 -38
- package/src/hooks/use-sandbox-terminal.ts +3 -2
- package/src/hooks/use-session-capabilities.ts +59 -46
- package/src/hooks/use-site-auth-connections.ts +226 -0
- package/src/hooks/use-supergrok-accounts.ts +81 -0
- package/src/hooks/use-terminal-stream.ts +359 -47
- package/src/hooks/use-video-artifact-playback.ts +28 -0
- package/src/hooks/use-workspace-edit.ts +2 -1
- package/src/index.ts +18 -0
- package/src/interaction.ts +101 -0
- package/src/lib/host-platform.ts +10 -0
- package/src/lib/relay-wire.ts +47 -4
- package/src/lib/sandbox-liveness.ts +11 -0
- package/src/lib/terminal-capability.ts +10 -0
- package/src/model-policy.ts +19 -4
- package/src/provider.tsx +133 -1
- package/src/realtime/realtime-control.tsx +34 -6
- package/src/session-context.ts +156 -1
- package/src/timeline/activity-rail.tsx +25 -3
- package/src/timeline/index.ts +3 -0
- package/src/timeline/parsers.ts +14 -1
- package/src/timeline/projection.ts +269 -14
- package/src/timeline/registry.ts +13 -1
- package/src/timeline/screenshot-lightbox.tsx +19 -9
- package/src/timeline/shared.tsx +56 -8
- package/src/timeline/tool-renderers.tsx +303 -50
- package/src/timeline/types.ts +57 -3
- package/styles/compiled.css +312 -21
- package/styles/index.css +15 -0
- package/styles/responsive.css +13 -1
- package/dist/chunk-23EJ676W.js.map +0 -1
- package/dist/chunk-EKZH6IDG.js.map +0 -1
- package/dist/chunk-FWXGSNXT.js.map +0 -1
- package/dist/chunk-LHLYWG5R.js.map +0 -1
- package/dist/chunk-O4VXOBGF.js.map +0 -1
- package/dist/chunk-OL6QVXGF.js.map +0 -1
- package/dist/chunk-WYA65Y3F.js.map +0 -1
- package/dist/chunk-XCNTXWBB.js.map +0 -1
- package/dist/fleet-decision-projection-SLNZMS42.js +0 -241
- package/dist/fleet-decision-projection-SLNZMS42.js.map +0 -1
- package/dist/queue-surface-implementation-U5JFG7HT.js.map +0 -1
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
GitFileDiff,
|
|
3
|
+
SessionCapabilities,
|
|
3
4
|
SessionEvent,
|
|
4
5
|
WorkspaceCaptureManifest,
|
|
5
6
|
WorkspaceCaptureRepo,
|
|
6
7
|
} from "@opengeni/sdk";
|
|
7
8
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
8
9
|
import { type ClientOverride, useOpenGeni } from "../provider";
|
|
10
|
+
import { sandboxAcceptsLiveIo } from "../lib/sandbox-liveness";
|
|
9
11
|
|
|
10
12
|
export type UseSandboxGitOptions = ClientOverride & {
|
|
11
13
|
/** Live event log (usually `useSessionEvents().events`) — drives auto-refresh
|
|
@@ -26,9 +28,10 @@ export type UseSandboxGitOptions = ClientOverride & {
|
|
|
26
28
|
enabled?: boolean | undefined;
|
|
27
29
|
/** Pause live reads/invalidation while preserving the last rendered result. */
|
|
28
30
|
active?: boolean | undefined;
|
|
29
|
-
/** Lease liveness
|
|
30
|
-
*
|
|
31
|
-
|
|
31
|
+
/** Lease liveness. `null` means capability negotiation is still pending and
|
|
32
|
+
* blocks live I/O. When not warm, the diff is served from the capture instead
|
|
33
|
+
* of a live `gitDiff` RPC. Omission preserves legacy direct-hook behavior. */
|
|
34
|
+
liveness?: SessionCapabilities["liveness"] | null | undefined;
|
|
32
35
|
/** The latest turn-end capture (from `useWorkspaceCapture`). Seeds the diff
|
|
33
36
|
* immediately; a warm box reconciles live and leaves this durable review
|
|
34
37
|
* surface in place if the live request is temporarily unavailable. */
|
|
@@ -93,6 +96,11 @@ function uniqueRoots(roots: readonly string[]): string[] {
|
|
|
93
96
|
return [...new Set(roots.map(normalizeRoot))].sort((a, b) => a.localeCompare(b));
|
|
94
97
|
}
|
|
95
98
|
|
|
99
|
+
// GitReadBatchRequest is contract-bounded to 32 items. Keep workspace-wide
|
|
100
|
+
// reads bounded without changing the wire limit; sequential chunks preserve
|
|
101
|
+
// deterministic root/result mapping and cap Channel-A lease concurrency at one.
|
|
102
|
+
const GIT_READ_BATCH_REQUEST_LIMIT = 32;
|
|
103
|
+
|
|
96
104
|
/** Find the capture repo matching `repoPath` (default workspace root). */
|
|
97
105
|
function repoForPath(
|
|
98
106
|
manifest: WorkspaceCaptureManifest,
|
|
@@ -201,7 +209,13 @@ export function useSandboxGit(
|
|
|
201
209
|
// remains live-only; legacy captures simply lack branchDiff and fall back via
|
|
202
210
|
// the workspace controller without inventing an empty branch comparison.
|
|
203
211
|
const capture = comparison === "staged" ? null : (options.capture ?? null);
|
|
204
|
-
const isLive = options.liveness
|
|
212
|
+
const isLive = sandboxAcceptsLiveIo(options.liveness);
|
|
213
|
+
// `liveness` is optional for compatibility with direct hook consumers. Explicit
|
|
214
|
+
// `null` (negotiating) and lifecycle values require authoritative warm.
|
|
215
|
+
const acceptsEventReads = options.liveness === undefined || isLive;
|
|
216
|
+
const acceptsEventReadsRef = useRef(acceptsEventReads);
|
|
217
|
+
acceptsEventReadsRef.current = acceptsEventReads;
|
|
218
|
+
const hasLiveIoFence = options.liveness !== undefined;
|
|
205
219
|
const identityKey = `${workspaceId}\u0000${sessionId ?? ""}\u0000${repoPathsKey}\u0000${workspaceWide}\u0000${comparison}`;
|
|
206
220
|
|
|
207
221
|
const [diff, setDiff] = useState<SandboxGitFileDiff[]>([]);
|
|
@@ -251,35 +265,54 @@ export function useSandboxGit(
|
|
|
251
265
|
// take several seconds on a remote sandbox. The legacy root probe remains
|
|
252
266
|
// serial so a non-repository workspace never receives a failing git diff.
|
|
253
267
|
const prefetched = hasAdvertisedRepoPaths
|
|
254
|
-
? await
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
268
|
+
? await (async () => {
|
|
269
|
+
const prefetchedRepos: Array<{
|
|
270
|
+
root: string;
|
|
271
|
+
status: Awaited<ReturnType<typeof client.gitStatus>>;
|
|
272
|
+
result: Awaited<ReturnType<typeof client.gitDiff>>;
|
|
273
|
+
}> = [];
|
|
274
|
+
for (
|
|
275
|
+
let offset = 0;
|
|
276
|
+
offset < repoPaths.length;
|
|
277
|
+
offset += GIT_READ_BATCH_REQUEST_LIMIT
|
|
278
|
+
) {
|
|
279
|
+
const roots = repoPaths.slice(offset, offset + GIT_READ_BATCH_REQUEST_LIMIT);
|
|
280
|
+
const batch = await client.gitReadBatch(
|
|
281
|
+
workspaceId,
|
|
282
|
+
sessionId,
|
|
283
|
+
{
|
|
284
|
+
requests: roots.map((root) => ({
|
|
285
|
+
status: { path: root },
|
|
286
|
+
diff: {
|
|
287
|
+
path: root,
|
|
288
|
+
...(comparison === "branch"
|
|
289
|
+
? { fromRef: "origin/HEAD", includeUntracked: true }
|
|
290
|
+
: comparison === "working"
|
|
291
|
+
? { fromRef: "HEAD", includeUntracked: true }
|
|
292
|
+
: { staged: true, includeUntracked: false }),
|
|
293
|
+
},
|
|
294
|
+
})),
|
|
280
295
|
},
|
|
281
|
-
|
|
282
|
-
|
|
296
|
+
{ signal: refreshAbort.signal },
|
|
297
|
+
);
|
|
298
|
+
if (batch.results.length !== roots.length) {
|
|
299
|
+
throw new Error(
|
|
300
|
+
`Workspace Git batch returned ${batch.results.length} results for ${roots.length} repositories.`,
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
batch.results.forEach((item, index) => {
|
|
304
|
+
prefetchedRepos.push({
|
|
305
|
+
root: roots[index]!,
|
|
306
|
+
status: item.status,
|
|
307
|
+
result: item.diff ?? {
|
|
308
|
+
files: [],
|
|
309
|
+
revision: item.status.revision,
|
|
310
|
+
},
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
return prefetchedRepos;
|
|
315
|
+
})()
|
|
283
316
|
: null;
|
|
284
317
|
const statuses =
|
|
285
318
|
prefetched ??
|
|
@@ -427,7 +460,7 @@ export function useSandboxGit(
|
|
|
427
460
|
if (!active) setLoading(false);
|
|
428
461
|
return;
|
|
429
462
|
}
|
|
430
|
-
if (isLive || !currentCapture) {
|
|
463
|
+
if (isLive || (!hasLiveIoFence && !currentCapture)) {
|
|
431
464
|
void refresh();
|
|
432
465
|
}
|
|
433
466
|
return () => {
|
|
@@ -435,14 +468,36 @@ export function useSandboxGit(
|
|
|
435
468
|
refreshAbortRef.current = null;
|
|
436
469
|
refreshGenerationRef.current += 1;
|
|
437
470
|
};
|
|
438
|
-
}, [
|
|
471
|
+
}, [
|
|
472
|
+
enabled,
|
|
473
|
+
active,
|
|
474
|
+
isLive,
|
|
475
|
+
hasLiveIoFence,
|
|
476
|
+
captureRevision,
|
|
477
|
+
identityKey,
|
|
478
|
+
refresh,
|
|
479
|
+
seedFromCapture,
|
|
480
|
+
]);
|
|
439
481
|
|
|
440
482
|
// git.changed → re-fetch the LIVE diff. Agent tool completion is also an
|
|
441
483
|
// invalidation point because shell/apply-patch writes can bypass Channel-A and
|
|
442
484
|
// therefore emit no git.changed event. This is event-driven, never polling.
|
|
443
485
|
const events = options.events;
|
|
444
486
|
useEffect(() => {
|
|
445
|
-
if (!enabled || !
|
|
487
|
+
if (!enabled || !events) {
|
|
488
|
+
if (commandRefreshTimerRef.current) {
|
|
489
|
+
clearTimeout(commandRefreshTimerRef.current);
|
|
490
|
+
commandRefreshTimerRef.current = null;
|
|
491
|
+
}
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
// Historical events describe the capture we already loaded. Inactive tabs
|
|
495
|
+
// and holderless draining/cold leases advance the cursor without issuing
|
|
496
|
+
// provider I/O; becoming warm performs one authoritative reconcile above.
|
|
497
|
+
if (!active || !acceptsEventReads) {
|
|
498
|
+
for (const event of events) {
|
|
499
|
+
if (event.sequence > lastChangeRef.current) lastChangeRef.current = event.sequence;
|
|
500
|
+
}
|
|
446
501
|
if (commandRefreshTimerRef.current) {
|
|
447
502
|
clearTimeout(commandRefreshTimerRef.current);
|
|
448
503
|
commandRefreshTimerRef.current = null;
|
|
@@ -467,15 +522,15 @@ export function useSandboxGit(
|
|
|
467
522
|
if (immediate) {
|
|
468
523
|
if (commandRefreshTimerRef.current) clearTimeout(commandRefreshTimerRef.current);
|
|
469
524
|
commandRefreshTimerRef.current = null;
|
|
470
|
-
void refresh();
|
|
525
|
+
if (acceptsEventReadsRef.current) void refresh();
|
|
471
526
|
} else if (commandDelta && !commandRefreshTimerRef.current) {
|
|
472
527
|
commandRefreshTimerRef.current = setTimeout(() => {
|
|
473
528
|
commandRefreshTimerRef.current = null;
|
|
474
|
-
void refresh();
|
|
529
|
+
if (acceptsEventReadsRef.current) void refresh();
|
|
475
530
|
}, 1_000);
|
|
476
531
|
}
|
|
477
532
|
}
|
|
478
|
-
}, [enabled, active, events, refresh]);
|
|
533
|
+
}, [enabled, active, acceptsEventReads, events, refresh]);
|
|
479
534
|
|
|
480
535
|
useEffect(
|
|
481
536
|
() => () => {
|
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
import { OpenGeniApiError } from "@opengeni/sdk";
|
|
8
8
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
9
9
|
import { useOpenGeni, type ClientOverride } from "../provider";
|
|
10
|
+
import { sandboxAcceptsLiveIo } from "../lib/sandbox-liveness";
|
|
10
11
|
|
|
11
12
|
export type TerminalChunk = {
|
|
12
13
|
/** Stable key (the source event id) so the xterm writer tracks a written-cursor. */
|
|
@@ -107,9 +108,9 @@ export function useSandboxTerminal(
|
|
|
107
108
|
// projection is the single source of truth for what's written). The open is
|
|
108
109
|
// gated on a WARM box: opening on a cold/warming lease races the box (the PTY
|
|
109
110
|
// exec-session is created on a box that the next op may not resume), which is
|
|
110
|
-
// exactly the "session not found" terminal failure; we wait for warm
|
|
111
|
+
// exactly the "session not found" terminal failure; we wait for warm.
|
|
111
112
|
const openInFlight = useRef(false);
|
|
112
|
-
const boxWarm = liveness === undefined || liveness
|
|
113
|
+
const boxWarm = liveness === undefined || sandboxAcceptsLiveIo(liveness);
|
|
113
114
|
useEffect(() => {
|
|
114
115
|
if (!interactive || !sessionId || ptyFilter || !boxWarm) {
|
|
115
116
|
setOpenError(null);
|
|
@@ -7,10 +7,13 @@ import {
|
|
|
7
7
|
} from "@opengeni/sdk";
|
|
8
8
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
9
9
|
import { useOpenGeni, type ClientOverride } from "../provider";
|
|
10
|
+
import { sandboxAcceptsLiveIo } from "../lib/sandbox-liveness";
|
|
11
|
+
import { terminalCanAcquirePty } from "../lib/terminal-capability";
|
|
10
12
|
import { usePageLiveActivity } from "./internal";
|
|
11
13
|
|
|
12
|
-
// "on-demand" is the boxless-benign resting state: the lease is
|
|
13
|
-
// asked to warm a box (no viewer/consent action, no
|
|
14
|
+
// "on-demand" is the boxless-benign resting state: the lease is not currently
|
|
15
|
+
// holder-backed and NOTHING asked to warm a box (no viewer/consent action, no
|
|
16
|
+
// files-tab warm). Under lazy
|
|
14
17
|
// provisioning a chat-only turn never creates a box, so this is the correct calm
|
|
15
18
|
// terminal state — NOT an error. It is distinct from "cold", which now means "a
|
|
16
19
|
// warm-up is genuinely in flight" (a viewer attach was requested) and therefore
|
|
@@ -84,7 +87,8 @@ export type UseSessionCapabilitiesResult = {
|
|
|
84
87
|
error: Error | null;
|
|
85
88
|
/**
|
|
86
89
|
* 409 from the desktop attach: the un-redacted (or shared) plane needs explicit
|
|
87
|
-
* acknowledgment before a viewer holder is granted.
|
|
90
|
+
* acknowledgment before a viewer holder is granted. The workbench uses this
|
|
91
|
+
* to complete acknowledgment automatically while Desktop is opening.
|
|
88
92
|
*/
|
|
89
93
|
acknowledgmentRequired: "unredacted" | "shared" | null;
|
|
90
94
|
/** 429 from the desktop attach: the per-session viewer cap is reached. */
|
|
@@ -112,24 +116,10 @@ function desktopAttachable(cell: SessionCapabilities["DesktopStream"]): boolean
|
|
|
112
116
|
|
|
113
117
|
/**
|
|
114
118
|
* Whether warming the box for the interactive terminal (pty-ws) is worth it.
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
* what flips `sse-events` → `pty-ws` by warming the box and minting the ttyd
|
|
119
|
-
* tunnel URL, so we attach whenever the terminal is not hard-unavailable.
|
|
119
|
+
* A real-PTY descriptor is attachable when already routed as `pty-ws`, or while
|
|
120
|
+
* transiently cold. A firehose-only backend and policy/OS/backend degradation
|
|
121
|
+
* cannot become interactive, so attaching there would only waste a holder.
|
|
120
122
|
*/
|
|
121
|
-
function terminalAttachable(cell: SessionCapabilities["Terminal"]): boolean {
|
|
122
|
-
if (cell.transport === null) {
|
|
123
|
-
// No terminal at all unless the only blocker is a transient cold state.
|
|
124
|
-
return (
|
|
125
|
-
cell.reason === "lease_cold" || cell.reason === "not_provisioned" || cell.reason === null
|
|
126
|
-
);
|
|
127
|
-
}
|
|
128
|
-
// Already pty-ws (warm) is fine to (re)attach; sse-events is the cold state the
|
|
129
|
-
// attach upgrades. Either way it's attachable.
|
|
130
|
-
return true;
|
|
131
|
-
}
|
|
132
|
-
|
|
133
123
|
/** Read `stream.url.rotated` payloads off the live event log, newest last. */
|
|
134
124
|
function rotationsFrom(events: SessionEvent[]): StreamUrlRotatedPayload[] {
|
|
135
125
|
const out: StreamUrlRotatedPayload[] = [];
|
|
@@ -149,8 +139,8 @@ function rotationsFrom(events: SessionEvent[]): StreamUrlRotatedPayload[] {
|
|
|
149
139
|
*
|
|
150
140
|
* Degradation is a value, never a crash: an unsupported surface comes back
|
|
151
141
|
* `available:false`/`transport:null` + a `reason`; the components render the
|
|
152
|
-
* reason-aware empty state. 409 (
|
|
153
|
-
* typed signals, not thrown.
|
|
142
|
+
* reason-aware empty state. 409 (acknowledgment) and 429 (viewer cap) are
|
|
143
|
+
* surfaced as typed signals, not thrown.
|
|
154
144
|
*/
|
|
155
145
|
export function useSessionCapabilities(
|
|
156
146
|
sessionId: string | null | undefined,
|
|
@@ -207,8 +197,15 @@ export function useSessionCapabilities(
|
|
|
207
197
|
if (!lifecycleEnabled || !sessionId) {
|
|
208
198
|
setState("idle");
|
|
209
199
|
setCapabilities(null);
|
|
200
|
+
viewerIdRef.current = null;
|
|
201
|
+
setViewerId(null);
|
|
210
202
|
return;
|
|
211
203
|
}
|
|
204
|
+
// The preceding effect cleanup released any old holder before this fresh
|
|
205
|
+
// lifecycle starts. Never project that stale id while re-negotiating or if
|
|
206
|
+
// the replacement grant degrades.
|
|
207
|
+
viewerIdRef.current = null;
|
|
208
|
+
setViewerId(null);
|
|
212
209
|
let cancelled = false;
|
|
213
210
|
let pollTimer: ReturnType<typeof setTimeout> | null = null;
|
|
214
211
|
let heartbeatTimer: ReturnType<typeof setTimeout> | null = null;
|
|
@@ -273,7 +270,7 @@ export function useSessionCapabilities(
|
|
|
273
270
|
.then((caps) => {
|
|
274
271
|
if (cancelled) return;
|
|
275
272
|
settle(caps);
|
|
276
|
-
if (caps.liveness
|
|
273
|
+
if (sandboxAcceptsLiveIo(caps.liveness)) {
|
|
277
274
|
setState("ready");
|
|
278
275
|
return;
|
|
279
276
|
}
|
|
@@ -308,8 +305,8 @@ export function useSessionCapabilities(
|
|
|
308
305
|
// attach serves BOTH live planes — the desktop pixel stream AND the
|
|
309
306
|
// interactive terminal (ttyd pty-ws) ride the same warm box and the same
|
|
310
307
|
// holder; the response folds the minted address for each requested plane.
|
|
311
|
-
// The
|
|
312
|
-
// rather than throwing — the tabs degrade gracefully.
|
|
308
|
+
// The acknowledgment requirement (409) and viewer cap (429) surface as
|
|
309
|
+
// typed signals rather than throwing — the tabs degrade gracefully.
|
|
313
310
|
//
|
|
314
311
|
// KEY: the handshake (getStreamCapabilities) NEVER spins up a cold box, so
|
|
315
312
|
// on a cold/warming lease the desktop cell comes back transport:null and
|
|
@@ -321,7 +318,7 @@ export function useSessionCapabilities(
|
|
|
321
318
|
// already live OR cold-but-feasible — and let POST /viewers warm the box and
|
|
322
319
|
// mint the URLs. Only a genuinely-unsupported reason suppresses the attach.
|
|
323
320
|
const wantDesktopAttach = attachDesktop && desktopAttachable(caps.DesktopStream);
|
|
324
|
-
const wantTerminalAttach = attachTerminal &&
|
|
321
|
+
const wantTerminalAttach = attachTerminal && terminalCanAcquirePty(caps.Terminal);
|
|
325
322
|
// The Files attach warms the box for a file WRITE (wake-on-edit intent). It
|
|
326
323
|
// folds no live URL (files are stateless HTTP) — the attach is purely a
|
|
327
324
|
// liveness refcount. Unlike the previous "keep the box warm while the Files
|
|
@@ -339,6 +336,8 @@ export function useSessionCapabilities(
|
|
|
339
336
|
// pty-ws terminal cell WITHOUT tripping the desktop consent 409.
|
|
340
337
|
const holder = await client.attachViewer(workspaceId, sessionId, {
|
|
341
338
|
desktop: wantDesktopAttach,
|
|
339
|
+
terminal: wantTerminalAttach,
|
|
340
|
+
files: wantFilesAttach,
|
|
342
341
|
});
|
|
343
342
|
if (cancelled) {
|
|
344
343
|
// The attach crossed an identity/unmount boundary after the server
|
|
@@ -348,6 +347,18 @@ export function useSessionCapabilities(
|
|
|
348
347
|
void client.detachViewer(workspaceId, sessionId, holder.viewerId).catch(() => {});
|
|
349
348
|
return;
|
|
350
349
|
}
|
|
350
|
+
if (
|
|
351
|
+
(wantDesktopAttach &&
|
|
352
|
+
(!holder.dataPlaneUrl || !holder.transport || !holder.client)) ||
|
|
353
|
+
(wantTerminalAttach && (!holder.terminalUrl || !holder.terminalTransport))
|
|
354
|
+
) {
|
|
355
|
+
// Nullable plane cells are valid graceful-degradation values at
|
|
356
|
+
// the raw API boundary, but an explicit UI grant must not become
|
|
357
|
+
// a successful holder that waits forever. Release it and surface
|
|
358
|
+
// a retryable error instead of retaining stale credentials.
|
|
359
|
+
await client.detachViewer(workspaceId, sessionId, holder.viewerId).catch(() => {});
|
|
360
|
+
throw new Error("live plane unavailable");
|
|
361
|
+
}
|
|
351
362
|
localViewerId = holder.viewerId;
|
|
352
363
|
viewerIdRef.current = holder.viewerId;
|
|
353
364
|
setViewerId(holder.viewerId);
|
|
@@ -367,24 +378,24 @@ export function useSessionCapabilities(
|
|
|
367
378
|
...prev.DesktopStream,
|
|
368
379
|
transport: holder.transport ?? prev.DesktopStream.transport,
|
|
369
380
|
client: holder.client ?? prev.DesktopStream.client,
|
|
370
|
-
url: holder.dataPlaneUrl
|
|
371
|
-
token: holder.streamToken
|
|
372
|
-
expiresAt: holder.streamExpiresAt
|
|
381
|
+
url: holder.dataPlaneUrl,
|
|
382
|
+
token: holder.streamToken,
|
|
383
|
+
expiresAt: holder.streamExpiresAt,
|
|
373
384
|
resolution: holder.resolution ?? prev.DesktopStream.resolution,
|
|
374
385
|
}
|
|
375
386
|
: prev.DesktopStream,
|
|
376
|
-
Terminal:
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
387
|
+
Terminal: wantTerminalAttach
|
|
388
|
+
? {
|
|
389
|
+
...prev.Terminal,
|
|
390
|
+
transport: holder.terminalTransport ?? prev.Terminal.transport,
|
|
391
|
+
url: holder.terminalUrl,
|
|
392
|
+
token: holder.terminalToken,
|
|
393
|
+
expiresAt: holder.terminalExpiresAt,
|
|
394
|
+
// A live pty-ws means the box is pty-capable for real.
|
|
395
|
+
ptyCapable: holder.terminalTransport ? true : prev.Terminal.ptyCapable,
|
|
396
|
+
reason: holder.terminalTransport ? null : prev.Terminal.reason,
|
|
397
|
+
}
|
|
398
|
+
: prev.Terminal,
|
|
388
399
|
}
|
|
389
400
|
: prev,
|
|
390
401
|
);
|
|
@@ -393,8 +404,8 @@ export function useSessionCapabilities(
|
|
|
393
404
|
if (cause instanceof OpenGeniApiError) {
|
|
394
405
|
if (cause.status === 409) {
|
|
395
406
|
// The un-redacted/shared consent gate is a DESKTOP requirement. A
|
|
396
|
-
// terminal-only warm attach needs no
|
|
397
|
-
// a
|
|
407
|
+
// terminal-only warm attach needs no acknowledgment, so a 409 there
|
|
408
|
+
// is not a desktop-open signal — the terminal stays on the read-only
|
|
398
409
|
// firehose. Only raise the consent requirement when the desktop was
|
|
399
410
|
// the (or a) reason we attached.
|
|
400
411
|
if (wantDesktopAttach) {
|
|
@@ -408,6 +419,8 @@ export function useSessionCapabilities(
|
|
|
408
419
|
setState("error");
|
|
409
420
|
setError(cause);
|
|
410
421
|
return;
|
|
422
|
+
} else {
|
|
423
|
+
throw cause;
|
|
411
424
|
}
|
|
412
425
|
// 409/429 are recoverable: structured surfaces still negotiated;
|
|
413
426
|
// keep going to set ready/cold below.
|
|
@@ -418,16 +431,16 @@ export function useSessionCapabilities(
|
|
|
418
431
|
}
|
|
419
432
|
|
|
420
433
|
// Whether ANYTHING asked to warm a box this negotiation. Only then is a
|
|
421
|
-
//
|
|
434
|
+
// non-warm lease "warming in flight" — worth polling, and worth surfacing a
|
|
422
435
|
// stall as an error. With no warm-up requested a cold lease is the benign
|
|
423
436
|
// boxless resting state (below), never an error.
|
|
424
437
|
const warmUpRequested = wantDesktopAttach || wantTerminalAttach || wantFilesAttach;
|
|
425
|
-
if (caps.liveness
|
|
438
|
+
if (sandboxAcceptsLiveIo(caps.liveness) || localViewerId) {
|
|
426
439
|
setState("ready");
|
|
427
440
|
startHeartbeat(caps);
|
|
428
441
|
} else if (warmUpRequested) {
|
|
429
442
|
// A warm-up is genuinely in flight (a viewer attach was requested) but the
|
|
430
|
-
// box isn't warm yet — poll until it
|
|
443
|
+
// box isn't holder-confirmed warm yet — poll until it is, with the deadline so a
|
|
431
444
|
// real stall surfaces as an error.
|
|
432
445
|
setState("cold");
|
|
433
446
|
pollUntilWarm();
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
CreateSiteAuthConnectionRequest,
|
|
3
|
+
SiteAuthConnection,
|
|
4
|
+
SiteAuthConnectionMutationResponse,
|
|
5
|
+
UpdateSiteAuthConnectionRequest,
|
|
6
|
+
} from "@opengeni/sdk/interaction";
|
|
7
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
8
|
+
import type { EmbeddedBrowserInteractionClientLike } from "../client";
|
|
9
|
+
import {
|
|
10
|
+
type EmbeddedBrowserInteractionClientOverride,
|
|
11
|
+
useEmbeddedBrowserInteraction,
|
|
12
|
+
} from "../session-context";
|
|
13
|
+
import { useInteractionInvalidation } from "./use-interaction-invalidation";
|
|
14
|
+
|
|
15
|
+
export type UseSiteAuthConnectionsOptions = EmbeddedBrowserInteractionClientOverride & {
|
|
16
|
+
enabled?: boolean | undefined;
|
|
17
|
+
includeArchived?: boolean | undefined;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type UseSiteAuthConnectionsResult = {
|
|
21
|
+
revision: number;
|
|
22
|
+
connections: SiteAuthConnection[];
|
|
23
|
+
loading: boolean;
|
|
24
|
+
refreshing: boolean;
|
|
25
|
+
mutating: boolean;
|
|
26
|
+
error: Error | null;
|
|
27
|
+
refresh: () => Promise<void>;
|
|
28
|
+
create: (
|
|
29
|
+
request: Omit<CreateSiteAuthConnectionRequest, "operationId"> & { operationId?: string },
|
|
30
|
+
) => Promise<SiteAuthConnectionMutationResponse>;
|
|
31
|
+
update: (
|
|
32
|
+
connectionId: string,
|
|
33
|
+
request: Omit<UpdateSiteAuthConnectionRequest, "operationId"> & {
|
|
34
|
+
operationId?: string;
|
|
35
|
+
},
|
|
36
|
+
) => Promise<SiteAuthConnectionMutationResponse>;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** Secret-free workspace login recipes and their verified health. */
|
|
40
|
+
export function useSiteAuthConnections(
|
|
41
|
+
options: UseSiteAuthConnectionsOptions = {},
|
|
42
|
+
): UseSiteAuthConnectionsResult {
|
|
43
|
+
const { client, workspaceId, workspaceInteractionEvent, workspaceInteractionConnectionState } =
|
|
44
|
+
useEmbeddedBrowserInteraction(options);
|
|
45
|
+
const enabled = options.enabled ?? true;
|
|
46
|
+
const includeArchived = options.includeArchived ?? false;
|
|
47
|
+
const key = `${workspaceId}:${includeArchived ? "all" : "active"}`;
|
|
48
|
+
const [state, setState] = useState(() => emptyState(key, enabled));
|
|
49
|
+
const [mutationCount, setMutationCount] = useState(0);
|
|
50
|
+
const requestRef = useRef<{ id: number; controller: AbortController | null }>({
|
|
51
|
+
id: 0,
|
|
52
|
+
controller: null,
|
|
53
|
+
});
|
|
54
|
+
const mountedRef = useRef(true);
|
|
55
|
+
const visible = state.key === key ? state : emptyState(key, enabled);
|
|
56
|
+
|
|
57
|
+
const cancelLoad = useCallback(() => {
|
|
58
|
+
requestRef.current.controller?.abort();
|
|
59
|
+
requestRef.current = { id: requestRef.current.id + 1, controller: null };
|
|
60
|
+
}, []);
|
|
61
|
+
|
|
62
|
+
const load = useCallback(
|
|
63
|
+
async (foreground: boolean): Promise<void> => {
|
|
64
|
+
if (!enabled) return;
|
|
65
|
+
const id = requestRef.current.id + 1;
|
|
66
|
+
requestRef.current.controller?.abort();
|
|
67
|
+
const controller = new AbortController();
|
|
68
|
+
requestRef.current = { id, controller };
|
|
69
|
+
setState((current) => ({
|
|
70
|
+
...(current.key === key ? current : emptyState(key, true)),
|
|
71
|
+
loading: foreground,
|
|
72
|
+
refreshing: !foreground,
|
|
73
|
+
error: foreground || current.key !== key ? null : current.error,
|
|
74
|
+
}));
|
|
75
|
+
try {
|
|
76
|
+
const response = await client.listSiteAuthConnections(workspaceId, {
|
|
77
|
+
includeArchived,
|
|
78
|
+
signal: controller.signal,
|
|
79
|
+
});
|
|
80
|
+
if (!mountedRef.current || requestRef.current.id !== id) return;
|
|
81
|
+
setState({
|
|
82
|
+
key,
|
|
83
|
+
revision: response.revision,
|
|
84
|
+
connections: sortConnections(response.connections),
|
|
85
|
+
loading: false,
|
|
86
|
+
refreshing: false,
|
|
87
|
+
error: null,
|
|
88
|
+
});
|
|
89
|
+
} catch (cause) {
|
|
90
|
+
if (controller.signal.aborted || !mountedRef.current || requestRef.current.id !== id)
|
|
91
|
+
return;
|
|
92
|
+
setState((current) => ({
|
|
93
|
+
...(current.key === key ? current : emptyState(key, true)),
|
|
94
|
+
loading: false,
|
|
95
|
+
refreshing: false,
|
|
96
|
+
error: asError(cause),
|
|
97
|
+
}));
|
|
98
|
+
} finally {
|
|
99
|
+
if (requestRef.current.id === id) requestRef.current = { id, controller: null };
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
[client, enabled, includeArchived, key, workspaceId],
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const refresh = useCallback(async () => await load(false), [load]);
|
|
106
|
+
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
mountedRef.current = true;
|
|
109
|
+
if (!enabled) {
|
|
110
|
+
cancelLoad();
|
|
111
|
+
setState(emptyState(key, false));
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
void load(true);
|
|
115
|
+
return () => {
|
|
116
|
+
mountedRef.current = false;
|
|
117
|
+
cancelLoad();
|
|
118
|
+
};
|
|
119
|
+
}, [cancelLoad, enabled, key, load]);
|
|
120
|
+
|
|
121
|
+
useInteractionInvalidation({
|
|
122
|
+
workspaceId,
|
|
123
|
+
key,
|
|
124
|
+
enabled,
|
|
125
|
+
event: workspaceInteractionEvent,
|
|
126
|
+
connectionState: workspaceInteractionConnectionState,
|
|
127
|
+
knownRevision: visible.revision,
|
|
128
|
+
refresh,
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const mutate = useCallback(async <T>(operation: () => Promise<T>): Promise<T> => {
|
|
132
|
+
setMutationCount((count) => count + 1);
|
|
133
|
+
try {
|
|
134
|
+
return await operation();
|
|
135
|
+
} finally {
|
|
136
|
+
if (mountedRef.current) setMutationCount((count) => Math.max(0, count - 1));
|
|
137
|
+
}
|
|
138
|
+
}, []);
|
|
139
|
+
|
|
140
|
+
const merge = useCallback(
|
|
141
|
+
(connection: SiteAuthConnection) => {
|
|
142
|
+
setState((current) => {
|
|
143
|
+
if (current.key !== key) return current;
|
|
144
|
+
const connections = current.connections.filter(
|
|
145
|
+
(candidate) => candidate.id !== connection.id,
|
|
146
|
+
);
|
|
147
|
+
if (includeArchived || connection.status !== "archived") connections.push(connection);
|
|
148
|
+
return { ...current, connections: sortConnections(connections), error: null };
|
|
149
|
+
});
|
|
150
|
+
},
|
|
151
|
+
[includeArchived, key],
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const create = useCallback(
|
|
155
|
+
async (
|
|
156
|
+
request: Omit<CreateSiteAuthConnectionRequest, "operationId"> & {
|
|
157
|
+
operationId?: string;
|
|
158
|
+
},
|
|
159
|
+
): Promise<SiteAuthConnectionMutationResponse> =>
|
|
160
|
+
await mutate(async () => {
|
|
161
|
+
const response = await client.createSiteAuthConnection(workspaceId, {
|
|
162
|
+
...request,
|
|
163
|
+
operationId: request.operationId ?? crypto.randomUUID(),
|
|
164
|
+
});
|
|
165
|
+
merge(response.connection);
|
|
166
|
+
return response;
|
|
167
|
+
}),
|
|
168
|
+
[client, merge, mutate, workspaceId],
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
const update = useCallback(
|
|
172
|
+
async (
|
|
173
|
+
connectionId: string,
|
|
174
|
+
request: Omit<UpdateSiteAuthConnectionRequest, "operationId"> & {
|
|
175
|
+
operationId?: string;
|
|
176
|
+
},
|
|
177
|
+
): Promise<SiteAuthConnectionMutationResponse> =>
|
|
178
|
+
await mutate(async () => {
|
|
179
|
+
const response = await client.updateSiteAuthConnection(workspaceId, connectionId, {
|
|
180
|
+
...request,
|
|
181
|
+
operationId: request.operationId ?? crypto.randomUUID(),
|
|
182
|
+
});
|
|
183
|
+
merge(response.connection);
|
|
184
|
+
return response;
|
|
185
|
+
}),
|
|
186
|
+
[client, merge, mutate, workspaceId],
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
return {
|
|
190
|
+
revision: visible.revision,
|
|
191
|
+
connections: visible.connections,
|
|
192
|
+
loading: visible.loading,
|
|
193
|
+
refreshing: visible.refreshing,
|
|
194
|
+
mutating: mutationCount > 0,
|
|
195
|
+
error: visible.error,
|
|
196
|
+
refresh,
|
|
197
|
+
create,
|
|
198
|
+
update,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function emptyState(key: string, loading: boolean) {
|
|
203
|
+
return {
|
|
204
|
+
key,
|
|
205
|
+
revision: 0,
|
|
206
|
+
connections: [] as SiteAuthConnection[],
|
|
207
|
+
loading,
|
|
208
|
+
refreshing: false,
|
|
209
|
+
error: null as Error | null,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function sortConnections(connections: readonly SiteAuthConnection[]): SiteAuthConnection[] {
|
|
214
|
+
return [...connections].sort(
|
|
215
|
+
(left, right) =>
|
|
216
|
+
Number(left.status === "archived") - Number(right.status === "archived") ||
|
|
217
|
+
left.name.localeCompare(right.name) ||
|
|
218
|
+
left.accountLabel.localeCompare(right.accountLabel),
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function asError(cause: unknown): Error {
|
|
223
|
+
return cause instanceof Error ? cause : new Error(String(cause));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export type SiteAuthConnectionsClient = EmbeddedBrowserInteractionClientLike;
|