@opengeni/react 0.46.1 → 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 +158 -14
- 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-HFO4ERGQ.js → chunk-724BW4TW.js} +586 -35
- 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-U3CSCMWA.js → chunk-FK6VG4LL.js} +9 -13
- package/dist/chunk-FK6VG4LL.js.map +1 -0
- package/dist/{chunk-SRFUT2ZU.js → chunk-FWS3KC3Z.js} +120 -64
- package/dist/chunk-FWS3KC3Z.js.map +1 -0
- 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-LGVMUIRV.js → chunk-JAGDUCZQ.js} +1053 -390
- package/dist/chunk-JAGDUCZQ.js.map +1 -0
- package/dist/{chunk-LWR4MXSS.js → chunk-JR3QFY6B.js} +420 -35
- 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-EB67J347.js → chunk-KZ73RL76.js} +111 -31
- package/dist/chunk-KZ73RL76.js.map +1 -0
- package/dist/chunk-PEI6RANI.js +3234 -0
- package/dist/chunk-PEI6RANI.js.map +1 -0
- package/dist/{chunk-3PB7MIT6.js → chunk-ROIRTM37.js} +220 -72
- package/dist/chunk-ROIRTM37.js.map +1 -0
- package/dist/chunk-VZTQ73XT.js +86 -0
- package/dist/chunk-VZTQ73XT.js.map +1 -0
- package/dist/{chunk-VCBFUSUZ.js → chunk-WSK7G5LE.js} +72 -4
- package/dist/chunk-WSK7G5LE.js.map +1 -0
- package/dist/{chunk-FGZUCXZF.js → chunk-WYA65Y3F.js} +16 -16
- package/dist/chunk-WYA65Y3F.js.map +1 -0
- package/dist/{chunk-6EDV6YFE.js → chunk-X4S56Y2H.js} +147 -35
- package/dist/chunk-X4S56Y2H.js.map +1 -0
- 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 +13 -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 +4 -2
- package/dist/components/composer.d.ts +29 -15
- 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 +5 -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 +5 -5
- package/dist/components/markdown.d.ts +1 -1
- package/dist/components/message-timeline.d.ts +22 -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 +5 -5
- package/dist/components/sandbox-terminal.d.ts +50 -4
- package/dist/components/sandbox-workspace.d.ts +30 -10
- 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 +3 -1
- package/dist/composer.d.ts +11 -11
- package/dist/composer.js +7 -7
- package/dist/hooks/internal.d.ts +7 -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 +3 -3
- 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 +11 -5
- 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 +10 -3
- 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 +25 -4
- package/dist/hooks/use-sandbox-git.d.ts +13 -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-transcription.d.ts +2 -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 +6 -4
- 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 -154
- package/dist/index.js +1718 -908
- 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/composer-responsive-context.d.ts +9 -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/use-portal-token-style.d.ts +14 -3
- 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-J4XJFKRW.js → queue-surface-implementation-2KVJTYXH.js} +117 -64
- 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 +15 -6
- package/dist/realtime.d.ts +4 -4
- package/dist/realtime.js +30 -33
- package/dist/realtime.js.map +1 -1
- package/dist/session-context.d.ts +19 -1
- package/dist/session-ui.d.ts +14 -14
- package/dist/session-ui.js +6 -5
- package/dist/session.d.ts +28 -28
- package/dist/session.js +5 -5
- package/dist/timeline/activity-rail.d.ts +6 -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 +6 -3
- package/dist/timeline/projection.d.ts +3 -3
- package/dist/timeline/registry.d.ts +10 -1
- 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 +42 -2
- 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/dist/voice-recording-store.d.ts +8 -2
- package/package.json +52 -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 +90 -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 +25 -2
- package/src/components/code-editor.tsx +90 -31
- package/src/components/command-palette.tsx +1 -1
- package/src/components/composer-transcription-control.tsx +51 -22
- package/src/components/composer.tsx +49 -23
- package/src/components/computer-viewer.tsx +1139 -0
- package/src/components/copy-button.tsx +1 -1
- package/src/components/desktop-viewer.tsx +103 -75
- package/src/components/file-browser.tsx +39 -17
- package/src/components/generated-video-player.tsx +89 -0
- package/src/components/human-input-form.tsx +2 -2
- package/src/components/interactive-workbench-viewers.ts +2 -0
- package/src/components/machine-card.tsx +1 -1
- package/src/components/machine-input-display.ts +3 -0
- package/src/components/machines/machine-detail.tsx +39 -15
- package/src/components/machines-dashboard.tsx +7 -6
- package/src/components/message-timeline.tsx +327 -37
- package/src/components/model-picker.tsx +2 -2
- package/src/components/model-policy-picker.tsx +16 -15
- package/src/components/queue-surface-implementation.tsx +135 -75
- package/src/components/queue-surface-state.tsx +2 -2
- package/src/components/sandbox-files.tsx +142 -62
- package/src/components/sandbox-terminal.tsx +119 -35
- package/src/components/sandbox-workspace.tsx +394 -228
- package/src/components/session-chrome.tsx +67 -27
- 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/tooltip.tsx +13 -16
- package/src/components/user-message-body.tsx +1 -1
- package/src/components/workbench-changes.tsx +192 -35
- package/src/components/workspace-dock.tsx +13 -5
- package/src/composer.ts +3 -0
- package/src/hooks/internal.ts +132 -38
- 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-codex-accounts.ts +4 -4
- 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-environments.ts +24 -5
- package/src/hooks/use-file-attachments.ts +70 -32
- package/src/hooks/use-goal.ts +17 -5
- package/src/hooks/use-machine-chip.ts +5 -3
- package/src/hooks/use-machines.ts +79 -3
- package/src/hooks/use-packs.ts +51 -0
- package/src/hooks/use-sandbox-files.ts +382 -88
- package/src/hooks/use-sandbox-git.ts +208 -46
- package/src/hooks/use-sandbox-terminal.ts +3 -2
- package/src/hooks/use-session-capabilities.ts +94 -70
- package/src/hooks/use-session-events.ts +18 -4
- package/src/hooks/use-terminal-stream.ts +102 -23
- package/src/hooks/use-transcription.ts +4 -14
- package/src/hooks/use-turn-queue.ts +17 -5
- package/src/hooks/use-video-artifact-playback.ts +28 -0
- package/src/hooks/use-voice-input.ts +163 -19
- package/src/hooks/use-windowed-sections.ts +9 -7
- package/src/hooks/use-workspace-edit.ts +2 -1
- package/src/index.ts +21 -6
- package/src/interaction.ts +71 -0
- package/src/lib/composer-responsive-context.ts +15 -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/lib/use-portal-token-style.ts +60 -15
- package/src/provider.tsx +52 -7
- package/src/realtime/dropdown-menu.tsx +15 -19
- package/src/realtime/realtime-control.tsx +16 -5
- package/src/session-context.ts +90 -0
- package/src/timeline/activity-rail.tsx +31 -6
- package/src/timeline/index.ts +4 -1
- package/src/timeline/parsers.ts +46 -20
- package/src/timeline/projection.ts +233 -13
- package/src/timeline/registry.ts +19 -0
- package/src/timeline/screenshot-lightbox.tsx +30 -9
- package/src/timeline/shared.tsx +59 -11
- package/src/timeline/tool-renderers.tsx +421 -24
- package/src/timeline/turn-summary.tsx +2 -2
- package/src/timeline/types.ts +58 -2
- package/src/voice-recording-store.ts +15 -0
- package/styles/compiled.css +5298 -0
- package/styles/compiled.d.ts +2 -0
- package/styles/effective-tokens.css +27 -0
- package/styles/index.css +18 -0
- package/styles/responsive.css +208 -0
- package/styles/responsive.d.ts +2 -0
- package/styles/tokens.css +50 -17
- package/dist/chunk-3PB7MIT6.js.map +0 -1
- package/dist/chunk-6EDV6YFE.js.map +0 -1
- package/dist/chunk-EB67J347.js.map +0 -1
- package/dist/chunk-EKZH6IDG.js.map +0 -1
- package/dist/chunk-FGZUCXZF.js.map +0 -1
- package/dist/chunk-HFO4ERGQ.js.map +0 -1
- package/dist/chunk-JB5725BI.js +0 -48
- package/dist/chunk-JB5725BI.js.map +0 -1
- package/dist/chunk-LGVMUIRV.js.map +0 -1
- package/dist/chunk-LWR4MXSS.js.map +0 -1
- package/dist/chunk-SRFUT2ZU.js.map +0 -1
- package/dist/chunk-U3CSCMWA.js.map +0 -1
- package/dist/chunk-VCBFUSUZ.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-J4XJFKRW.js.map +0 -1
|
@@ -5,12 +5,15 @@ import type {
|
|
|
5
5
|
FsWriteResponse,
|
|
6
6
|
GitChangedPayload,
|
|
7
7
|
GitFileStatusCode,
|
|
8
|
+
GitStatusResponse,
|
|
8
9
|
OpenGeniRequestOptions,
|
|
10
|
+
SessionCapabilities,
|
|
9
11
|
SessionEvent,
|
|
10
12
|
WorkspaceCaptureManifest,
|
|
11
13
|
} from "@opengeni/sdk";
|
|
12
|
-
import { useCallback, useEffect, useRef, useState } from "react";
|
|
13
|
-
import {
|
|
14
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
15
|
+
import { type ClientOverride, useOpenGeni } from "../provider";
|
|
16
|
+
import { sandboxAcceptsLiveIo } from "../lib/sandbox-liveness";
|
|
14
17
|
|
|
15
18
|
/** The git-status overlay a file row may carry (tints modified files in the tree). */
|
|
16
19
|
export type FileTreeStatus = "added" | "modified" | "deleted" | "renamed" | "untracked";
|
|
@@ -82,12 +85,18 @@ export type UseSandboxFilesOptions = ClientOverride & {
|
|
|
82
85
|
rootPath?: string | undefined;
|
|
83
86
|
/** Hold off the initial list (e.g. panel collapsed). Default true. */
|
|
84
87
|
enabled?: boolean | undefined;
|
|
85
|
-
/**
|
|
88
|
+
/** Pause live reads/invalidation while preserving the last rendered result. */
|
|
89
|
+
active?: boolean | undefined;
|
|
90
|
+
/** Repository roots advertised by capabilities, relative to the workspace. */
|
|
91
|
+
repoPaths?: readonly string[] | undefined;
|
|
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
|
|
86
95
|
* capability is advertised even on a COLD box, so the mount-time list can race
|
|
87
96
|
* the box: it lists before the box is warm, gets an empty/errored result, and
|
|
88
97
|
* (with no `fs.changed` event) never re-lists. Passing liveness re-lists when
|
|
89
98
|
* the box first becomes warm, so the tree populates as soon as the box is up. */
|
|
90
|
-
liveness?:
|
|
99
|
+
liveness?: SessionCapabilities["liveness"] | null | undefined;
|
|
91
100
|
/** The latest turn-end workspace capture (from `useWorkspaceCapture`). The tree
|
|
92
101
|
* paints immediately from this durable index, then a warm box reconciles live
|
|
93
102
|
* in place. If that live read fails, the capture remains a truthful read-only
|
|
@@ -127,15 +136,58 @@ export type UseSandboxFilesResult = {
|
|
|
127
136
|
moveEntry: (path: string, newPath: string, opts?: { overwrite?: boolean }) => Promise<void>;
|
|
128
137
|
/** Re-list the whole tree from the root. */
|
|
129
138
|
refresh: () => Promise<void>;
|
|
139
|
+
/** Monotonic invalidation token for open file previews. Advances when an
|
|
140
|
+
* external filesystem change or completed agent tool may have changed bytes. */
|
|
141
|
+
contentRevision?: number | undefined;
|
|
130
142
|
/** Which source the tree is currently served from: the live box, the turn-end
|
|
131
143
|
* capture (cold/offline), or neither yet. M5's source badge reads this. */
|
|
132
144
|
source: "live" | "capture" | null;
|
|
133
145
|
/** When the served capture was taken (ISO), when `source === "capture"`. */
|
|
134
146
|
capturedAt: string | null;
|
|
147
|
+
/** Lightweight repository context loaded by the same status request that tints files. */
|
|
148
|
+
gitSummary?: SandboxFilesGitSummary | null | undefined;
|
|
149
|
+
/** True only while the initial repository-status overlay is still loading. */
|
|
150
|
+
gitLoading?: boolean | undefined;
|
|
135
151
|
loading: boolean;
|
|
136
152
|
error: Error | null;
|
|
137
153
|
};
|
|
138
154
|
|
|
155
|
+
export type SandboxFilesGitSummary = {
|
|
156
|
+
branch: string | null;
|
|
157
|
+
isRepo: boolean;
|
|
158
|
+
repoCount: number;
|
|
159
|
+
ahead: number;
|
|
160
|
+
behind: number;
|
|
161
|
+
dirtyCount: number;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
function normalizeRoot(root: string): string {
|
|
165
|
+
const normalized = root
|
|
166
|
+
.replaceAll("\\", "/")
|
|
167
|
+
.replace(/^\.\/+/, "")
|
|
168
|
+
.replace(/\/+$/, "");
|
|
169
|
+
return normalized === "." ? "" : normalized;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function qualifyStatusPath(root: string, path: string): string {
|
|
173
|
+
const normalized = normalizeRoot(root);
|
|
174
|
+
return normalized ? `${normalized}/${path}` : path;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function summarizeStatuses(
|
|
178
|
+
statuses: Array<{ root: string; status: GitStatusResponse }>,
|
|
179
|
+
): SandboxFilesGitSummary {
|
|
180
|
+
const repositories = statuses.filter(({ status }) => status.isRepo);
|
|
181
|
+
return {
|
|
182
|
+
branch: repositories.length === 1 ? (repositories[0]?.status.head ?? null) : null,
|
|
183
|
+
isRepo: repositories.length > 0,
|
|
184
|
+
repoCount: repositories.length,
|
|
185
|
+
ahead: repositories.reduce((sum, { status }) => sum + status.ahead, 0),
|
|
186
|
+
behind: repositories.reduce((sum, { status }) => sum + status.behind, 0),
|
|
187
|
+
dirtyCount: repositories.reduce((sum, { status }) => sum + status.files.length, 0),
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
139
191
|
/** The workspace-relative parent directory of a POSIX path ("" for a root entry). */
|
|
140
192
|
function parentOf(path: string): string {
|
|
141
193
|
const i = path.lastIndexOf("/");
|
|
@@ -209,7 +261,10 @@ function replaceChildren(
|
|
|
209
261
|
return { ...node, children };
|
|
210
262
|
}
|
|
211
263
|
if (node.kind === "dir" && node.children && targetPath.startsWith(`${node.path}/`)) {
|
|
212
|
-
return {
|
|
264
|
+
return {
|
|
265
|
+
...node,
|
|
266
|
+
children: replaceChildren(node.children, targetPath, children),
|
|
267
|
+
};
|
|
213
268
|
}
|
|
214
269
|
return node;
|
|
215
270
|
});
|
|
@@ -227,6 +282,42 @@ function findNodeByPath(nodes: FileTreeNode[], path: string): FileTreeNode | und
|
|
|
227
282
|
return undefined;
|
|
228
283
|
}
|
|
229
284
|
|
|
285
|
+
/** Root plus every directory whose children are currently loaded. Re-listing
|
|
286
|
+
* this small visible frontier refreshes agent shell edits without expanding or
|
|
287
|
+
* traversing hidden directories. */
|
|
288
|
+
function loadedDirectoryPaths(nodes: FileTreeNode[]): string[] {
|
|
289
|
+
const paths = [""];
|
|
290
|
+
const walk = (list: FileTreeNode[]) => {
|
|
291
|
+
for (const node of list) {
|
|
292
|
+
if (node.kind !== "dir" || node.children === undefined) continue;
|
|
293
|
+
paths.push(node.path);
|
|
294
|
+
walk(node.children);
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
walk(nodes);
|
|
298
|
+
return paths;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/** Repository paths are capability-advertised and therefore safe to use as a
|
|
302
|
+
* bounded initial lazy-tree frontier. Listing their ancestors concurrently with
|
|
303
|
+
* the root avoids a root → repositories → repo remote waterfall. */
|
|
304
|
+
function initialDirectoryFrontier(rootPath: string, repoPaths: readonly string[]): string[] {
|
|
305
|
+
const rootParts = normalizeRoot(rootPath).split("/").filter(Boolean);
|
|
306
|
+
const paths = new Set<string>();
|
|
307
|
+
for (const repoPath of repoPaths) {
|
|
308
|
+
const parts = normalizeRoot(repoPath).split("/").filter(Boolean);
|
|
309
|
+
if (rootParts.some((part, index) => parts[index] !== part)) continue;
|
|
310
|
+
for (let depth = rootParts.length + 1; depth <= parts.length; depth += 1) {
|
|
311
|
+
paths.add(parts.slice(0, depth).join("/"));
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
// A workspace with hundreds of repositories should remain lazy. This still
|
|
315
|
+
// preloads the shared repository container and the first visible roots.
|
|
316
|
+
return [...paths]
|
|
317
|
+
.sort((a, b) => a.split("/").length - b.split("/").length || a.localeCompare(b))
|
|
318
|
+
.slice(0, 8);
|
|
319
|
+
}
|
|
320
|
+
|
|
230
321
|
// ── In-place (optimistic) tree mutations ────────────────────────────────────
|
|
231
322
|
// The Pierre mutation-handle analogue. These splice a single node in/out/across
|
|
232
323
|
// the immutable tree, PRESERVING every other node's `children` (and therefore the
|
|
@@ -279,7 +370,10 @@ function removeNode(nodes: FileTreeNode[], path: string): FileTreeNode[] {
|
|
|
279
370
|
return nodes.map((node) => {
|
|
280
371
|
if (node.path === parent) {
|
|
281
372
|
if (node.kind !== "dir" || node.children === undefined) return node;
|
|
282
|
-
return {
|
|
373
|
+
return {
|
|
374
|
+
...node,
|
|
375
|
+
children: node.children.filter((n) => n.path !== path),
|
|
376
|
+
};
|
|
283
377
|
}
|
|
284
378
|
if (node.kind === "dir" && node.children && parent.startsWith(`${node.path}/`)) {
|
|
285
379
|
return { ...node, children: removeNode(node.children, path) };
|
|
@@ -390,10 +484,21 @@ export function useSandboxFiles(
|
|
|
390
484
|
): UseSandboxFilesResult {
|
|
391
485
|
const { client, workspaceId } = useOpenGeni(options);
|
|
392
486
|
const enabled = (options.enabled ?? true) && Boolean(sessionId);
|
|
487
|
+
const active = options.active ?? true;
|
|
393
488
|
const rootPath = options.rootPath ?? "";
|
|
489
|
+
const repoPathsKey = [
|
|
490
|
+
...new Set((options.repoPaths?.length ? options.repoPaths : [rootPath]).map(normalizeRoot)),
|
|
491
|
+
].join("\u0000");
|
|
492
|
+
const repoPaths = useMemo(() => repoPathsKey.split("\u0000"), [repoPathsKey]);
|
|
394
493
|
const capture = options.capture ?? null;
|
|
395
|
-
const isLive = options.liveness
|
|
396
|
-
|
|
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;
|
|
501
|
+
const identityKey = `${workspaceId}\u0000${sessionId ?? ""}\u0000${rootPath}\u0000${repoPathsKey}`;
|
|
397
502
|
|
|
398
503
|
const [tree, setTree] = useState<FileTreeNode[]>([]);
|
|
399
504
|
// A ref mirror of the current tree — lets the optimistic path snapshot the
|
|
@@ -404,6 +509,9 @@ export function useSandboxFiles(
|
|
|
404
509
|
const [loading, setLoading] = useState(false);
|
|
405
510
|
const [expandingPaths, setExpandingPaths] = useState<Set<string>>(new Set());
|
|
406
511
|
const [error, setError] = useState<Error | null>(null);
|
|
512
|
+
const [gitSummary, setGitSummary] = useState<SandboxFilesGitSummary | null>(null);
|
|
513
|
+
const [gitLoading, setGitLoading] = useState(false);
|
|
514
|
+
const [contentRevision, setContentRevision] = useState(0);
|
|
407
515
|
const [stateIdentity, setStateIdentity] = useState(identityKey);
|
|
408
516
|
// Which source the tree currently reflects — "capture" (cold paint) or "live".
|
|
409
517
|
const [source, setSource] = useState<"live" | "capture" | null>(null);
|
|
@@ -414,11 +522,14 @@ export function useSandboxFiles(
|
|
|
414
522
|
const refreshGenerationRef = useRef(0);
|
|
415
523
|
const refreshAbortRef = useRef<AbortController | null>(null);
|
|
416
524
|
const identityAbortRef = useRef(new AbortController());
|
|
525
|
+
const eventReadAbortRef = useRef(new AbortController());
|
|
417
526
|
const identityGenerationRef = useRef(0);
|
|
418
527
|
const lastSeqRef = useRef(0);
|
|
419
528
|
const pendingParentsRef = useRef<Set<string>>(new Set());
|
|
420
529
|
const pendingGitRef = useRef(false);
|
|
530
|
+
const pendingAgentToolRef = useRef(false);
|
|
421
531
|
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
532
|
+
const lastCommandRefreshAtRef = useRef(0);
|
|
422
533
|
|
|
423
534
|
// ── Self-emitted fs.changed de-dupe ───────────────────────────────────────
|
|
424
535
|
// Every one of OUR mutations emits an `fs.changed` event back on the live log
|
|
@@ -470,55 +581,97 @@ export function useSandboxFiles(
|
|
|
470
581
|
refreshAbortRef.current = refreshAbort;
|
|
471
582
|
const generation = (refreshGenerationRef.current += 1);
|
|
472
583
|
setLoading(true);
|
|
584
|
+
setGitLoading(true);
|
|
473
585
|
setError(null);
|
|
474
586
|
try {
|
|
475
|
-
//
|
|
476
|
-
//
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
587
|
+
// Start the optional Git tint and the actual directory listing together.
|
|
588
|
+
// A remote status read must never block the first useful file-tree paint.
|
|
589
|
+
const frontierPaths = initialDirectoryFrontier(rootPath, repoPaths);
|
|
590
|
+
const listRequests = [rootPath, ...frontierPaths].map((path) => ({
|
|
591
|
+
path,
|
|
592
|
+
depth: 1,
|
|
593
|
+
}));
|
|
594
|
+
const statusPromise =
|
|
595
|
+
repoPaths.length > 0
|
|
596
|
+
? client
|
|
597
|
+
.gitReadBatch(
|
|
598
|
+
workspaceId,
|
|
599
|
+
sessionId,
|
|
600
|
+
{
|
|
601
|
+
requests: repoPaths.map((root) => ({
|
|
602
|
+
status: { path: root },
|
|
603
|
+
})),
|
|
604
|
+
},
|
|
605
|
+
{ signal: refreshAbort.signal },
|
|
606
|
+
)
|
|
607
|
+
.then((batch) =>
|
|
608
|
+
batch.results.map((result, index) => ({
|
|
609
|
+
root: repoPaths[index] ?? "",
|
|
610
|
+
status: result.status,
|
|
611
|
+
})),
|
|
612
|
+
)
|
|
613
|
+
.catch(() => null)
|
|
614
|
+
: Promise.resolve([]);
|
|
615
|
+
// Root plus the bounded repository frontier share one API request, one
|
|
616
|
+
// direct holder, and one provider attach. The old per-directory fan-out
|
|
617
|
+
// paid that fixed control-plane cost up to nine times on every Files open.
|
|
618
|
+
const listedBatch = await client.fsListBatch(
|
|
497
619
|
workspaceId,
|
|
498
620
|
sessionId,
|
|
499
|
-
{
|
|
621
|
+
{ requests: listRequests },
|
|
500
622
|
{ signal: refreshAbort.signal },
|
|
501
623
|
);
|
|
502
624
|
if (refreshGenerationRef.current !== generation) return;
|
|
625
|
+
const listed = listedBatch.results[0];
|
|
626
|
+
if (!listed) throw new Error("Workspace list batch did not return the root result.");
|
|
503
627
|
const children = (listed.root.children ?? []).map((node) => fsNodeToTree(node));
|
|
504
|
-
//
|
|
505
|
-
//
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
628
|
+
// Paint the root and already-returned frontier in one commit so preloaded
|
|
629
|
+
// repository folders never flicker through a collapsed intermediate tree.
|
|
630
|
+
setTree((prev) => {
|
|
631
|
+
let hydrated = prev.length === 0 ? children : mergeRootChildren(prev, children);
|
|
632
|
+
for (const [index, path] of frontierPaths.entries()) {
|
|
633
|
+
const prefetched = listedBatch.results[index + 1];
|
|
634
|
+
if (!prefetched) continue;
|
|
635
|
+
hydrated = replaceChildren(
|
|
636
|
+
hydrated,
|
|
637
|
+
path,
|
|
638
|
+
(prefetched.root.children ?? []).map((node) => fsNodeToTree(node)),
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
return applyStatus(hydrated);
|
|
642
|
+
});
|
|
512
643
|
sourceRef.current = "live";
|
|
513
644
|
setSource("live");
|
|
645
|
+
setLoading(false);
|
|
646
|
+
|
|
647
|
+
const statuses = await statusPromise;
|
|
648
|
+
if (refreshGenerationRef.current !== generation) return;
|
|
649
|
+
if (statuses) {
|
|
650
|
+
const overlay = new Map<string, FileTreeStatus>();
|
|
651
|
+
for (const { root, status } of statuses) {
|
|
652
|
+
for (const file of status.files) {
|
|
653
|
+
const code = file.worktree ?? file.index;
|
|
654
|
+
const mapped = code ? GIT_STATUS_TO_TREE[code] : undefined;
|
|
655
|
+
if (mapped) overlay.set(qualifyStatusPath(root, file.path), mapped);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
statusRef.current = overlay;
|
|
659
|
+
setGitSummary(summarizeStatuses(statuses));
|
|
660
|
+
} else {
|
|
661
|
+
statusRef.current = new Map();
|
|
662
|
+
setGitSummary(null);
|
|
663
|
+
}
|
|
664
|
+
setGitLoading(false);
|
|
665
|
+
setTree((prev) => applyStatus(prev));
|
|
514
666
|
} catch (cause) {
|
|
515
667
|
if (refreshGenerationRef.current !== generation) return;
|
|
516
668
|
setError(cause instanceof Error ? cause : new Error(String(cause)));
|
|
517
669
|
} finally {
|
|
518
670
|
if (refreshGenerationRef.current === generation) setLoading(false);
|
|
671
|
+
if (refreshGenerationRef.current === generation) setGitLoading(false);
|
|
519
672
|
if (refreshAbortRef.current === refreshAbort) refreshAbortRef.current = null;
|
|
520
673
|
}
|
|
521
|
-
}, [client, workspaceId, sessionId, rootPath, applyStatus]);
|
|
674
|
+
}, [client, workspaceId, sessionId, rootPath, repoPaths, applyStatus]);
|
|
522
675
|
|
|
523
676
|
// Seed (or re-seed) the tree from a turn-end capture — the COLD/offline paint,
|
|
524
677
|
// zero Channel-A calls. The tree index is workspace-relative (`treeIndex`); the
|
|
@@ -543,6 +696,15 @@ export function useSandboxFiles(
|
|
|
543
696
|
);
|
|
544
697
|
sourceRef.current = "capture";
|
|
545
698
|
setSource("capture");
|
|
699
|
+
setGitSummary({
|
|
700
|
+
branch: manifest.repos.length === 1 ? (manifest.repos[0]?.head ?? null) : null,
|
|
701
|
+
isRepo: manifest.repos.length > 0,
|
|
702
|
+
repoCount: manifest.repos.length,
|
|
703
|
+
ahead: manifest.repos.reduce((sum, repo) => sum + repo.ahead, 0),
|
|
704
|
+
behind: manifest.repos.reduce((sum, repo) => sum + repo.behind, 0),
|
|
705
|
+
dirtyCount: manifest.files.length,
|
|
706
|
+
});
|
|
707
|
+
setGitLoading(false);
|
|
546
708
|
setError(null);
|
|
547
709
|
},
|
|
548
710
|
[applyStatus],
|
|
@@ -695,20 +857,18 @@ export function useSandboxFiles(
|
|
|
695
857
|
// of a parent that isn't loaded (collapsed/unmounted) is a no-op — there's
|
|
696
858
|
// nothing visible to update, and it re-lists fresh when the user expands it.
|
|
697
859
|
const reconcilePath = useCallback(
|
|
698
|
-
async (path: string) => {
|
|
860
|
+
async (path: string, requestSignal?: AbortSignal) => {
|
|
699
861
|
if (!sessionId) return;
|
|
700
862
|
const identityGeneration = identityGenerationRef.current;
|
|
701
863
|
const identitySignal = identityAbortRef.current.signal;
|
|
864
|
+
const signal = requestSignal
|
|
865
|
+
? AbortSignal.any([identitySignal, requestSignal])
|
|
866
|
+
: identitySignal;
|
|
702
867
|
// Skip parents that aren't currently loaded/expanded in the tree (nothing
|
|
703
868
|
// visible to update; they re-list fresh on the next expand).
|
|
704
869
|
if (!parentIsLoaded(treeRef.current, path)) return;
|
|
705
870
|
try {
|
|
706
|
-
const listed = await client.fsList(
|
|
707
|
-
workspaceId,
|
|
708
|
-
sessionId,
|
|
709
|
-
{ path, depth: 1 },
|
|
710
|
-
{ signal: identitySignal },
|
|
711
|
-
);
|
|
871
|
+
const listed = await client.fsList(workspaceId, sessionId, { path, depth: 1 }, { signal });
|
|
712
872
|
if (identityGenerationRef.current !== identityGeneration) return;
|
|
713
873
|
const children = (listed.root.children ?? []).map((node) => fsNodeToTree(node));
|
|
714
874
|
if (path === "") setTree((prev) => applyStatus(mergeRootChildren(prev, children)));
|
|
@@ -795,7 +955,12 @@ export function useSandboxFiles(
|
|
|
795
955
|
// exist in a loaded parent (an editor SAVE to an existing file mutates no
|
|
796
956
|
// tree shape — just content — so it needs no optimistic node, no reconcile).
|
|
797
957
|
const exists = Boolean(findNodeByPath(tree, path));
|
|
798
|
-
const node: FileTreeNode = {
|
|
958
|
+
const node: FileTreeNode = {
|
|
959
|
+
path,
|
|
960
|
+
name: leafOf(path),
|
|
961
|
+
kind: "file",
|
|
962
|
+
size: content.length,
|
|
963
|
+
};
|
|
799
964
|
return await runOptimistic(
|
|
800
965
|
"write",
|
|
801
966
|
(nodes) => (exists ? nodes : insertNode(nodes, parent, node)),
|
|
@@ -835,11 +1000,21 @@ export function useSandboxFiles(
|
|
|
835
1000
|
async (path: string): Promise<void> => {
|
|
836
1001
|
if (!sessionId) throw new Error("no session");
|
|
837
1002
|
const parent = parentOf(path);
|
|
838
|
-
const node: FileTreeNode = {
|
|
1003
|
+
const node: FileTreeNode = {
|
|
1004
|
+
path,
|
|
1005
|
+
name: leafOf(path),
|
|
1006
|
+
kind: "file",
|
|
1007
|
+
size: 0,
|
|
1008
|
+
};
|
|
839
1009
|
await runOptimistic(
|
|
840
1010
|
"create file",
|
|
841
1011
|
(nodes) => insertNode(nodes, parent, node),
|
|
842
|
-
() =>
|
|
1012
|
+
() =>
|
|
1013
|
+
client.fsWrite(workspaceId, sessionId, {
|
|
1014
|
+
path,
|
|
1015
|
+
content: "",
|
|
1016
|
+
overwrite: false,
|
|
1017
|
+
}),
|
|
843
1018
|
[parent],
|
|
844
1019
|
);
|
|
845
1020
|
},
|
|
@@ -852,7 +1027,12 @@ export function useSandboxFiles(
|
|
|
852
1027
|
const parent = parentOf(path);
|
|
853
1028
|
// A freshly-created dir is empty + expanded: children:[] (not undefined, so
|
|
854
1029
|
// it doesn't show the lazy-expand marker over a dir we KNOW is empty).
|
|
855
|
-
const node: FileTreeNode = {
|
|
1030
|
+
const node: FileTreeNode = {
|
|
1031
|
+
path,
|
|
1032
|
+
name: leafOf(path),
|
|
1033
|
+
kind: "dir",
|
|
1034
|
+
children: [],
|
|
1035
|
+
};
|
|
856
1036
|
await runOptimistic(
|
|
857
1037
|
"create folder",
|
|
858
1038
|
(nodes) => insertNode(nodes, parent, node),
|
|
@@ -904,9 +1084,10 @@ export function useSandboxFiles(
|
|
|
904
1084
|
|
|
905
1085
|
// Initial paint + reset on identity change. Source selection:
|
|
906
1086
|
// • any box WITH a capture → paint instantly from the durable capture index;
|
|
907
|
-
// a warm
|
|
1087
|
+
// a holder-confirmed warm box then reconciles live in place.
|
|
908
1088
|
// • if that live reconciliation fails, keep the capture visible and read-only.
|
|
909
|
-
// •
|
|
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.
|
|
910
1091
|
// Key the seed on the capture's REVISION (a primitive), not the manifest object
|
|
911
1092
|
// — a consumer passing a fresh object each render (or a new revision) must not
|
|
912
1093
|
// spin the effect. The latest manifest is read from a ref at run time.
|
|
@@ -921,6 +1102,8 @@ export function useSandboxFiles(
|
|
|
921
1102
|
refreshAbortRef.current?.abort();
|
|
922
1103
|
refreshAbortRef.current = null;
|
|
923
1104
|
refreshGenerationRef.current += 1;
|
|
1105
|
+
eventReadAbortRef.current.abort();
|
|
1106
|
+
eventReadAbortRef.current = new AbortController();
|
|
924
1107
|
const identityChanged = previousIdentityRef.current !== identityKey;
|
|
925
1108
|
previousIdentityRef.current = identityKey;
|
|
926
1109
|
if (identityChanged || !enabled) {
|
|
@@ -930,6 +1113,8 @@ export function useSandboxFiles(
|
|
|
930
1113
|
lastSeqRef.current = 0;
|
|
931
1114
|
pendingParentsRef.current = new Set();
|
|
932
1115
|
pendingGitRef.current = false;
|
|
1116
|
+
pendingAgentToolRef.current = false;
|
|
1117
|
+
lastCommandRefreshAtRef.current = 0;
|
|
933
1118
|
if (debounceRef.current) {
|
|
934
1119
|
clearTimeout(debounceRef.current);
|
|
935
1120
|
debounceRef.current = null;
|
|
@@ -944,6 +1129,9 @@ export function useSandboxFiles(
|
|
|
944
1129
|
setSource(null);
|
|
945
1130
|
setLoading(false);
|
|
946
1131
|
setError(null);
|
|
1132
|
+
setGitSummary(null);
|
|
1133
|
+
setGitLoading(false);
|
|
1134
|
+
setContentRevision(0);
|
|
947
1135
|
}
|
|
948
1136
|
if (!enabled) {
|
|
949
1137
|
return;
|
|
@@ -952,43 +1140,76 @@ export function useSandboxFiles(
|
|
|
952
1140
|
if (currentCapture && (identityChanged || !isLive || sourceRef.current !== "live")) {
|
|
953
1141
|
seedFromCapture(currentCapture);
|
|
954
1142
|
}
|
|
955
|
-
if (
|
|
1143
|
+
if (!active) {
|
|
1144
|
+
if (!active) {
|
|
1145
|
+
setLoading(false);
|
|
1146
|
+
setGitLoading(false);
|
|
1147
|
+
}
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
if (isLive || (!hasLiveIoFence && !currentCapture)) {
|
|
956
1151
|
void refresh();
|
|
957
1152
|
}
|
|
958
1153
|
return () => {
|
|
959
1154
|
refreshAbortRef.current?.abort();
|
|
960
1155
|
refreshAbortRef.current = null;
|
|
961
1156
|
refreshGenerationRef.current += 1;
|
|
1157
|
+
eventReadAbortRef.current.abort();
|
|
962
1158
|
};
|
|
963
|
-
}, [
|
|
1159
|
+
}, [
|
|
1160
|
+
enabled,
|
|
1161
|
+
active,
|
|
1162
|
+
isLive,
|
|
1163
|
+
hasLiveIoFence,
|
|
1164
|
+
captureRevision,
|
|
1165
|
+
identityKey,
|
|
1166
|
+
refresh,
|
|
1167
|
+
seedFromCapture,
|
|
1168
|
+
]);
|
|
964
1169
|
|
|
965
1170
|
// Re-pull JUST the git-status overlay and re-tint the existing tree in place —
|
|
966
1171
|
// no fs re-list, no collapse. This is all a `git.changed` (commit/stage/checkout)
|
|
967
1172
|
// needs: the tree SHAPE is unchanged, only the tints move.
|
|
968
|
-
const refreshGitOverlay = useCallback(
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
const
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
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
|
+
}
|
|
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);
|
|
985
1209
|
}
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
/* a non-repo box has no overlay — leave the tree untinted */
|
|
990
|
-
}
|
|
991
|
-
}, [client, workspaceId, sessionId, rootPath, applyStatus]);
|
|
1210
|
+
},
|
|
1211
|
+
[client, workspaceId, sessionId, repoPaths, applyStatus],
|
|
1212
|
+
);
|
|
992
1213
|
|
|
993
1214
|
// Auto-reconcile on fs/git change notifications — TARGETED, never a root
|
|
994
1215
|
// collapse-reload, and de-duped against our OWN mutations.
|
|
@@ -1004,7 +1225,25 @@ export function useSandboxFiles(
|
|
|
1004
1225
|
|
|
1005
1226
|
useEffect(() => {
|
|
1006
1227
|
if (!enabled || !events) return;
|
|
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) {
|
|
1232
|
+
for (const event of events) {
|
|
1233
|
+
if (event.sequence > lastSeqRef.current) lastSeqRef.current = event.sequence;
|
|
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;
|
|
1242
|
+
return;
|
|
1243
|
+
}
|
|
1007
1244
|
let sawNew = false;
|
|
1245
|
+
let contentChanged = false;
|
|
1246
|
+
let sawCommandDelta = false;
|
|
1008
1247
|
for (const event of events) {
|
|
1009
1248
|
if (event.sequence <= lastSeqRef.current) continue;
|
|
1010
1249
|
if (event.type === "fs.changed") {
|
|
@@ -1015,6 +1254,7 @@ export function useSandboxFiles(
|
|
|
1015
1254
|
if (payload.source === "write") continue;
|
|
1016
1255
|
if (typeof payload.revision === "number" && ownRevisionsRef.current.has(payload.revision))
|
|
1017
1256
|
continue;
|
|
1257
|
+
contentChanged = true;
|
|
1018
1258
|
for (const change of payload.changes ?? []) {
|
|
1019
1259
|
pendingParentsRef.current.add(parentOf(change.path));
|
|
1020
1260
|
if (change.oldPath) pendingParentsRef.current.add(parentOf(change.oldPath));
|
|
@@ -1033,39 +1273,90 @@ export function useSandboxFiles(
|
|
|
1033
1273
|
)
|
|
1034
1274
|
continue;
|
|
1035
1275
|
pendingGitRef.current = true;
|
|
1276
|
+
} else if (event.type === "agent.toolCall.output") {
|
|
1277
|
+
// Agent shell/apply-patch writes do not necessarily pass through the
|
|
1278
|
+
// structured FS service, so they may have no fs.changed notification.
|
|
1279
|
+
// A completed tool is a bounded invalidation point: refresh only the
|
|
1280
|
+
// currently-loaded directory frontier, never poll and never traverse a
|
|
1281
|
+
// collapsed subtree.
|
|
1282
|
+
sawNew = true;
|
|
1283
|
+
contentChanged = true;
|
|
1284
|
+
pendingAgentToolRef.current = true;
|
|
1285
|
+
// Shell/apply-patch writes bypass the structured Git service too, so no
|
|
1286
|
+
// git.changed event is guaranteed. Refresh the summary/tints alongside
|
|
1287
|
+
// the visible tree frontier; both remote reads can run concurrently.
|
|
1288
|
+
pendingGitRef.current = true;
|
|
1289
|
+
} else if (event.type === "sandbox.command.output.delta") {
|
|
1290
|
+
// Ordinary shell writes can bypass Channel-A and therefore emit no
|
|
1291
|
+
// fs.changed/git.changed event until the command completes. Reconcile
|
|
1292
|
+
// the visible frontier at most once per second while output streams so
|
|
1293
|
+
// Files and Changes stay useful without turning the dock into polling.
|
|
1294
|
+
sawNew = true;
|
|
1295
|
+
contentChanged = true;
|
|
1296
|
+
sawCommandDelta = true;
|
|
1297
|
+
pendingAgentToolRef.current = true;
|
|
1298
|
+
pendingGitRef.current = true;
|
|
1036
1299
|
}
|
|
1037
1300
|
}
|
|
1038
1301
|
// Advance the high-water mark past everything we've folded.
|
|
1039
1302
|
for (const event of events)
|
|
1040
1303
|
if (event.sequence > lastSeqRef.current) lastSeqRef.current = event.sequence;
|
|
1041
1304
|
if (!sawNew) return;
|
|
1305
|
+
if (contentChanged && !sawCommandDelta) setContentRevision((revision) => revision + 1);
|
|
1042
1306
|
|
|
1043
1307
|
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
1044
1308
|
const identityGeneration = identityGenerationRef.current;
|
|
1309
|
+
const delay = sawCommandDelta
|
|
1310
|
+
? Math.max(0, 1_000 - (Date.now() - lastCommandRefreshAtRef.current))
|
|
1311
|
+
: 150;
|
|
1045
1312
|
debounceRef.current = setTimeout(() => {
|
|
1046
1313
|
debounceRef.current = null;
|
|
1047
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
|
+
}
|
|
1321
|
+
if (sawCommandDelta) {
|
|
1322
|
+
lastCommandRefreshAtRef.current = Date.now();
|
|
1323
|
+
setContentRevision((revision) => revision + 1);
|
|
1324
|
+
}
|
|
1048
1325
|
const parents = pendingParentsRef.current;
|
|
1049
1326
|
pendingParentsRef.current = new Set();
|
|
1050
1327
|
const wantGit = pendingGitRef.current;
|
|
1051
1328
|
pendingGitRef.current = false;
|
|
1052
|
-
|
|
1053
|
-
|
|
1329
|
+
const wantAgentTool = pendingAgentToolRef.current;
|
|
1330
|
+
pendingAgentToolRef.current = false;
|
|
1331
|
+
// Reconcile the changed directories and Git overlay concurrently. Both are
|
|
1332
|
+
// independent remote reads; applyStatus makes their completion order safe.
|
|
1333
|
+
const eventReadSignal = eventReadAbortRef.current.signal;
|
|
1054
1334
|
void (async () => {
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
}
|
|
1335
|
+
const paths = wantAgentTool ? loadedDirectoryPaths(treeRef.current) : [...parents];
|
|
1336
|
+
await Promise.all([
|
|
1337
|
+
wantGit ? refreshGitOverlay(eventReadSignal) : Promise.resolve(),
|
|
1338
|
+
Promise.all(paths.map((path) => reconcilePath(path, eventReadSignal))),
|
|
1339
|
+
]);
|
|
1061
1340
|
})();
|
|
1062
|
-
},
|
|
1063
|
-
}, [enabled, events, reconcilePath, refreshGitOverlay]);
|
|
1341
|
+
}, delay);
|
|
1342
|
+
}, [enabled, active, acceptsEventReads, events, reconcilePath, refreshGitOverlay]);
|
|
1343
|
+
|
|
1344
|
+
useEffect(() => {
|
|
1345
|
+
if (active && acceptsEventReads) return;
|
|
1346
|
+
if (debounceRef.current) {
|
|
1347
|
+
clearTimeout(debounceRef.current);
|
|
1348
|
+
debounceRef.current = null;
|
|
1349
|
+
}
|
|
1350
|
+
pendingParentsRef.current = new Set();
|
|
1351
|
+
pendingGitRef.current = false;
|
|
1352
|
+
pendingAgentToolRef.current = false;
|
|
1353
|
+
}, [active, acceptsEventReads]);
|
|
1064
1354
|
|
|
1065
1355
|
useEffect(
|
|
1066
1356
|
() => () => {
|
|
1067
1357
|
refreshAbortRef.current?.abort();
|
|
1068
1358
|
identityAbortRef.current.abort();
|
|
1359
|
+
eventReadAbortRef.current.abort();
|
|
1069
1360
|
identityGenerationRef.current += 1;
|
|
1070
1361
|
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
1071
1362
|
},
|
|
@@ -1084,8 +1375,11 @@ export function useSandboxFiles(
|
|
|
1084
1375
|
deleteEntry,
|
|
1085
1376
|
moveEntry,
|
|
1086
1377
|
refresh,
|
|
1378
|
+
contentRevision: identityMatches ? contentRevision : 0,
|
|
1087
1379
|
source: identityMatches ? source : null,
|
|
1088
1380
|
capturedAt: identityMatches && source === "capture" ? (capture?.capturedAt ?? null) : null,
|
|
1381
|
+
gitSummary: identityMatches ? gitSummary : null,
|
|
1382
|
+
gitLoading: identityMatches && gitLoading,
|
|
1089
1383
|
loading: identityMatches && loading,
|
|
1090
1384
|
error: identityMatches ? error : null,
|
|
1091
1385
|
};
|