@opengeni/react 0.48.0 → 0.52.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +90 -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 +170 -0
- package/dist/artifacts.js.map +1 -0
- package/dist/chunk-6BBWWOOU.js +82 -0
- package/dist/chunk-6BBWWOOU.js.map +1 -0
- package/dist/{chunk-FWXGSNXT.js → chunk-724BW4TW.js} +585 -34
- package/dist/chunk-724BW4TW.js.map +1 -0
- package/dist/chunk-7I2KB3OK.js +3895 -0
- package/dist/chunk-7I2KB3OK.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-FWS3KC3Z.js} +3 -3
- package/dist/{chunk-EKZH6IDG.js → chunk-HDBG7XGO.js} +62 -1
- package/dist/chunk-HDBG7XGO.js.map +1 -0
- package/dist/chunk-HUIKIDM5.js +2148 -0
- package/dist/chunk-HUIKIDM5.js.map +1 -0
- package/dist/{chunk-XCNTXWBB.js → chunk-JAGDUCZQ.js} +854 -340
- package/dist/chunk-JAGDUCZQ.js.map +1 -0
- package/dist/{chunk-LHLYWG5R.js → chunk-JR3QFY6B.js} +413 -37
- package/dist/chunk-JR3QFY6B.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-PEI6RANI.js +3234 -0
- package/dist/chunk-PEI6RANI.js.map +1 -0
- package/dist/{chunk-NJ7M2B6Z.js → chunk-ROIRTM37.js} +18 -10
- package/dist/{chunk-NJ7M2B6Z.js.map → chunk-ROIRTM37.js.map} +1 -1
- package/dist/{chunk-O4VXOBGF.js → chunk-WSK7G5LE.js} +70 -2
- package/dist/chunk-WSK7G5LE.js.map +1 -0
- package/dist/{chunk-OL6QVXGF.js → chunk-X4S56Y2H.js} +27 -7
- package/dist/{chunk-OL6QVXGF.js.map → chunk-X4S56Y2H.js.map} +1 -1
- package/dist/client.d.ts +7 -1
- 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 +8 -0
- package/dist/components/artifacts/presentation-editor.d.ts +297 -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-viewer.d.ts +29 -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 +23 -0
- package/dist/components/copy-button.d.ts +2 -2
- package/dist/components/desktop-viewer.d.ts +25 -21
- 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/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 +21 -4
- package/dist/components/sandbox-workspace.d.ts +25 -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 +5 -5
- package/dist/hooks/internal.d.ts +1 -1
- package/dist/hooks/use-attached-browsers.d.ts +21 -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-frame-stream.d.ts +28 -0
- package/dist/hooks/use-browser-identities.d.ts +26 -0
- package/dist/hooks/use-browser-session.d.ts +28 -0
- package/dist/hooks/use-browser-sessions.d.ts +32 -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 +28 -0
- package/dist/hooks/use-computer-sessions.d.ts +28 -0
- package/dist/hooks/use-desktop-stream.d.ts +1 -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-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-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-slash-commands.d.ts +1 -1
- package/dist/hooks/use-terminal-stream.d.ts +8 -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 +158 -155
- package/dist/index.js +906 -462
- package/dist/index.js.map +1 -1
- package/dist/interaction.d.ts +23 -0
- package/dist/interaction.js +36 -0
- package/dist/interaction.js.map +1 -0
- package/dist/interactive-workbench-viewers-KHNOHWQE.js +13 -0
- package/dist/interactive-workbench-viewers-KHNOHWQE.js.map +1 -0
- package/dist/lib/relay-wire.d.ts +4 -0
- 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 +3 -3
- package/dist/provider.d.ts +4 -4
- 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 +6 -6
- package/dist/realtime.d.ts +4 -4
- package/dist/realtime.js +3 -3
- package/dist/session-context.d.ts +19 -1
- 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 +5 -5
- 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 +39 -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 +86 -0
- package/src/client.ts +87 -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 +99 -0
- package/src/components/artifacts/presentation-editor.tsx +3148 -0
- package/src/components/artifacts/spreadsheet-canvas.ts +799 -0
- package/src/components/artifacts/spreadsheet-grid.tsx +2129 -0
- package/src/components/browser-viewer.tsx +1592 -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 +1139 -0
- package/src/components/desktop-viewer.tsx +101 -73
- package/src/components/generated-video-player.tsx +89 -0
- package/src/components/interactive-workbench-viewers.ts +2 -0
- package/src/components/machine-input-display.ts +3 -0
- package/src/components/message-timeline.tsx +289 -32
- package/src/components/queue-surface-implementation.tsx +116 -60
- package/src/components/sandbox-files.tsx +2 -2
- package/src/components/sandbox-terminal.tsx +106 -41
- package/src/components/sandbox-workspace.tsx +215 -87
- 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/hooks/use-attached-browsers.ts +152 -0
- package/src/hooks/use-browser-frame-stream.ts +316 -0
- package/src/hooks/use-browser-identities.ts +225 -0
- package/src/hooks/use-browser-session.ts +476 -0
- package/src/hooks/use-browser-sessions.ts +281 -0
- package/src/hooks/use-composer.ts +769 -24
- package/src/hooks/use-computer-frame-stream.ts +311 -0
- package/src/hooks/use-computer-session.ts +442 -0
- package/src/hooks/use-computer-sessions.ts +245 -0
- package/src/hooks/use-machine-chip.ts +5 -3
- package/src/hooks/use-machines.ts +33 -3
- 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-terminal-stream.ts +102 -23
- package/src/hooks/use-video-artifact-playback.ts +28 -0
- package/src/hooks/use-workspace-edit.ts +2 -1
- package/src/index.ts +10 -0
- package/src/interaction.ts +71 -0
- package/src/lib/relay-wire.ts +4 -0
- package/src/lib/sandbox-liveness.ts +11 -0
- package/src/lib/terminal-capability.ts +10 -0
- package/src/session-context.ts +90 -0
- package/src/timeline/activity-rail.tsx +19 -3
- package/src/timeline/index.ts +3 -0
- package/src/timeline/parsers.ts +14 -1
- package/src/timeline/projection.ts +211 -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 +55 -3
- package/styles/compiled.css +248 -15
- package/styles/index.css +15 -0
- package/styles/responsive.css +13 -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-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
- /package/dist/{chunk-I4MPFDRP.js.map → chunk-FWS3KC3Z.js.map} +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useMemo } from "react";
|
|
2
2
|
import type { MachineState } from "@opengeni/sdk";
|
|
3
3
|
import type { SessionCapabilitiesState } from "./use-session-capabilities";
|
|
4
|
+
import { sandboxAcceptsLiveIo } from "../lib/sandbox-liveness";
|
|
4
5
|
|
|
5
6
|
/** The one truthful machine indicator. Honest staleness beats
|
|
6
7
|
* fake liveness: a cold/asleep box reads "offline — as of <time>", never "live". */
|
|
@@ -53,7 +54,7 @@ export function formatAsOf(capturedAt: string, now: number): string {
|
|
|
53
54
|
* it; the whole point is a deterministic, testable projection).
|
|
54
55
|
*
|
|
55
56
|
* Precedence:
|
|
56
|
-
* 1. warm
|
|
57
|
+
* 1. warm lease → **live** (a holder-confirmed box always wins).
|
|
57
58
|
* 2. actively warming (negotiating, wake-on-edit, or a reconnecting/enrolling
|
|
58
59
|
* machine) → **waking**.
|
|
59
60
|
* 3. everything else (a cold/asleep box, a self-hosted machine that's offline,
|
|
@@ -64,8 +65,9 @@ export function deriveMachineChip(input: DeriveMachineChipInput): MachineChip {
|
|
|
64
65
|
const asOf = input.capturedAt ?? null;
|
|
65
66
|
const offlineLabel = asOf ? `Offline — as of ${formatAsOf(asOf, now)}` : "Offline";
|
|
66
67
|
|
|
67
|
-
// 1.
|
|
68
|
-
|
|
68
|
+
// 1. Only holder-confirmed warm is live. Draining is teardown-owned and may
|
|
69
|
+
// already have lost its provider instance, so it uses the capture-backed label.
|
|
70
|
+
if (sandboxAcceptsLiveIo(input.liveness)) {
|
|
69
71
|
return { state: "live", label: "Live", asOf: null };
|
|
70
72
|
}
|
|
71
73
|
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { useCallback, useRef, useState } from "react";
|
|
2
|
-
import type {
|
|
2
|
+
import type {
|
|
3
|
+
RemoveEnrollmentRequest,
|
|
4
|
+
RemoveEnrollmentResponse,
|
|
5
|
+
SwapActiveSandboxResponse,
|
|
6
|
+
} from "@opengeni/sdk";
|
|
3
7
|
import { useOpenGeni, type ClientOverride } from "../provider";
|
|
4
8
|
import { useMutationRunner, usePolledValue } from "./internal";
|
|
5
9
|
import type { MachinesResponse, MachineView, MetricSample } from "../types/machines";
|
|
@@ -46,7 +50,7 @@ export type MachinesClientLike = {
|
|
|
46
50
|
workspaceId: string,
|
|
47
51
|
sessionId: string,
|
|
48
52
|
request: { target: string },
|
|
49
|
-
) => Promise<
|
|
53
|
+
) => Promise<SwapActiveSandboxResponse>;
|
|
50
54
|
/**
|
|
51
55
|
* Host-supplied swap adapter (an escape hatch). When present it wins over the
|
|
52
56
|
* default `swapActiveSandbox` path. Session-scoped, like the swap it backs.
|
|
@@ -99,6 +103,24 @@ export type UseMachinesResult = {
|
|
|
99
103
|
|
|
100
104
|
const EMPTY: MachinesResponse = { activeSandboxId: null, activeEpoch: 0, machines: [] };
|
|
101
105
|
|
|
106
|
+
function swapFailureMessage(result: SwapActiveSandboxResponse): string {
|
|
107
|
+
if (result.reason?.trim()) return result.reason;
|
|
108
|
+
switch (result.code) {
|
|
109
|
+
case "recovery_in_progress":
|
|
110
|
+
return "The managed sandbox is still recovering. Try again shortly.";
|
|
111
|
+
case "recovery_degraded":
|
|
112
|
+
return "The managed sandbox recovery is degraded and needs attention.";
|
|
113
|
+
case "recovery_unrecoverable":
|
|
114
|
+
return "The managed sandbox could not be recovered.";
|
|
115
|
+
case "offline_enrollment":
|
|
116
|
+
return "The selected machine is offline.";
|
|
117
|
+
case "concurrent_swap":
|
|
118
|
+
return "The session route changed concurrently. Refresh and try again.";
|
|
119
|
+
default:
|
|
120
|
+
return "The session stayed on its current sandbox.";
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
102
124
|
/**
|
|
103
125
|
* The workspace Machines fleet: the selfhosted enrollments + the session's Modal
|
|
104
126
|
* box, each with latest metrics + state, plus the active-sandbox pointer. Polls
|
|
@@ -171,7 +193,15 @@ export function useMachines(options: UseMachinesOptions = {}): UseMachinesResult
|
|
|
171
193
|
const ownedIdentity = identityKey;
|
|
172
194
|
setAttachState({ identity: ownedIdentity, sandboxId });
|
|
173
195
|
const result = await run(async () => {
|
|
174
|
-
await runSwap();
|
|
196
|
+
const response = await runSwap();
|
|
197
|
+
if (
|
|
198
|
+
response &&
|
|
199
|
+
typeof response === "object" &&
|
|
200
|
+
"swapped" in response &&
|
|
201
|
+
response.swapped === false
|
|
202
|
+
) {
|
|
203
|
+
throw new Error(swapFailureMessage(response as SwapActiveSandboxResponse));
|
|
204
|
+
}
|
|
175
205
|
return true;
|
|
176
206
|
});
|
|
177
207
|
if (identityRef.current === ownedIdentity) {
|
package/src/hooks/use-packs.ts
CHANGED
|
@@ -1,8 +1,14 @@
|
|
|
1
1
|
import type {
|
|
2
2
|
CapabilityPack,
|
|
3
3
|
EnablePackRequest,
|
|
4
|
+
InstallPackRequest,
|
|
5
|
+
PackInstallationPreview,
|
|
4
6
|
PackInstallation,
|
|
7
|
+
PackUninstallPreview,
|
|
8
|
+
PreviewPackInstallationRequest,
|
|
5
9
|
RegisterCapabilityPackRequest,
|
|
10
|
+
UninstallPackRequest,
|
|
11
|
+
UninstallPackResult,
|
|
6
12
|
WorkspaceRegisteredPack,
|
|
7
13
|
} from "@opengeni/sdk";
|
|
8
14
|
import { useCallback } from "react";
|
|
@@ -28,7 +34,15 @@ export type UsePacksResult = {
|
|
|
28
34
|
refresh: () => Promise<void>;
|
|
29
35
|
/** Register (or replace) a workspace-scoped pack manifest. */
|
|
30
36
|
register: (manifest: RegisterCapabilityPackRequest) => Promise<WorkspaceRegisteredPack | null>;
|
|
37
|
+
/** @deprecated Use previewInstallation + install for component/Rig-aware Packs. */
|
|
31
38
|
enable: (packId: string, request?: EnablePackRequest) => Promise<PackInstallation | null>;
|
|
39
|
+
previewInstallation: (
|
|
40
|
+
packId: string,
|
|
41
|
+
request?: PreviewPackInstallationRequest,
|
|
42
|
+
) => Promise<PackInstallationPreview | null>;
|
|
43
|
+
install: (packId: string, request: InstallPackRequest) => Promise<PackInstallation | null>;
|
|
44
|
+
previewUninstall: (packId: string) => Promise<PackUninstallPreview | null>;
|
|
45
|
+
uninstall: (packId: string, request: UninstallPackRequest) => Promise<UninstallPackResult | null>;
|
|
32
46
|
/** Unregister a workspace-scoped pack (built-ins cannot be removed). */
|
|
33
47
|
remove: (packId: string) => Promise<boolean>;
|
|
34
48
|
mutating: boolean;
|
|
@@ -68,6 +82,39 @@ export function usePacks(options: UsePacksOptions = {}): UsePacksResult {
|
|
|
68
82
|
[client, workspaceId, run, refresh],
|
|
69
83
|
);
|
|
70
84
|
|
|
85
|
+
const previewInstallation = useCallback(
|
|
86
|
+
async (
|
|
87
|
+
packId: string,
|
|
88
|
+
request: PreviewPackInstallationRequest = {},
|
|
89
|
+
): Promise<PackInstallationPreview | null> =>
|
|
90
|
+
await run(() => client.previewPackInstallation(workspaceId, packId, request)),
|
|
91
|
+
[client, workspaceId, run],
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const install = useCallback(
|
|
95
|
+
async (packId: string, request: InstallPackRequest): Promise<PackInstallation | null> => {
|
|
96
|
+
const result = await run(() => client.installPack(workspaceId, packId, request));
|
|
97
|
+
if (result) await refresh();
|
|
98
|
+
return result;
|
|
99
|
+
},
|
|
100
|
+
[client, workspaceId, run, refresh],
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
const previewUninstall = useCallback(
|
|
104
|
+
async (packId: string): Promise<PackUninstallPreview | null> =>
|
|
105
|
+
await run(() => client.previewPackUninstall(workspaceId, packId)),
|
|
106
|
+
[client, workspaceId, run],
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
const uninstall = useCallback(
|
|
110
|
+
async (packId: string, request: UninstallPackRequest): Promise<UninstallPackResult | null> => {
|
|
111
|
+
const result = await run(() => client.uninstallPack(workspaceId, packId, request));
|
|
112
|
+
if (result) await refresh();
|
|
113
|
+
return result;
|
|
114
|
+
},
|
|
115
|
+
[client, workspaceId, run, refresh],
|
|
116
|
+
);
|
|
117
|
+
|
|
71
118
|
const remove = useCallback(
|
|
72
119
|
async (packId: string): Promise<boolean> => {
|
|
73
120
|
const result = await run(async () => {
|
|
@@ -98,6 +145,10 @@ export function usePacks(options: UsePacksOptions = {}): UsePacksResult {
|
|
|
98
145
|
refresh,
|
|
99
146
|
register,
|
|
100
147
|
enable,
|
|
148
|
+
previewInstallation,
|
|
149
|
+
install,
|
|
150
|
+
previewUninstall,
|
|
151
|
+
uninstall,
|
|
101
152
|
remove,
|
|
102
153
|
mutating,
|
|
103
154
|
mutationError,
|
|
@@ -7,11 +7,13 @@ import type {
|
|
|
7
7
|
GitFileStatusCode,
|
|
8
8
|
GitStatusResponse,
|
|
9
9
|
OpenGeniRequestOptions,
|
|
10
|
+
SessionCapabilities,
|
|
10
11
|
SessionEvent,
|
|
11
12
|
WorkspaceCaptureManifest,
|
|
12
13
|
} from "@opengeni/sdk";
|
|
13
14
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
14
15
|
import { type ClientOverride, useOpenGeni } from "../provider";
|
|
16
|
+
import { sandboxAcceptsLiveIo } from "../lib/sandbox-liveness";
|
|
15
17
|
|
|
16
18
|
/** The git-status overlay a file row may carry (tints modified files in the tree). */
|
|
17
19
|
export type FileTreeStatus = "added" | "modified" | "deleted" | "renamed" | "untracked";
|
|
@@ -87,12 +89,14 @@ export type UseSandboxFilesOptions = ClientOverride & {
|
|
|
87
89
|
active?: boolean | undefined;
|
|
88
90
|
/** Repository roots advertised by capabilities, relative to the workspace. */
|
|
89
91
|
repoPaths?: readonly string[] | undefined;
|
|
90
|
-
/** The lease liveness
|
|
92
|
+
/** The lease liveness. `null` means capability negotiation has not produced
|
|
93
|
+
* an authoritative lifecycle value yet and therefore blocks live I/O. Omitting
|
|
94
|
+
* the option preserves the legacy direct-hook behavior. The structured FileSystem
|
|
91
95
|
* capability is advertised even on a COLD box, so the mount-time list can race
|
|
92
96
|
* the box: it lists before the box is warm, gets an empty/errored result, and
|
|
93
97
|
* (with no `fs.changed` event) never re-lists. Passing liveness re-lists when
|
|
94
98
|
* the box first becomes warm, so the tree populates as soon as the box is up. */
|
|
95
|
-
liveness?:
|
|
99
|
+
liveness?: SessionCapabilities["liveness"] | null | undefined;
|
|
96
100
|
/** The latest turn-end workspace capture (from `useWorkspaceCapture`). The tree
|
|
97
101
|
* paints immediately from this durable index, then a warm box reconciles live
|
|
98
102
|
* in place. If that live read fails, the capture remains a truthful read-only
|
|
@@ -487,7 +491,13 @@ export function useSandboxFiles(
|
|
|
487
491
|
].join("\u0000");
|
|
488
492
|
const repoPaths = useMemo(() => repoPathsKey.split("\u0000"), [repoPathsKey]);
|
|
489
493
|
const capture = options.capture ?? null;
|
|
490
|
-
const isLive = options.liveness
|
|
494
|
+
const isLive = sandboxAcceptsLiveIo(options.liveness);
|
|
495
|
+
// `liveness` predates this hook option. Preserve legacy embedders that omit it;
|
|
496
|
+
// explicit `null` (negotiating) and lifecycle values require authoritative warm.
|
|
497
|
+
const acceptsEventReads = options.liveness === undefined || isLive;
|
|
498
|
+
const acceptsEventReadsRef = useRef(acceptsEventReads);
|
|
499
|
+
acceptsEventReadsRef.current = acceptsEventReads;
|
|
500
|
+
const hasLiveIoFence = options.liveness !== undefined;
|
|
491
501
|
const identityKey = `${workspaceId}\u0000${sessionId ?? ""}\u0000${rootPath}\u0000${repoPathsKey}`;
|
|
492
502
|
|
|
493
503
|
const [tree, setTree] = useState<FileTreeNode[]>([]);
|
|
@@ -512,6 +522,7 @@ export function useSandboxFiles(
|
|
|
512
522
|
const refreshGenerationRef = useRef(0);
|
|
513
523
|
const refreshAbortRef = useRef<AbortController | null>(null);
|
|
514
524
|
const identityAbortRef = useRef(new AbortController());
|
|
525
|
+
const eventReadAbortRef = useRef(new AbortController());
|
|
515
526
|
const identityGenerationRef = useRef(0);
|
|
516
527
|
const lastSeqRef = useRef(0);
|
|
517
528
|
const pendingParentsRef = useRef<Set<string>>(new Set());
|
|
@@ -846,20 +857,18 @@ export function useSandboxFiles(
|
|
|
846
857
|
// of a parent that isn't loaded (collapsed/unmounted) is a no-op — there's
|
|
847
858
|
// nothing visible to update, and it re-lists fresh when the user expands it.
|
|
848
859
|
const reconcilePath = useCallback(
|
|
849
|
-
async (path: string) => {
|
|
860
|
+
async (path: string, requestSignal?: AbortSignal) => {
|
|
850
861
|
if (!sessionId) return;
|
|
851
862
|
const identityGeneration = identityGenerationRef.current;
|
|
852
863
|
const identitySignal = identityAbortRef.current.signal;
|
|
864
|
+
const signal = requestSignal
|
|
865
|
+
? AbortSignal.any([identitySignal, requestSignal])
|
|
866
|
+
: identitySignal;
|
|
853
867
|
// Skip parents that aren't currently loaded/expanded in the tree (nothing
|
|
854
868
|
// visible to update; they re-list fresh on the next expand).
|
|
855
869
|
if (!parentIsLoaded(treeRef.current, path)) return;
|
|
856
870
|
try {
|
|
857
|
-
const listed = await client.fsList(
|
|
858
|
-
workspaceId,
|
|
859
|
-
sessionId,
|
|
860
|
-
{ path, depth: 1 },
|
|
861
|
-
{ signal: identitySignal },
|
|
862
|
-
);
|
|
871
|
+
const listed = await client.fsList(workspaceId, sessionId, { path, depth: 1 }, { signal });
|
|
863
872
|
if (identityGenerationRef.current !== identityGeneration) return;
|
|
864
873
|
const children = (listed.root.children ?? []).map((node) => fsNodeToTree(node));
|
|
865
874
|
if (path === "") setTree((prev) => applyStatus(mergeRootChildren(prev, children)));
|
|
@@ -1075,9 +1084,10 @@ export function useSandboxFiles(
|
|
|
1075
1084
|
|
|
1076
1085
|
// Initial paint + reset on identity change. Source selection:
|
|
1077
1086
|
// • any box WITH a capture → paint instantly from the durable capture index;
|
|
1078
|
-
// a warm
|
|
1087
|
+
// a holder-confirmed warm box then reconciles live in place.
|
|
1079
1088
|
// • if that live reconciliation fails, keep the capture visible and read-only.
|
|
1080
|
-
// •
|
|
1089
|
+
// • legacy callers that omit liveness and have no capture retain their live
|
|
1090
|
+
// fallback; an explicitly cold/draining lease never wakes from a passive mount.
|
|
1081
1091
|
// Key the seed on the capture's REVISION (a primitive), not the manifest object
|
|
1082
1092
|
// — a consumer passing a fresh object each render (or a new revision) must not
|
|
1083
1093
|
// spin the effect. The latest manifest is read from a ref at run time.
|
|
@@ -1092,6 +1102,8 @@ export function useSandboxFiles(
|
|
|
1092
1102
|
refreshAbortRef.current?.abort();
|
|
1093
1103
|
refreshAbortRef.current = null;
|
|
1094
1104
|
refreshGenerationRef.current += 1;
|
|
1105
|
+
eventReadAbortRef.current.abort();
|
|
1106
|
+
eventReadAbortRef.current = new AbortController();
|
|
1095
1107
|
const identityChanged = previousIdentityRef.current !== identityKey;
|
|
1096
1108
|
previousIdentityRef.current = identityKey;
|
|
1097
1109
|
if (identityChanged || !enabled) {
|
|
@@ -1135,53 +1147,69 @@ export function useSandboxFiles(
|
|
|
1135
1147
|
}
|
|
1136
1148
|
return;
|
|
1137
1149
|
}
|
|
1138
|
-
if (isLive || !currentCapture) {
|
|
1150
|
+
if (isLive || (!hasLiveIoFence && !currentCapture)) {
|
|
1139
1151
|
void refresh();
|
|
1140
1152
|
}
|
|
1141
1153
|
return () => {
|
|
1142
1154
|
refreshAbortRef.current?.abort();
|
|
1143
1155
|
refreshAbortRef.current = null;
|
|
1144
1156
|
refreshGenerationRef.current += 1;
|
|
1157
|
+
eventReadAbortRef.current.abort();
|
|
1145
1158
|
};
|
|
1146
|
-
}, [
|
|
1159
|
+
}, [
|
|
1160
|
+
enabled,
|
|
1161
|
+
active,
|
|
1162
|
+
isLive,
|
|
1163
|
+
hasLiveIoFence,
|
|
1164
|
+
captureRevision,
|
|
1165
|
+
identityKey,
|
|
1166
|
+
refresh,
|
|
1167
|
+
seedFromCapture,
|
|
1168
|
+
]);
|
|
1147
1169
|
|
|
1148
1170
|
// Re-pull JUST the git-status overlay and re-tint the existing tree in place —
|
|
1149
1171
|
// no fs re-list, no collapse. This is all a `git.changed` (commit/stage/checkout)
|
|
1150
1172
|
// needs: the tree SHAPE is unchanged, only the tints move.
|
|
1151
|
-
const refreshGitOverlay = useCallback(
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1173
|
+
const refreshGitOverlay = useCallback(
|
|
1174
|
+
async (requestSignal?: AbortSignal) => {
|
|
1175
|
+
if (!sessionId) return;
|
|
1176
|
+
const identityGeneration = identityGenerationRef.current;
|
|
1177
|
+
const identitySignal = identityAbortRef.current.signal;
|
|
1178
|
+
const signal = requestSignal
|
|
1179
|
+
? AbortSignal.any([identitySignal, requestSignal])
|
|
1180
|
+
: identitySignal;
|
|
1181
|
+
setGitLoading(true);
|
|
1182
|
+
try {
|
|
1183
|
+
const batch = await client.gitReadBatch(
|
|
1184
|
+
workspaceId,
|
|
1185
|
+
sessionId,
|
|
1186
|
+
{ requests: repoPaths.map((root) => ({ status: { path: root } })) },
|
|
1187
|
+
{ signal },
|
|
1188
|
+
);
|
|
1189
|
+
const statuses = batch.results.map((result, index) => ({
|
|
1190
|
+
root: repoPaths[index] ?? "",
|
|
1191
|
+
status: result.status,
|
|
1192
|
+
}));
|
|
1193
|
+
if (identityGenerationRef.current !== identityGeneration) return;
|
|
1194
|
+
const overlay = new Map<string, FileTreeStatus>();
|
|
1195
|
+
for (const { root, status } of statuses) {
|
|
1196
|
+
for (const file of status.files) {
|
|
1197
|
+
const code = file.worktree ?? file.index;
|
|
1198
|
+
const mapped = code ? GIT_STATUS_TO_TREE[code] : undefined;
|
|
1199
|
+
if (mapped) overlay.set(qualifyStatusPath(root, file.path), mapped);
|
|
1200
|
+
}
|
|
1174
1201
|
}
|
|
1202
|
+
statusRef.current = overlay;
|
|
1203
|
+
setGitSummary(summarizeStatuses(statuses));
|
|
1204
|
+
setTree((prev) => applyStatus(prev));
|
|
1205
|
+
} catch {
|
|
1206
|
+
/* a non-repo box has no overlay — leave the tree untinted */
|
|
1207
|
+
} finally {
|
|
1208
|
+
if (identityGenerationRef.current === identityGeneration) setGitLoading(false);
|
|
1175
1209
|
}
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
} catch {
|
|
1180
|
-
/* a non-repo box has no overlay — leave the tree untinted */
|
|
1181
|
-
} finally {
|
|
1182
|
-
if (identityGenerationRef.current === identityGeneration) setGitLoading(false);
|
|
1183
|
-
}
|
|
1184
|
-
}, [client, workspaceId, sessionId, repoPaths, applyStatus]);
|
|
1210
|
+
},
|
|
1211
|
+
[client, workspaceId, sessionId, repoPaths, applyStatus],
|
|
1212
|
+
);
|
|
1185
1213
|
|
|
1186
1214
|
// Auto-reconcile on fs/git change notifications — TARGETED, never a root
|
|
1187
1215
|
// collapse-reload, and de-duped against our OWN mutations.
|
|
@@ -1197,13 +1225,20 @@ export function useSandboxFiles(
|
|
|
1197
1225
|
|
|
1198
1226
|
useEffect(() => {
|
|
1199
1227
|
if (!enabled || !events) return;
|
|
1200
|
-
// An inactive tab must not replay the
|
|
1201
|
-
//
|
|
1202
|
-
//
|
|
1203
|
-
if (!active) {
|
|
1228
|
+
// An inactive tab or holderless draining/cold lease must not replay the
|
|
1229
|
+
// historical log into live provider reads. The capture already includes
|
|
1230
|
+
// those events; a later warm transition performs one authoritative refresh.
|
|
1231
|
+
if (!active || !acceptsEventReads) {
|
|
1204
1232
|
for (const event of events) {
|
|
1205
1233
|
if (event.sequence > lastSeqRef.current) lastSeqRef.current = event.sequence;
|
|
1206
1234
|
}
|
|
1235
|
+
if (debounceRef.current) {
|
|
1236
|
+
clearTimeout(debounceRef.current);
|
|
1237
|
+
debounceRef.current = null;
|
|
1238
|
+
}
|
|
1239
|
+
pendingParentsRef.current = new Set();
|
|
1240
|
+
pendingGitRef.current = false;
|
|
1241
|
+
pendingAgentToolRef.current = false;
|
|
1207
1242
|
return;
|
|
1208
1243
|
}
|
|
1209
1244
|
let sawNew = false;
|
|
@@ -1277,6 +1312,12 @@ export function useSandboxFiles(
|
|
|
1277
1312
|
debounceRef.current = setTimeout(() => {
|
|
1278
1313
|
debounceRef.current = null;
|
|
1279
1314
|
if (identityGenerationRef.current !== identityGeneration) return;
|
|
1315
|
+
if (!acceptsEventReadsRef.current) {
|
|
1316
|
+
pendingParentsRef.current = new Set();
|
|
1317
|
+
pendingGitRef.current = false;
|
|
1318
|
+
pendingAgentToolRef.current = false;
|
|
1319
|
+
return;
|
|
1320
|
+
}
|
|
1280
1321
|
if (sawCommandDelta) {
|
|
1281
1322
|
lastCommandRefreshAtRef.current = Date.now();
|
|
1282
1323
|
setContentRevision((revision) => revision + 1);
|
|
@@ -1289,18 +1330,19 @@ export function useSandboxFiles(
|
|
|
1289
1330
|
pendingAgentToolRef.current = false;
|
|
1290
1331
|
// Reconcile the changed directories and Git overlay concurrently. Both are
|
|
1291
1332
|
// independent remote reads; applyStatus makes their completion order safe.
|
|
1333
|
+
const eventReadSignal = eventReadAbortRef.current.signal;
|
|
1292
1334
|
void (async () => {
|
|
1293
1335
|
const paths = wantAgentTool ? loadedDirectoryPaths(treeRef.current) : [...parents];
|
|
1294
1336
|
await Promise.all([
|
|
1295
|
-
wantGit ? refreshGitOverlay() : Promise.resolve(),
|
|
1296
|
-
Promise.all(paths.map((path) => reconcilePath(path))),
|
|
1337
|
+
wantGit ? refreshGitOverlay(eventReadSignal) : Promise.resolve(),
|
|
1338
|
+
Promise.all(paths.map((path) => reconcilePath(path, eventReadSignal))),
|
|
1297
1339
|
]);
|
|
1298
1340
|
})();
|
|
1299
1341
|
}, delay);
|
|
1300
|
-
}, [enabled, active, events, reconcilePath, refreshGitOverlay]);
|
|
1342
|
+
}, [enabled, active, acceptsEventReads, events, reconcilePath, refreshGitOverlay]);
|
|
1301
1343
|
|
|
1302
1344
|
useEffect(() => {
|
|
1303
|
-
if (active) return;
|
|
1345
|
+
if (active && acceptsEventReads) return;
|
|
1304
1346
|
if (debounceRef.current) {
|
|
1305
1347
|
clearTimeout(debounceRef.current);
|
|
1306
1348
|
debounceRef.current = null;
|
|
@@ -1308,12 +1350,13 @@ export function useSandboxFiles(
|
|
|
1308
1350
|
pendingParentsRef.current = new Set();
|
|
1309
1351
|
pendingGitRef.current = false;
|
|
1310
1352
|
pendingAgentToolRef.current = false;
|
|
1311
|
-
}, [active]);
|
|
1353
|
+
}, [active, acceptsEventReads]);
|
|
1312
1354
|
|
|
1313
1355
|
useEffect(
|
|
1314
1356
|
() => () => {
|
|
1315
1357
|
refreshAbortRef.current?.abort();
|
|
1316
1358
|
identityAbortRef.current.abort();
|
|
1359
|
+
eventReadAbortRef.current.abort();
|
|
1317
1360
|
identityGenerationRef.current += 1;
|
|
1318
1361
|
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
1319
1362
|
},
|
|
@@ -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
|
() => () => {
|