@opengeni/react 1.0.1 → 2.2.0-canary.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -10
- package/dist/artifacts-spreadsheet.js +1 -1
- package/dist/artifacts.js +1 -1
- package/dist/browser-viewer-GXUICIAG.js +14 -0
- package/dist/{chunk-R3AZOAB5.js → chunk-4MDLEHXL.js} +9 -115
- package/dist/chunk-4MDLEHXL.js.map +1 -0
- package/dist/{chunk-Y7OEDUJ5.js → chunk-5RZN3DD5.js} +1014 -420
- package/dist/chunk-5RZN3DD5.js.map +1 -0
- package/dist/chunk-5YK2ZDIG.js +116 -0
- package/dist/chunk-5YK2ZDIG.js.map +1 -0
- package/dist/chunk-H5YGYNXS.js +329 -0
- package/dist/chunk-H5YGYNXS.js.map +1 -0
- package/dist/{chunk-7WCLAEFV.js → chunk-ICA6R6TW.js} +2 -2
- package/dist/{chunk-7WCLAEFV.js.map → chunk-ICA6R6TW.js.map} +1 -1
- package/dist/{chunk-IGC6A2ZI.js → chunk-JOAVA2TW.js} +589 -217
- package/dist/chunk-JOAVA2TW.js.map +1 -0
- package/dist/{chunk-5UQPNLIO.js → chunk-LTCHJ4MS.js} +9 -9
- package/dist/chunk-LTCHJ4MS.js.map +1 -0
- package/dist/{chunk-C5ZPSSS5.js → chunk-NEXLLTP4.js} +2 -1
- package/dist/{chunk-C5ZPSSS5.js.map → chunk-NEXLLTP4.js.map} +1 -1
- package/dist/chunk-QGBJ6GHG.js +2300 -0
- package/dist/chunk-QGBJ6GHG.js.map +1 -0
- package/dist/{chunk-B6WPZSTT.js → chunk-UOLJWT5Y.js} +78 -16
- package/dist/chunk-UOLJWT5Y.js.map +1 -0
- package/dist/{chunk-MKPIAGVL.js → chunk-W6NFJXSA.js} +8 -19
- package/dist/chunk-W6NFJXSA.js.map +1 -0
- package/dist/{chunk-XBZWOM2K.js → chunk-X4A66EWZ.js} +8 -3
- package/dist/chunk-X4A66EWZ.js.map +1 -0
- package/dist/chunk-X4JAFD5R.js +3907 -0
- package/dist/chunk-X4JAFD5R.js.map +1 -0
- package/dist/{chunk-6KLX2RH5.js → chunk-XSA2QAKP.js} +334 -19
- package/dist/chunk-XSA2QAKP.js.map +1 -0
- package/dist/{chunk-HA46CYQK.js → chunk-XXWH2JLC.js} +792 -206
- package/dist/chunk-XXWH2JLC.js.map +1 -0
- package/dist/client.d.ts +2 -2
- package/dist/components/browser-viewer.d.ts +7 -1
- package/dist/components/computer-viewer.d.ts +4 -1
- package/dist/components/enrollment-consent.d.ts +6 -3
- package/dist/components/file-browser.d.ts +7 -1
- package/dist/components/human-input-form.d.ts +1 -1
- package/dist/components/human-input-surface.d.ts +1 -1
- package/dist/components/machine-card.d.ts +4 -1
- package/dist/components/machines/machine-detail.d.ts +4 -1
- package/dist/components/machines/machine-operation-policy.d.ts +11 -0
- package/dist/components/machines-dashboard.d.ts +3 -1
- package/dist/components/markdown.d.ts +20 -1
- package/dist/components/message-timeline.d.ts +1 -1
- package/dist/components/model-policy-picker.d.ts +2 -0
- package/dist/components/pierre-file.d.ts +1 -1
- package/dist/components/sandbox-files.d.ts +8 -2
- package/dist/components/sandbox-workspace.d.ts +33 -3
- package/dist/components/session-chrome.d.ts +15 -3
- package/dist/components/workspace-dock.d.ts +6 -2
- package/dist/composer.js +5 -5
- package/dist/computer-viewer-ZOP4Q7A7.js +16 -0
- package/dist/computer-viewer-ZOP4Q7A7.js.map +1 -0
- package/dist/hooks/channel-cache.d.ts +23 -0
- package/dist/hooks/shared-machines-list.d.ts +14 -0
- package/dist/hooks/use-attached-browsers.d.ts +2 -1
- package/dist/hooks/use-channels.d.ts +1 -0
- package/dist/hooks/use-composer.d.ts +37 -9
- package/dist/hooks/use-machine-chip.d.ts +3 -3
- package/dist/hooks/use-machines.d.ts +19 -1
- package/dist/hooks/use-sandbox-files.d.ts +1 -0
- package/dist/hooks/use-session-background-commands.d.ts +14 -0
- package/dist/hooks/use-turn-queue.d.ts +12 -0
- package/dist/hooks/use-workspace-sessions.d.ts +1 -0
- package/dist/index.d.ts +7 -5
- package/dist/index.js +1096 -623
- package/dist/index.js.map +1 -1
- package/dist/interaction.js +29 -181
- package/dist/interaction.js.map +1 -1
- package/dist/lib/interaction-errors.d.ts +3 -0
- package/dist/machines.d.ts +4 -2
- package/dist/machines.js +10 -2
- package/dist/model-policy.d.ts +1 -1
- package/dist/model-policy.js +1 -1
- package/dist/platform-activity-row-BJHUHXAT.js +169 -0
- package/dist/platform-activity-row-BJHUHXAT.js.map +1 -0
- package/dist/{queue-surface-implementation-2KVJTYXH.js → queue-surface-implementation-BSKUWHRI.js} +76 -16
- package/dist/queue-surface-implementation-BSKUWHRI.js.map +1 -0
- package/dist/realtime/realtime-control.d.ts +6 -3
- package/dist/realtime.js +36 -14
- package/dist/realtime.js.map +1 -1
- package/dist/session-ui.d.ts +1 -1
- package/dist/session-ui.js +6 -2
- package/dist/session.d.ts +2 -2
- package/dist/session.js +4 -4
- package/dist/timeline/index.d.ts +1 -1
- package/dist/timeline/platform-activity-row.d.ts +15 -0
- package/dist/timeline/screenshot-lightbox.d.ts +1 -1
- package/dist/timeline/shared.d.ts +4 -2
- package/dist/timeline/types.d.ts +48 -5
- package/dist/types/machines.d.ts +2 -2
- package/package.json +3 -3
- package/src/client.ts +12 -1
- package/src/components/artifacts/editable-spreadsheet.tsx +1 -1
- package/src/components/browser-viewer.tsx +361 -58
- package/src/components/chat-composer.tsx +1 -1
- package/src/components/composer.tsx +6 -6
- package/src/components/computer-viewer.tsx +368 -115
- package/src/components/enrollment-consent.tsx +64 -6
- package/src/components/file-browser.tsx +117 -2
- package/src/components/human-input-form.tsx +91 -55
- package/src/components/human-input-surface.tsx +1 -1
- package/src/components/machine-card.tsx +111 -0
- package/src/components/machine-input-display.ts +15 -0
- package/src/components/machines/machine-detail.tsx +47 -0
- package/src/components/machines/machine-operation-policy.tsx +179 -0
- package/src/components/machines-dashboard.tsx +6 -0
- package/src/components/markdown.tsx +174 -20
- package/src/components/message-timeline.tsx +162 -17
- package/src/components/model-policy-picker.tsx +9 -17
- package/src/components/pierre-file.tsx +1 -1
- package/src/components/queue-surface-implementation.tsx +82 -16
- package/src/components/sandbox-files.tsx +135 -19
- package/src/components/sandbox-workspace.tsx +204 -92
- package/src/components/session-chrome.tsx +415 -208
- package/src/components/session-status.tsx +1 -1
- package/src/components/user-message-body.tsx +168 -48
- package/src/components/workspace-dock.tsx +141 -175
- package/src/hooks/channel-cache.ts +104 -0
- package/src/hooks/shared-machines-list.ts +239 -0
- package/src/hooks/use-attached-browsers.ts +5 -1
- package/src/hooks/use-browser-frame-stream.ts +6 -1
- package/src/hooks/use-browser-session.ts +6 -4
- package/src/hooks/use-browser-sessions.ts +7 -1
- package/src/hooks/use-channels.ts +32 -10
- package/src/hooks/use-composer.ts +760 -291
- package/src/hooks/use-computer-frame-stream.ts +14 -9
- package/src/hooks/use-computer-session.ts +16 -14
- package/src/hooks/use-computer-sessions.ts +8 -1
- package/src/hooks/use-last-started-turn-policy.ts +1 -1
- package/src/hooks/use-machine-chip.ts +23 -6
- package/src/hooks/use-machines.ts +104 -6
- package/src/hooks/use-sandbox-files.ts +195 -140
- package/src/hooks/use-sandbox-git.ts +58 -29
- package/src/hooks/use-session-background-commands.ts +56 -0
- package/src/hooks/use-session-capabilities.ts +18 -0
- package/src/hooks/use-turn-queue.ts +111 -12
- package/src/hooks/use-workspace-sessions.ts +5 -1
- package/src/index.ts +26 -4
- package/src/lib/interaction-errors.ts +18 -0
- package/src/machines.ts +9 -1
- package/src/model-policy.ts +10 -2
- package/src/realtime/realtime-control.tsx +53 -17
- package/src/session-context.ts +1 -0
- package/src/session-ui.ts +6 -1
- package/src/session.ts +9 -1
- package/src/timeline/activity-rail.tsx +20 -75
- package/src/timeline/index.ts +4 -0
- package/src/timeline/platform-activity-row.tsx +201 -0
- package/src/timeline/projection.ts +476 -23
- package/src/timeline/screenshot-lightbox.tsx +48 -14
- package/src/timeline/shared.tsx +8 -2
- package/src/timeline/tool-renderers.tsx +104 -6
- package/src/timeline/turn-summary.tsx +6 -1
- package/src/timeline/types.ts +75 -2
- package/src/types/machines.ts +10 -0
- package/styles/compiled.css +115 -104
- package/dist/chunk-5UQPNLIO.js.map +0 -1
- package/dist/chunk-6KLX2RH5.js.map +0 -1
- package/dist/chunk-B6WPZSTT.js.map +0 -1
- package/dist/chunk-HA46CYQK.js.map +0 -1
- package/dist/chunk-IGC6A2ZI.js.map +0 -1
- package/dist/chunk-LLOLUHVG.js +0 -5937
- package/dist/chunk-LLOLUHVG.js.map +0 -1
- package/dist/chunk-MKPIAGVL.js.map +0 -1
- package/dist/chunk-R3AZOAB5.js.map +0 -1
- package/dist/chunk-XBZWOM2K.js.map +0 -1
- package/dist/chunk-Y7OEDUJ5.js.map +0 -1
- package/dist/interactive-workbench-viewers-TOHBXJEE.js +0 -15
- package/dist/queue-surface-implementation-2KVJTYXH.js.map +0 -1
- /package/dist/{interactive-workbench-viewers-TOHBXJEE.js.map → browser-viewer-GXUICIAG.js.map} +0 -0
|
@@ -65,7 +65,8 @@ export type SandboxWriteFileOptions = {
|
|
|
65
65
|
/** A node in the Pierre file tree. `children === undefined` ⇒ an unexpanded dir
|
|
66
66
|
* (lazy treeMode); `children: []` ⇒ an expanded-but-empty dir. */
|
|
67
67
|
export type FileTreeNode = {
|
|
68
|
-
path
|
|
68
|
+
/** Opaque path returned by the selected target's FileSystem boundary. */
|
|
69
|
+
path: string;
|
|
69
70
|
name: string;
|
|
70
71
|
kind: "file" | "dir";
|
|
71
72
|
children?: FileTreeNode[] | undefined;
|
|
@@ -162,16 +163,21 @@ export type SandboxFilesGitSummary = {
|
|
|
162
163
|
};
|
|
163
164
|
|
|
164
165
|
function normalizeRoot(root: string): string {
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
.replace(/\/+$/, "");
|
|
166
|
+
const slashNormalized = root.replaceAll("\\", "/").replace(/^\.\/+/, "");
|
|
167
|
+
if (slashNormalized === "/") return "/";
|
|
168
|
+
const normalized = slashNormalized.replace(/\/+$/, "");
|
|
169
169
|
return normalized === "." ? "" : normalized;
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
-
function
|
|
172
|
+
function joinTreeRoot(root: string, path: string): string {
|
|
173
|
+
if (!root || path.startsWith("/")) return path;
|
|
174
|
+
return root === "/" ? `/${path}` : `${root}/${path}`;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function qualifyStatusPath(root: string, path: string, treeRoot: string): string {
|
|
173
178
|
const normalized = normalizeRoot(root);
|
|
174
|
-
|
|
179
|
+
const workspaceRelative = joinTreeRoot(normalized, path);
|
|
180
|
+
return joinTreeRoot(normalizeRoot(treeRoot), workspaceRelative);
|
|
175
181
|
}
|
|
176
182
|
|
|
177
183
|
function summarizeStatuses(
|
|
@@ -194,6 +200,11 @@ function parentOf(path: string): string {
|
|
|
194
200
|
return i <= 0 ? "" : path.slice(0, i);
|
|
195
201
|
}
|
|
196
202
|
|
|
203
|
+
function parentWithinTree(path: string, rootPath: string): string {
|
|
204
|
+
const parent = parentOf(path);
|
|
205
|
+
return parent === "" && normalizeRoot(rootPath) === "/" && path.startsWith("/") ? "/" : parent;
|
|
206
|
+
}
|
|
207
|
+
|
|
197
208
|
/** The leaf name of a POSIX path. */
|
|
198
209
|
function leafOf(path: string): string {
|
|
199
210
|
const i = path.lastIndexOf("/");
|
|
@@ -282,33 +293,22 @@ function findNodeByPath(nodes: FileTreeNode[], path: string): FileTreeNode | und
|
|
|
282
293
|
return undefined;
|
|
283
294
|
}
|
|
284
295
|
|
|
285
|
-
|
|
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
|
-
}
|
|
296
|
+
const FS_LIST_BATCH_REQUEST_LIMIT = 16;
|
|
300
297
|
|
|
301
298
|
/** Repository paths are capability-advertised and therefore safe to use as a
|
|
302
299
|
* bounded initial lazy-tree frontier. Listing their ancestors concurrently with
|
|
303
300
|
* the root avoids a root → repositories → repo remote waterfall. */
|
|
304
301
|
function initialDirectoryFrontier(rootPath: string, repoPaths: readonly string[]): string[] {
|
|
305
|
-
const
|
|
302
|
+
const root = normalizeRoot(rootPath);
|
|
303
|
+
const rootParts = root.split("/").filter(Boolean);
|
|
306
304
|
const paths = new Set<string>();
|
|
307
305
|
for (const repoPath of repoPaths) {
|
|
308
|
-
const
|
|
306
|
+
const canonicalRepoPath = joinTreeRoot(root, normalizeRoot(repoPath));
|
|
307
|
+
const parts = canonicalRepoPath.split("/").filter(Boolean);
|
|
309
308
|
if (rootParts.some((part, index) => parts[index] !== part)) continue;
|
|
310
309
|
for (let depth = rootParts.length + 1; depth <= parts.length; depth += 1) {
|
|
311
|
-
|
|
310
|
+
const path = parts.slice(0, depth).join("/");
|
|
311
|
+
paths.add(root.startsWith("/") ? `/${path}` : path);
|
|
312
312
|
}
|
|
313
313
|
}
|
|
314
314
|
// A workspace with hundreds of repositories should remain lazy. This still
|
|
@@ -333,8 +333,8 @@ function initialDirectoryFrontier(rootPath: string, repoPaths: readonly string[]
|
|
|
333
333
|
|
|
334
334
|
/** True when `parent` is the root ("") or a dir node that is actually present in
|
|
335
335
|
* the loaded tree (so an insert/remove there would be visible). */
|
|
336
|
-
function parentIsLoaded(nodes: FileTreeNode[], parent: string): boolean {
|
|
337
|
-
if (parent ===
|
|
336
|
+
function parentIsLoaded(nodes: FileTreeNode[], parent: string, rootPath = ""): boolean {
|
|
337
|
+
if (parent === normalizeRoot(rootPath)) return true;
|
|
338
338
|
const node = findNodeByPath(nodes, parent);
|
|
339
339
|
return Boolean(node && node.kind === "dir" && node.children !== undefined);
|
|
340
340
|
}
|
|
@@ -342,8 +342,13 @@ function parentIsLoaded(nodes: FileTreeNode[], parent: string): boolean {
|
|
|
342
342
|
/** Insert `child` under `parent` (immutably), keeping siblings sorted. A no-op
|
|
343
343
|
* (returns the same array) when the parent isn't a loaded dir or the child
|
|
344
344
|
* already exists. */
|
|
345
|
-
function insertNode(
|
|
346
|
-
|
|
345
|
+
function insertNode(
|
|
346
|
+
nodes: FileTreeNode[],
|
|
347
|
+
parent: string,
|
|
348
|
+
child: FileTreeNode,
|
|
349
|
+
rootPath = "",
|
|
350
|
+
): FileTreeNode[] {
|
|
351
|
+
if (parent === normalizeRoot(rootPath)) {
|
|
347
352
|
if (nodes.some((n) => n.path === child.path)) return nodes;
|
|
348
353
|
return sortNodes([...nodes, child]);
|
|
349
354
|
}
|
|
@@ -354,16 +359,16 @@ function insertNode(nodes: FileTreeNode[], parent: string, child: FileTreeNode):
|
|
|
354
359
|
return { ...node, children: sortNodes([...node.children, child]) };
|
|
355
360
|
}
|
|
356
361
|
if (node.kind === "dir" && node.children && parent.startsWith(`${node.path}/`)) {
|
|
357
|
-
return { ...node, children: insertNode(node.children, parent, child) };
|
|
362
|
+
return { ...node, children: insertNode(node.children, parent, child, rootPath) };
|
|
358
363
|
}
|
|
359
364
|
return node;
|
|
360
365
|
});
|
|
361
366
|
}
|
|
362
367
|
|
|
363
368
|
/** Remove the node at `path` (immutably). No-op when it isn't in the tree. */
|
|
364
|
-
function removeNode(nodes: FileTreeNode[], path: string): FileTreeNode[] {
|
|
365
|
-
const parent =
|
|
366
|
-
if (parent ===
|
|
369
|
+
function removeNode(nodes: FileTreeNode[], path: string, rootPath = ""): FileTreeNode[] {
|
|
370
|
+
const parent = parentWithinTree(path, rootPath);
|
|
371
|
+
if (parent === normalizeRoot(rootPath)) {
|
|
367
372
|
if (!nodes.some((n) => n.path === path)) return nodes;
|
|
368
373
|
return nodes.filter((n) => n.path !== path);
|
|
369
374
|
}
|
|
@@ -376,7 +381,7 @@ function removeNode(nodes: FileTreeNode[], path: string): FileTreeNode[] {
|
|
|
376
381
|
};
|
|
377
382
|
}
|
|
378
383
|
if (node.kind === "dir" && node.children && parent.startsWith(`${node.path}/`)) {
|
|
379
|
-
return { ...node, children: removeNode(node.children, path) };
|
|
384
|
+
return { ...node, children: removeNode(node.children, path, rootPath) };
|
|
380
385
|
}
|
|
381
386
|
return node;
|
|
382
387
|
});
|
|
@@ -527,9 +532,9 @@ export function useSandboxFiles(
|
|
|
527
532
|
const lastSeqRef = useRef(0);
|
|
528
533
|
const pendingParentsRef = useRef<Set<string>>(new Set());
|
|
529
534
|
const pendingGitRef = useRef(false);
|
|
530
|
-
const
|
|
535
|
+
const eventReconcileInFlightRef = useRef<Promise<void> | null>(null);
|
|
536
|
+
const eventReconcilePendingRef = useRef(false);
|
|
531
537
|
const debounceRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
532
|
-
const lastCommandRefreshAtRef = useRef(0);
|
|
533
538
|
|
|
534
539
|
// ── Self-emitted fs.changed de-dupe ───────────────────────────────────────
|
|
535
540
|
// Every one of OUR mutations emits an `fs.changed` event back on the live log
|
|
@@ -584,34 +589,15 @@ export function useSandboxFiles(
|
|
|
584
589
|
setGitLoading(true);
|
|
585
590
|
setError(null);
|
|
586
591
|
try {
|
|
587
|
-
//
|
|
588
|
-
//
|
|
592
|
+
// Paint the useful file tree before the optional Git tint. Separate remote
|
|
593
|
+
// requests for one Modal box are deliberately serialized by the API; firing
|
|
594
|
+
// both here only made the Git request contend with the file request during
|
|
595
|
+
// cold starts.
|
|
589
596
|
const frontierPaths = initialDirectoryFrontier(rootPath, repoPaths);
|
|
590
597
|
const listRequests = [rootPath, ...frontierPaths].map((path) => ({
|
|
591
598
|
path,
|
|
592
599
|
depth: 1,
|
|
593
600
|
}));
|
|
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
601
|
// Root plus the bounded repository frontier share one API request, one
|
|
616
602
|
// direct holder, and one provider attach. The old per-directory fan-out
|
|
617
603
|
// paid that fixed control-plane cost up to nine times on every Files open.
|
|
@@ -644,7 +630,27 @@ export function useSandboxFiles(
|
|
|
644
630
|
setSource("live");
|
|
645
631
|
setLoading(false);
|
|
646
632
|
|
|
647
|
-
const statuses =
|
|
633
|
+
const statuses =
|
|
634
|
+
repoPaths.length > 0
|
|
635
|
+
? await client
|
|
636
|
+
.gitReadBatch(
|
|
637
|
+
workspaceId,
|
|
638
|
+
sessionId,
|
|
639
|
+
{
|
|
640
|
+
requests: repoPaths.map((root) => ({
|
|
641
|
+
status: { path: root },
|
|
642
|
+
})),
|
|
643
|
+
},
|
|
644
|
+
{ signal: refreshAbort.signal },
|
|
645
|
+
)
|
|
646
|
+
.then((batch) =>
|
|
647
|
+
batch.results.map((result, index) => ({
|
|
648
|
+
root: repoPaths[index] ?? "",
|
|
649
|
+
status: result.status,
|
|
650
|
+
})),
|
|
651
|
+
)
|
|
652
|
+
.catch(() => null)
|
|
653
|
+
: [];
|
|
648
654
|
if (refreshGenerationRef.current !== generation) return;
|
|
649
655
|
if (statuses) {
|
|
650
656
|
const overlay = new Map<string, FileTreeStatus>();
|
|
@@ -652,7 +658,7 @@ export function useSandboxFiles(
|
|
|
652
658
|
for (const file of status.files) {
|
|
653
659
|
const code = file.worktree ?? file.index;
|
|
654
660
|
const mapped = code ? GIT_STATUS_TO_TREE[code] : undefined;
|
|
655
|
-
if (mapped) overlay.set(qualifyStatusPath(root, file.path), mapped);
|
|
661
|
+
if (mapped) overlay.set(qualifyStatusPath(root, file.path, rootPath), mapped);
|
|
656
662
|
}
|
|
657
663
|
}
|
|
658
664
|
statusRef.current = overlay;
|
|
@@ -866,13 +872,14 @@ export function useSandboxFiles(
|
|
|
866
872
|
: identitySignal;
|
|
867
873
|
// Skip parents that aren't currently loaded/expanded in the tree (nothing
|
|
868
874
|
// visible to update; they re-list fresh on the next expand).
|
|
869
|
-
if (!parentIsLoaded(treeRef.current, path)) return;
|
|
875
|
+
if (!parentIsLoaded(treeRef.current, path, rootPath)) return;
|
|
870
876
|
try {
|
|
871
877
|
const listed = await client.fsList(workspaceId, sessionId, { path, depth: 1 }, { signal });
|
|
872
878
|
if (identityGenerationRef.current !== identityGeneration) return;
|
|
873
879
|
const children = (listed.root.children ?? []).map((node) => fsNodeToTree(node));
|
|
874
|
-
if (path ===
|
|
875
|
-
|
|
880
|
+
if (path === normalizeRoot(rootPath)) {
|
|
881
|
+
setTree((prev) => applyStatus(mergeRootChildren(prev, children)));
|
|
882
|
+
} else
|
|
876
883
|
setTree((prev) => {
|
|
877
884
|
const existing = findNodeByPath(prev, path);
|
|
878
885
|
const merged = existing?.children
|
|
@@ -885,7 +892,58 @@ export function useSandboxFiles(
|
|
|
885
892
|
// root-refresh here (that would collapse the tree the user is working in).
|
|
886
893
|
}
|
|
887
894
|
},
|
|
888
|
-
[client, workspaceId, sessionId, applyStatus],
|
|
895
|
+
[client, workspaceId, sessionId, rootPath, applyStatus],
|
|
896
|
+
);
|
|
897
|
+
|
|
898
|
+
// Reconcile the visible directory frontier through bounded batch requests.
|
|
899
|
+
// One tool completion can affect several expanded directories; issuing one
|
|
900
|
+
// provider request per directory created a self-inflicted lock convoy.
|
|
901
|
+
const reconcilePaths = useCallback(
|
|
902
|
+
async (requestedPaths: readonly string[], requestSignal?: AbortSignal) => {
|
|
903
|
+
if (!sessionId) return;
|
|
904
|
+
const identityGeneration = identityGenerationRef.current;
|
|
905
|
+
const identitySignal = identityAbortRef.current.signal;
|
|
906
|
+
const signal = requestSignal
|
|
907
|
+
? AbortSignal.any([identitySignal, requestSignal])
|
|
908
|
+
: identitySignal;
|
|
909
|
+
const paths = [...new Set(requestedPaths)].filter((path) =>
|
|
910
|
+
parentIsLoaded(treeRef.current, path, rootPath),
|
|
911
|
+
);
|
|
912
|
+
for (let offset = 0; offset < paths.length; offset += FS_LIST_BATCH_REQUEST_LIMIT) {
|
|
913
|
+
const chunk = paths.slice(offset, offset + FS_LIST_BATCH_REQUEST_LIMIT);
|
|
914
|
+
try {
|
|
915
|
+
const listed = await client.fsListBatch(
|
|
916
|
+
workspaceId,
|
|
917
|
+
sessionId,
|
|
918
|
+
{ requests: chunk.map((path) => ({ path, depth: 1 })) },
|
|
919
|
+
{ signal },
|
|
920
|
+
);
|
|
921
|
+
if (identityGenerationRef.current !== identityGeneration) return;
|
|
922
|
+
setTree((previous) => {
|
|
923
|
+
let next = previous;
|
|
924
|
+
for (const [index, path] of chunk.entries()) {
|
|
925
|
+
const result = listed.results[index];
|
|
926
|
+
if (!result) continue;
|
|
927
|
+
const children = (result.root.children ?? []).map((node) => fsNodeToTree(node));
|
|
928
|
+
if (path === normalizeRoot(rootPath)) {
|
|
929
|
+
next = mergeRootChildren(next, children);
|
|
930
|
+
} else {
|
|
931
|
+
const existing = findNodeByPath(next, path);
|
|
932
|
+
const merged = existing?.children
|
|
933
|
+
? mergeChildren(existing.children, children)
|
|
934
|
+
: children;
|
|
935
|
+
next = replaceChildren(next, path, merged);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
return applyStatus(next);
|
|
939
|
+
});
|
|
940
|
+
} catch {
|
|
941
|
+
// Reconciliation is best-effort. Preserve the current visible tree and
|
|
942
|
+
// let the next real event or explicit refresh reconcile it.
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
},
|
|
946
|
+
[client, workspaceId, sessionId, rootPath, applyStatus],
|
|
889
947
|
);
|
|
890
948
|
|
|
891
949
|
// Run a Channel-A op behind an OPTIMISTIC tree edit. `apply` splices the change
|
|
@@ -950,7 +1008,7 @@ export function useSandboxFiles(
|
|
|
950
1008
|
if (!sessionId) throw new Error("no session");
|
|
951
1009
|
const identityGeneration = identityGenerationRef.current;
|
|
952
1010
|
const identitySignal = identityAbortRef.current.signal;
|
|
953
|
-
const parent =
|
|
1011
|
+
const parent = parentWithinTree(path, rootPath);
|
|
954
1012
|
// Splice a new file node in immediately ONLY when the path doesn't already
|
|
955
1013
|
// exist in a loaded parent (an editor SAVE to an existing file mutates no
|
|
956
1014
|
// tree shape — just content — so it needs no optimistic node, no reconcile).
|
|
@@ -963,7 +1021,7 @@ export function useSandboxFiles(
|
|
|
963
1021
|
};
|
|
964
1022
|
return await runOptimistic(
|
|
965
1023
|
"write",
|
|
966
|
-
(nodes) => (exists ? nodes : insertNode(nodes, parent, node)),
|
|
1024
|
+
(nodes) => (exists ? nodes : insertNode(nodes, parent, node, rootPath)),
|
|
967
1025
|
async () => {
|
|
968
1026
|
if (!writeOptions.force && writeOptions.expectedContent !== undefined) {
|
|
969
1027
|
const live = await client.fsRead(
|
|
@@ -993,13 +1051,13 @@ export function useSandboxFiles(
|
|
|
993
1051
|
exists ? [] : [parent],
|
|
994
1052
|
);
|
|
995
1053
|
},
|
|
996
|
-
[client, workspaceId, sessionId, tree, runOptimistic],
|
|
1054
|
+
[client, workspaceId, sessionId, tree, rootPath, runOptimistic],
|
|
997
1055
|
);
|
|
998
1056
|
|
|
999
1057
|
const createFile = useCallback(
|
|
1000
1058
|
async (path: string): Promise<void> => {
|
|
1001
1059
|
if (!sessionId) throw new Error("no session");
|
|
1002
|
-
const parent =
|
|
1060
|
+
const parent = parentWithinTree(path, rootPath);
|
|
1003
1061
|
const node: FileTreeNode = {
|
|
1004
1062
|
path,
|
|
1005
1063
|
name: leafOf(path),
|
|
@@ -1008,7 +1066,7 @@ export function useSandboxFiles(
|
|
|
1008
1066
|
};
|
|
1009
1067
|
await runOptimistic(
|
|
1010
1068
|
"create file",
|
|
1011
|
-
(nodes) => insertNode(nodes, parent, node),
|
|
1069
|
+
(nodes) => insertNode(nodes, parent, node, rootPath),
|
|
1012
1070
|
() =>
|
|
1013
1071
|
client.fsWrite(workspaceId, sessionId, {
|
|
1014
1072
|
path,
|
|
@@ -1018,13 +1076,13 @@ export function useSandboxFiles(
|
|
|
1018
1076
|
[parent],
|
|
1019
1077
|
);
|
|
1020
1078
|
},
|
|
1021
|
-
[client, workspaceId, sessionId, runOptimistic],
|
|
1079
|
+
[client, workspaceId, sessionId, rootPath, runOptimistic],
|
|
1022
1080
|
);
|
|
1023
1081
|
|
|
1024
1082
|
const createDir = useCallback(
|
|
1025
1083
|
async (path: string): Promise<void> => {
|
|
1026
1084
|
if (!sessionId) throw new Error("no session");
|
|
1027
|
-
const parent =
|
|
1085
|
+
const parent = parentWithinTree(path, rootPath);
|
|
1028
1086
|
// A freshly-created dir is empty + expanded: children:[] (not undefined, so
|
|
1029
1087
|
// it doesn't show the lazy-expand marker over a dir we KNOW is empty).
|
|
1030
1088
|
const node: FileTreeNode = {
|
|
@@ -1035,12 +1093,12 @@ export function useSandboxFiles(
|
|
|
1035
1093
|
};
|
|
1036
1094
|
await runOptimistic(
|
|
1037
1095
|
"create folder",
|
|
1038
|
-
(nodes) => insertNode(nodes, parent, node),
|
|
1096
|
+
(nodes) => insertNode(nodes, parent, node, rootPath),
|
|
1039
1097
|
() => client.fsMkdir(workspaceId, sessionId, { path, recursive: true }),
|
|
1040
1098
|
[parent],
|
|
1041
1099
|
);
|
|
1042
1100
|
},
|
|
1043
|
-
[client, workspaceId, sessionId, runOptimistic],
|
|
1101
|
+
[client, workspaceId, sessionId, rootPath, runOptimistic],
|
|
1044
1102
|
);
|
|
1045
1103
|
|
|
1046
1104
|
const deleteEntry = useCallback(
|
|
@@ -1048,19 +1106,19 @@ export function useSandboxFiles(
|
|
|
1048
1106
|
if (!sessionId) throw new Error("no session");
|
|
1049
1107
|
await runOptimistic(
|
|
1050
1108
|
"delete",
|
|
1051
|
-
(nodes) => removeNode(nodes, path),
|
|
1109
|
+
(nodes) => removeNode(nodes, path, rootPath),
|
|
1052
1110
|
() => client.fsDelete(workspaceId, sessionId, { path, recursive }),
|
|
1053
|
-
[
|
|
1111
|
+
[parentWithinTree(path, rootPath)],
|
|
1054
1112
|
);
|
|
1055
1113
|
},
|
|
1056
|
-
[client, workspaceId, sessionId, runOptimistic],
|
|
1114
|
+
[client, workspaceId, sessionId, rootPath, runOptimistic],
|
|
1057
1115
|
);
|
|
1058
1116
|
|
|
1059
1117
|
const moveEntry = useCallback(
|
|
1060
1118
|
async (path: string, newPath: string, opts?: { overwrite?: boolean }): Promise<void> => {
|
|
1061
1119
|
if (!sessionId) throw new Error("no session");
|
|
1062
|
-
const from =
|
|
1063
|
-
const to =
|
|
1120
|
+
const from = parentWithinTree(path, rootPath);
|
|
1121
|
+
const to = parentWithinTree(newPath, rootPath);
|
|
1064
1122
|
await runOptimistic(
|
|
1065
1123
|
"move",
|
|
1066
1124
|
(nodes) => {
|
|
@@ -1068,7 +1126,7 @@ export function useSandboxFiles(
|
|
|
1068
1126
|
if (!moving) return nodes; // not loaded — let the reconcile pick it up
|
|
1069
1127
|
// Re-path the moved subtree, drop it from its old parent, splice into new.
|
|
1070
1128
|
const moved = repathNode(moving, path, newPath);
|
|
1071
|
-
return insertNode(removeNode(nodes, path), to, moved);
|
|
1129
|
+
return insertNode(removeNode(nodes, path, rootPath), to, moved, rootPath);
|
|
1072
1130
|
},
|
|
1073
1131
|
() =>
|
|
1074
1132
|
client.fsMove(workspaceId, sessionId, {
|
|
@@ -1079,7 +1137,7 @@ export function useSandboxFiles(
|
|
|
1079
1137
|
to === from ? [from] : [from, to],
|
|
1080
1138
|
);
|
|
1081
1139
|
},
|
|
1082
|
-
[client, workspaceId, sessionId, runOptimistic],
|
|
1140
|
+
[client, workspaceId, sessionId, rootPath, runOptimistic],
|
|
1083
1141
|
);
|
|
1084
1142
|
|
|
1085
1143
|
// Initial paint + reset on identity change. Source selection:
|
|
@@ -1107,14 +1165,14 @@ export function useSandboxFiles(
|
|
|
1107
1165
|
const identityChanged = previousIdentityRef.current !== identityKey;
|
|
1108
1166
|
previousIdentityRef.current = identityKey;
|
|
1109
1167
|
if (identityChanged || !enabled) {
|
|
1168
|
+
if (identityChanged) eventReconcileInFlightRef.current = null;
|
|
1110
1169
|
identityAbortRef.current.abort();
|
|
1111
1170
|
identityAbortRef.current = new AbortController();
|
|
1112
1171
|
identityGenerationRef.current += 1;
|
|
1113
1172
|
lastSeqRef.current = 0;
|
|
1114
1173
|
pendingParentsRef.current = new Set();
|
|
1115
1174
|
pendingGitRef.current = false;
|
|
1116
|
-
|
|
1117
|
-
lastCommandRefreshAtRef.current = 0;
|
|
1175
|
+
eventReconcilePendingRef.current = false;
|
|
1118
1176
|
if (debounceRef.current) {
|
|
1119
1177
|
clearTimeout(debounceRef.current);
|
|
1120
1178
|
debounceRef.current = null;
|
|
@@ -1155,6 +1213,7 @@ export function useSandboxFiles(
|
|
|
1155
1213
|
refreshAbortRef.current = null;
|
|
1156
1214
|
refreshGenerationRef.current += 1;
|
|
1157
1215
|
eventReadAbortRef.current.abort();
|
|
1216
|
+
eventReconcilePendingRef.current = false;
|
|
1158
1217
|
};
|
|
1159
1218
|
}, [
|
|
1160
1219
|
enabled,
|
|
@@ -1196,7 +1255,7 @@ export function useSandboxFiles(
|
|
|
1196
1255
|
for (const file of status.files) {
|
|
1197
1256
|
const code = file.worktree ?? file.index;
|
|
1198
1257
|
const mapped = code ? GIT_STATUS_TO_TREE[code] : undefined;
|
|
1199
|
-
if (mapped) overlay.set(qualifyStatusPath(root, file.path), mapped);
|
|
1258
|
+
if (mapped) overlay.set(qualifyStatusPath(root, file.path, rootPath), mapped);
|
|
1200
1259
|
}
|
|
1201
1260
|
}
|
|
1202
1261
|
statusRef.current = overlay;
|
|
@@ -1208,9 +1267,47 @@ export function useSandboxFiles(
|
|
|
1208
1267
|
if (identityGenerationRef.current === identityGeneration) setGitLoading(false);
|
|
1209
1268
|
}
|
|
1210
1269
|
},
|
|
1211
|
-
[client, workspaceId, sessionId, repoPaths, applyStatus],
|
|
1270
|
+
[client, workspaceId, sessionId, repoPaths, rootPath, applyStatus],
|
|
1212
1271
|
);
|
|
1213
1272
|
|
|
1273
|
+
const performEventReconcile = useCallback(async () => {
|
|
1274
|
+
const parents = pendingParentsRef.current;
|
|
1275
|
+
pendingParentsRef.current = new Set();
|
|
1276
|
+
const wantGit = pendingGitRef.current;
|
|
1277
|
+
pendingGitRef.current = false;
|
|
1278
|
+
const signal = eventReadAbortRef.current.signal;
|
|
1279
|
+
const paths = [...parents];
|
|
1280
|
+
|
|
1281
|
+
// File content is the primary Files-tab result. Reconcile it first, then add
|
|
1282
|
+
// the optional Git tint through a second serial request. This matches the
|
|
1283
|
+
// API's exact-provider serialization instead of making the two reads contend.
|
|
1284
|
+
await reconcilePaths(paths, signal);
|
|
1285
|
+
if (wantGit) await refreshGitOverlay(signal);
|
|
1286
|
+
}, [reconcilePaths, refreshGitOverlay]);
|
|
1287
|
+
const performEventReconcileRef = useRef(performEventReconcile);
|
|
1288
|
+
performEventReconcileRef.current = performEventReconcile;
|
|
1289
|
+
const reconcilePendingEvents = useCallback(async () => {
|
|
1290
|
+
eventReconcilePendingRef.current = true;
|
|
1291
|
+
const existing = eventReconcileInFlightRef.current;
|
|
1292
|
+
if (existing) return await existing;
|
|
1293
|
+
const identityGeneration = identityGenerationRef.current;
|
|
1294
|
+
const run = (async () => {
|
|
1295
|
+
while (
|
|
1296
|
+
eventReconcilePendingRef.current &&
|
|
1297
|
+
identityGenerationRef.current === identityGeneration
|
|
1298
|
+
) {
|
|
1299
|
+
eventReconcilePendingRef.current = false;
|
|
1300
|
+
await performEventReconcileRef.current();
|
|
1301
|
+
}
|
|
1302
|
+
})();
|
|
1303
|
+
eventReconcileInFlightRef.current = run;
|
|
1304
|
+
try {
|
|
1305
|
+
await run;
|
|
1306
|
+
} finally {
|
|
1307
|
+
if (eventReconcileInFlightRef.current === run) eventReconcileInFlightRef.current = null;
|
|
1308
|
+
}
|
|
1309
|
+
}, []);
|
|
1310
|
+
|
|
1214
1311
|
// Auto-reconcile on fs/git change notifications — TARGETED, never a root
|
|
1215
1312
|
// collapse-reload, and de-duped against our OWN mutations.
|
|
1216
1313
|
//
|
|
@@ -1221,6 +1318,8 @@ export function useSandboxFiles(
|
|
|
1221
1318
|
// reconciles ONLY the affected parent directories (in place, expansion
|
|
1222
1319
|
// preserved). Bursts are debounced into a single reconcile pass.
|
|
1223
1320
|
// • A git.changed just re-tints (refreshes the status overlay) — no fs re-list.
|
|
1321
|
+
// • Generic command/tool output performs no provider reads. The canonical
|
|
1322
|
+
// turn-end capture refreshes the root once after the command burst settles.
|
|
1224
1323
|
const events = options.events;
|
|
1225
1324
|
|
|
1226
1325
|
useEffect(() => {
|
|
@@ -1238,12 +1337,11 @@ export function useSandboxFiles(
|
|
|
1238
1337
|
}
|
|
1239
1338
|
pendingParentsRef.current = new Set();
|
|
1240
1339
|
pendingGitRef.current = false;
|
|
1241
|
-
|
|
1340
|
+
eventReconcilePendingRef.current = false;
|
|
1242
1341
|
return;
|
|
1243
1342
|
}
|
|
1244
1343
|
let sawNew = false;
|
|
1245
1344
|
let contentChanged = false;
|
|
1246
|
-
let sawCommandDelta = false;
|
|
1247
1345
|
for (const event of events) {
|
|
1248
1346
|
if (event.sequence <= lastSeqRef.current) continue;
|
|
1249
1347
|
if (event.type === "fs.changed") {
|
|
@@ -1256,8 +1354,10 @@ export function useSandboxFiles(
|
|
|
1256
1354
|
continue;
|
|
1257
1355
|
contentChanged = true;
|
|
1258
1356
|
for (const change of payload.changes ?? []) {
|
|
1259
|
-
pendingParentsRef.current.add(
|
|
1260
|
-
if (change.oldPath)
|
|
1357
|
+
pendingParentsRef.current.add(parentWithinTree(change.path, rootPath));
|
|
1358
|
+
if (change.oldPath) {
|
|
1359
|
+
pendingParentsRef.current.add(parentWithinTree(change.oldPath, rootPath));
|
|
1360
|
+
}
|
|
1261
1361
|
}
|
|
1262
1362
|
} else if (event.type === "git.changed") {
|
|
1263
1363
|
sawNew = true;
|
|
@@ -1273,73 +1373,27 @@ export function useSandboxFiles(
|
|
|
1273
1373
|
)
|
|
1274
1374
|
continue;
|
|
1275
1375
|
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;
|
|
1299
1376
|
}
|
|
1300
1377
|
}
|
|
1301
1378
|
// Advance the high-water mark past everything we've folded.
|
|
1302
1379
|
for (const event of events)
|
|
1303
1380
|
if (event.sequence > lastSeqRef.current) lastSeqRef.current = event.sequence;
|
|
1304
1381
|
if (!sawNew) return;
|
|
1305
|
-
if (contentChanged
|
|
1382
|
+
if (contentChanged) setContentRevision((revision) => revision + 1);
|
|
1306
1383
|
|
|
1307
1384
|
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
1308
1385
|
const identityGeneration = identityGenerationRef.current;
|
|
1309
|
-
const delay = sawCommandDelta
|
|
1310
|
-
? Math.max(0, 1_000 - (Date.now() - lastCommandRefreshAtRef.current))
|
|
1311
|
-
: 150;
|
|
1312
1386
|
debounceRef.current = setTimeout(() => {
|
|
1313
1387
|
debounceRef.current = null;
|
|
1314
1388
|
if (identityGenerationRef.current !== identityGeneration) return;
|
|
1315
1389
|
if (!acceptsEventReadsRef.current) {
|
|
1316
1390
|
pendingParentsRef.current = new Set();
|
|
1317
1391
|
pendingGitRef.current = false;
|
|
1318
|
-
pendingAgentToolRef.current = false;
|
|
1319
1392
|
return;
|
|
1320
1393
|
}
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
}
|
|
1325
|
-
const parents = pendingParentsRef.current;
|
|
1326
|
-
pendingParentsRef.current = new Set();
|
|
1327
|
-
const wantGit = pendingGitRef.current;
|
|
1328
|
-
pendingGitRef.current = false;
|
|
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;
|
|
1334
|
-
void (async () => {
|
|
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
|
-
]);
|
|
1340
|
-
})();
|
|
1341
|
-
}, delay);
|
|
1342
|
-
}, [enabled, active, acceptsEventReads, events, reconcilePath, refreshGitOverlay]);
|
|
1394
|
+
void reconcilePendingEvents();
|
|
1395
|
+
}, 150);
|
|
1396
|
+
}, [enabled, active, acceptsEventReads, events, rootPath, reconcilePendingEvents]);
|
|
1343
1397
|
|
|
1344
1398
|
useEffect(() => {
|
|
1345
1399
|
if (active && acceptsEventReads) return;
|
|
@@ -1349,7 +1403,7 @@ export function useSandboxFiles(
|
|
|
1349
1403
|
}
|
|
1350
1404
|
pendingParentsRef.current = new Set();
|
|
1351
1405
|
pendingGitRef.current = false;
|
|
1352
|
-
|
|
1406
|
+
eventReconcilePendingRef.current = false;
|
|
1353
1407
|
}, [active, acceptsEventReads]);
|
|
1354
1408
|
|
|
1355
1409
|
useEffect(
|
|
@@ -1357,6 +1411,7 @@ export function useSandboxFiles(
|
|
|
1357
1411
|
refreshAbortRef.current?.abort();
|
|
1358
1412
|
identityAbortRef.current.abort();
|
|
1359
1413
|
eventReadAbortRef.current.abort();
|
|
1414
|
+
eventReconcilePendingRef.current = false;
|
|
1360
1415
|
identityGenerationRef.current += 1;
|
|
1361
1416
|
if (debounceRef.current) clearTimeout(debounceRef.current);
|
|
1362
1417
|
},
|