@opengeni/react 0.57.0 → 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 +36 -11
- 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 +10 -3
- package/dist/realtime.js +138 -29
- 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 +200 -37
- 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
|
@@ -84,6 +84,10 @@ export function QueueSurface({
|
|
|
84
84
|
const [replaceDraftFor, setReplaceDraftFor] = useState<string | null>(null);
|
|
85
85
|
const [announcement, setAnnouncement] = useState("");
|
|
86
86
|
const [draggedTurnId, setDraggedTurnId] = useState<string | null>(null);
|
|
87
|
+
const [pendingMove, setPendingMove] = useState<{
|
|
88
|
+
baseVersion: number | null;
|
|
89
|
+
turnIds: string[];
|
|
90
|
+
} | null>(null);
|
|
87
91
|
const surface = usePortalTokenSource<HTMLDivElement>();
|
|
88
92
|
const surfaceRef = surface.currentRef;
|
|
89
93
|
const portalTokenStyle = usePortalTokenStyle(surface.source);
|
|
@@ -112,30 +116,56 @@ export function QueueSurface({
|
|
|
112
116
|
const sensors = useSensors(useSensor(PointerSensor, { activationConstraint: { distance: 6 } }));
|
|
113
117
|
|
|
114
118
|
const displayedQueue = useMemo(() => {
|
|
115
|
-
if (
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
119
|
+
if (keyboardDrag) {
|
|
120
|
+
const oldIndex = queue.queue.findIndex((turn) => turn.id === keyboardDrag.turnId);
|
|
121
|
+
if (oldIndex < 0) return queue.queue;
|
|
122
|
+
return arrayMove(queue.queue, oldIndex, keyboardDrag.projectedIndex);
|
|
123
|
+
}
|
|
124
|
+
if (
|
|
125
|
+
!pendingMove ||
|
|
126
|
+
pendingMove.baseVersion === null ||
|
|
127
|
+
queue.snapshot?.version !== pendingMove.baseVersion
|
|
128
|
+
) {
|
|
129
|
+
return queue.queue;
|
|
130
|
+
}
|
|
131
|
+
const byId = new Map(queue.queue.map((turn) => [turn.id, turn]));
|
|
132
|
+
const projected = pendingMove.turnIds.map((turnId) => byId.get(turnId));
|
|
133
|
+
// Never let an optimistic ordering projection omit or invent a row. Any
|
|
134
|
+
// concurrent authoritative membership change wins immediately.
|
|
135
|
+
if (projected.some((turn) => !turn) || projected.length !== queue.queue.length) {
|
|
136
|
+
return queue.queue;
|
|
137
|
+
}
|
|
138
|
+
return projected as SessionTurn[];
|
|
139
|
+
}, [keyboardDrag, pendingMove, queue.queue, queue.snapshot?.version]);
|
|
120
140
|
|
|
121
141
|
const ids = useMemo(() => displayedQueue.map((turn) => turn.id), [displayedQueue]);
|
|
122
142
|
const moveToIndex = useCallback(
|
|
123
143
|
async (turnId: string, nextIndex: number): Promise<void> => {
|
|
144
|
+
if (queue.mutating || pendingMove) return;
|
|
124
145
|
const oldIndex = queue.queue.findIndex((turn) => turn.id === turnId);
|
|
125
146
|
if (oldIndex < 0) return;
|
|
126
147
|
const boundedIndex = Math.max(0, Math.min(nextIndex, queue.queue.length - 1));
|
|
127
148
|
if (oldIndex === boundedIndex) return;
|
|
128
149
|
const ordered = arrayMove(queue.queue, oldIndex, boundedIndex);
|
|
129
150
|
const beforeTurnId = ordered[boundedIndex + 1]?.id ?? null;
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
151
|
+
setPendingMove({
|
|
152
|
+
baseVersion: queue.snapshot?.version ?? null,
|
|
153
|
+
turnIds: ordered.map((turn) => turn.id),
|
|
154
|
+
});
|
|
155
|
+
setAnnouncement(`Saving queued prompt at position ${boundedIndex + 1}.`);
|
|
156
|
+
try {
|
|
157
|
+
const moved = await queue.moveTurn(turnId, beforeTurnId);
|
|
158
|
+
setAnnouncement(
|
|
159
|
+
moved
|
|
160
|
+
? `Queued prompt moved to position ${boundedIndex + 1}.`
|
|
161
|
+
: "The queue changed before that prompt could be moved. Refreshed server order.",
|
|
162
|
+
);
|
|
163
|
+
if (moved) focusQueueTurn(surfaceRef.current, turnId);
|
|
164
|
+
} finally {
|
|
165
|
+
setPendingMove(null);
|
|
166
|
+
}
|
|
137
167
|
},
|
|
138
|
-
[queue, surfaceRef],
|
|
168
|
+
[pendingMove, queue, surfaceRef],
|
|
139
169
|
);
|
|
140
170
|
|
|
141
171
|
const onDragEnd = useCallback(
|
|
@@ -211,11 +241,15 @@ export function QueueSurface({
|
|
|
211
241
|
const edit = useCallback(
|
|
212
242
|
async (turn: SessionTurn, replaceDraft: boolean) => {
|
|
213
243
|
if (!composer || !canEditInComposer) return;
|
|
244
|
+
setAnnouncement("Moving queued prompt to the composer…");
|
|
214
245
|
const restored = await queue.editTurn(turn.id, {
|
|
215
246
|
expectedDraftRevision: composer.draftRevision,
|
|
216
247
|
replaceDraft,
|
|
217
248
|
});
|
|
218
|
-
if (!restored)
|
|
249
|
+
if (!restored) {
|
|
250
|
+
setAnnouncement("That prompt changed before it could be moved to the composer.");
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
219
253
|
composer.applyDraft(restored);
|
|
220
254
|
setReplaceDraftFor(null);
|
|
221
255
|
setAnnouncement("Queued prompt moved back to the composer for editing.");
|
|
@@ -358,6 +392,7 @@ export function QueueSurface({
|
|
|
358
392
|
turn.id === queue.pendingInputAttachment?.turnId ? attachedInputs : []
|
|
359
393
|
}
|
|
360
394
|
pending={queue.mutationFor(turn.id)}
|
|
395
|
+
reorderDisabled={queue.mutating || pendingMove !== null}
|
|
361
396
|
confirmingReplace={replaceDraftFor === turn.id}
|
|
362
397
|
keyboardDragging={keyboardDrag?.turnId === turn.id}
|
|
363
398
|
portalTokenStyle={portalTokenStyle}
|
|
@@ -367,6 +402,7 @@ export function QueueSurface({
|
|
|
367
402
|
onConfirmReplace={() => void edit(turn, true)}
|
|
368
403
|
onCancelReplace={() => setReplaceDraftFor(null)}
|
|
369
404
|
onSteer={() => {
|
|
405
|
+
setAnnouncement("Requesting this queued prompt as the next direction…");
|
|
370
406
|
void queue.steerTurn(turn.id).then((steered) => {
|
|
371
407
|
setAnnouncement(
|
|
372
408
|
steered
|
|
@@ -379,6 +415,7 @@ export function QueueSurface({
|
|
|
379
415
|
});
|
|
380
416
|
}}
|
|
381
417
|
onDelete={() => {
|
|
418
|
+
setAnnouncement("Deleting queued prompt…");
|
|
382
419
|
void queue.removeTurn(turn.id).then((removed) => {
|
|
383
420
|
setAnnouncement(
|
|
384
421
|
removed
|
|
@@ -930,6 +967,7 @@ function SortableQueueRow({
|
|
|
930
967
|
count,
|
|
931
968
|
attachedInputs,
|
|
932
969
|
pending,
|
|
970
|
+
reorderDisabled,
|
|
933
971
|
confirmingReplace,
|
|
934
972
|
keyboardDragging,
|
|
935
973
|
portalTokenStyle,
|
|
@@ -947,6 +985,7 @@ function SortableQueueRow({
|
|
|
947
985
|
count: number;
|
|
948
986
|
attachedInputs: SessionPendingInputPreview[];
|
|
949
987
|
pending: QueueMutationKind | null;
|
|
988
|
+
reorderDisabled: boolean;
|
|
950
989
|
confirmingReplace: boolean;
|
|
951
990
|
keyboardDragging: boolean;
|
|
952
991
|
portalTokenStyle: PortalTokenStyle;
|
|
@@ -961,7 +1000,7 @@ function SortableQueueRow({
|
|
|
961
1000
|
}) {
|
|
962
1001
|
const sortable = useSortable({
|
|
963
1002
|
id: turn.id,
|
|
964
|
-
disabled:
|
|
1003
|
+
disabled: reorderDisabled || keyboardDragging,
|
|
965
1004
|
});
|
|
966
1005
|
return (
|
|
967
1006
|
<li
|
|
@@ -981,7 +1020,7 @@ function SortableQueueRow({
|
|
|
981
1020
|
{...sortable.attributes}
|
|
982
1021
|
{...sortable.listeners}
|
|
983
1022
|
onKeyDown={onHandleKeyDown}
|
|
984
|
-
disabled={
|
|
1023
|
+
disabled={reorderDisabled}
|
|
985
1024
|
className="col-start-1 row-start-1 mt-0.5 inline-flex size-7 shrink-0 touch-none items-center justify-center rounded-md text-fg-subtle hover:bg-surface-2 hover:text-fg focus-visible:ring-2 focus-visible:ring-ring/40 pointer-coarse:size-[44px]"
|
|
986
1025
|
aria-label={`Reorder queued prompt ${index + 1}`}
|
|
987
1026
|
title="Drag to reorder. Press Space, arrows, then Space to drop."
|
|
@@ -1015,6 +1054,20 @@ function SortableQueueRow({
|
|
|
1015
1054
|
<span className="min-w-0 truncate">{turn.model}</span>
|
|
1016
1055
|
<span className="shrink-0">{turn.reasoningEffort}</span>
|
|
1017
1056
|
</div>
|
|
1057
|
+
{pending ? (
|
|
1058
|
+
<div
|
|
1059
|
+
role="status"
|
|
1060
|
+
aria-live="polite"
|
|
1061
|
+
className="mt-1 flex items-center gap-1.5 text-og-xs font-medium text-status-waiting"
|
|
1062
|
+
data-testid={`queue-mutation-${pending}`}
|
|
1063
|
+
>
|
|
1064
|
+
<Loader2Icon
|
|
1065
|
+
aria-hidden="true"
|
|
1066
|
+
className="size-3 shrink-0 animate-spin motion-reduce:animate-none"
|
|
1067
|
+
/>
|
|
1068
|
+
{queueMutationPendingLabel(pending)}
|
|
1069
|
+
</div>
|
|
1070
|
+
) : null}
|
|
1018
1071
|
</div>
|
|
1019
1072
|
<div className="col-span-full row-start-3 flex min-w-0 items-start justify-end gap-1.5 sm:col-span-1 sm:col-start-4 sm:row-start-1 sm:gap-2">
|
|
1020
1073
|
<button
|
|
@@ -1142,6 +1195,19 @@ function SortableQueueRow({
|
|
|
1142
1195
|
);
|
|
1143
1196
|
}
|
|
1144
1197
|
|
|
1198
|
+
function queueMutationPendingLabel(kind: QueueMutationKind): string {
|
|
1199
|
+
switch (kind) {
|
|
1200
|
+
case "move":
|
|
1201
|
+
return "Saving new position…";
|
|
1202
|
+
case "edit":
|
|
1203
|
+
return "Moving to composer…";
|
|
1204
|
+
case "steer":
|
|
1205
|
+
return "Changing direction…";
|
|
1206
|
+
case "delete":
|
|
1207
|
+
return "Deleting…";
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1145
1211
|
const verticalOnly: Modifier = ({ transform }) => ({ ...transform, x: 0 });
|
|
1146
1212
|
|
|
1147
1213
|
function focusQueueTurn(surface: HTMLElement | null, turnId: string): void {
|
|
@@ -2,6 +2,7 @@ import { FileCode2Icon, FileWarningIcon, LoaderCircleIcon } from "lucide-react";
|
|
|
2
2
|
import { type ReactNode, useCallback, useEffect, useRef, useState } from "react";
|
|
3
3
|
import { Group, Panel, Separator } from "react-resizable-panels";
|
|
4
4
|
import { cn } from "../lib/cn";
|
|
5
|
+
import { prefersReducedMotion } from "../lib/motion";
|
|
5
6
|
import { useThemeType } from "../lib/use-theme-type";
|
|
6
7
|
import {
|
|
7
8
|
CapturedFileUnavailableError,
|
|
@@ -32,8 +33,14 @@ export type SandboxFilesProps = {
|
|
|
32
33
|
* dock warms the box on this so the save lands fast; opening/reading never fires
|
|
33
34
|
* it. Browsing the tree/diff must not warm a box. */
|
|
34
35
|
onEditIntent?: (() => void) | undefined;
|
|
35
|
-
/**
|
|
36
|
+
/** Restore the file last viewed in this task. Invalid/missing paths still
|
|
37
|
+
* surface the ordinary file-view error instead of changing files silently. */
|
|
38
|
+
initialSelectedPath?: string | null | undefined;
|
|
39
|
+
onSelectedPathChange?: ((path: string | null) => void) | undefined;
|
|
40
|
+
/** A deliberate file-open path routed here by the parent workspace. */
|
|
36
41
|
requestedPath?: string | undefined;
|
|
42
|
+
/** 1-based line to reveal after `requestedPath` opens. */
|
|
43
|
+
requestedLine?: number | null | undefined;
|
|
37
44
|
/** Identity for one guarded-file request. Increment this when the same path is
|
|
38
45
|
* deliberately requested again; it also lets a pending request be consumed
|
|
39
46
|
* without overriding later manual tree navigation. Defaults to the path. */
|
|
@@ -67,7 +74,10 @@ export function SandboxFiles({
|
|
|
67
74
|
usePierre = true,
|
|
68
75
|
editable = true,
|
|
69
76
|
onEditIntent,
|
|
77
|
+
initialSelectedPath,
|
|
78
|
+
onSelectedPathChange,
|
|
70
79
|
requestedPath,
|
|
80
|
+
requestedLine,
|
|
71
81
|
requestedPathRequestId,
|
|
72
82
|
requestedPathReady = true,
|
|
73
83
|
workspaceResting = false,
|
|
@@ -77,12 +87,18 @@ export function SandboxFiles({
|
|
|
77
87
|
themeType,
|
|
78
88
|
className,
|
|
79
89
|
}: SandboxFilesProps) {
|
|
80
|
-
const [selected, setSelected] = useState<string | null>(null);
|
|
90
|
+
const [selected, setSelected] = useState<string | null>(() => initialSelectedPath ?? null);
|
|
81
91
|
// View vs Edit for the selected file. Resets to View on every new selection so
|
|
82
92
|
// opening a file never lands you in a stale dirty editor for a different path.
|
|
83
93
|
const [editMode, setEditMode] = useState(false);
|
|
94
|
+
const [focusLine, setFocusLine] = useState<number | null>(null);
|
|
95
|
+
const [treeRevealRequest, setTreeRevealRequest] = useState<{
|
|
96
|
+
path: string;
|
|
97
|
+
requestId: string | number;
|
|
98
|
+
} | null>(null);
|
|
84
99
|
const [liveRequestedPath, setLiveRequestedPath] = useState<string | null>(null);
|
|
85
100
|
const [viewReloadRevision, setViewReloadRevision] = useState(0);
|
|
101
|
+
const viewerScrollRef = useRef<HTMLDivElement | null>(null);
|
|
86
102
|
const pendingRequestRef = useRef<string | number | null>(null);
|
|
87
103
|
const handledRequestRef = useRef<string | number | null>(null);
|
|
88
104
|
const requestKey = requestedPath ? (requestedPathRequestId ?? requestedPath) : null;
|
|
@@ -91,6 +107,7 @@ export function SandboxFiles({
|
|
|
91
107
|
if (!requestedPath || requestKey === null) {
|
|
92
108
|
pendingRequestRef.current = null;
|
|
93
109
|
handledRequestRef.current = null;
|
|
110
|
+
setTreeRevealRequest(null);
|
|
94
111
|
return;
|
|
95
112
|
}
|
|
96
113
|
if (handledRequestRef.current === requestKey) {
|
|
@@ -98,13 +115,18 @@ export function SandboxFiles({
|
|
|
98
115
|
}
|
|
99
116
|
if (!requestedPathReady) {
|
|
100
117
|
pendingRequestRef.current = requestKey;
|
|
118
|
+
setTreeRevealRequest(null);
|
|
101
119
|
return;
|
|
102
120
|
}
|
|
103
121
|
handledRequestRef.current = requestKey;
|
|
104
122
|
pendingRequestRef.current = null;
|
|
105
123
|
setSelected(requestedPath);
|
|
124
|
+
onSelectedPathChange?.(requestedPath);
|
|
106
125
|
setEditMode(false);
|
|
107
|
-
|
|
126
|
+
setFocusLine(requestedLine != null && requestedLine > 0 ? requestedLine : null);
|
|
127
|
+
setTreeRevealRequest({ path: requestedPath, requestId: requestKey });
|
|
128
|
+
setViewReloadRevision((revision) => revision + 1);
|
|
129
|
+
}, [onSelectedPathChange, requestKey, requestedLine, requestedPath, requestedPathReady]);
|
|
108
130
|
|
|
109
131
|
// Side-by-side (tree left, viewer right) once the surface is wide enough;
|
|
110
132
|
// stacked (tree over viewer) on a narrow dock. Tracked off the container so it
|
|
@@ -147,23 +169,35 @@ export function SandboxFiles({
|
|
|
147
169
|
const fileView = useFileView(
|
|
148
170
|
viewPath,
|
|
149
171
|
files.readFile,
|
|
150
|
-
|
|
172
|
+
`${files.contentRevision ?? 0}:${viewReloadRevision}`,
|
|
151
173
|
);
|
|
152
174
|
|
|
175
|
+
useEffect(() => {
|
|
176
|
+
if (focusLine === null && viewReloadRevision > 0 && viewerScrollRef.current) {
|
|
177
|
+
viewerScrollRef.current.scrollTop = 0;
|
|
178
|
+
}
|
|
179
|
+
}, [focusLine, viewReloadRevision]);
|
|
180
|
+
|
|
153
181
|
// Selecting a (different) file always returns to View — never drop the user into
|
|
154
182
|
// an editor whose buffer belongs to the previously-selected path. Manual
|
|
155
183
|
// navigation also consumes a pending guarded-file request: a late cold→warm
|
|
156
184
|
// transition must never pull the user away from the file they chose meanwhile.
|
|
157
|
-
const selectFile = useCallback(
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
185
|
+
const selectFile = useCallback(
|
|
186
|
+
(path: string) => {
|
|
187
|
+
if (pendingRequestRef.current !== null) {
|
|
188
|
+
handledRequestRef.current = pendingRequestRef.current;
|
|
189
|
+
pendingRequestRef.current = null;
|
|
190
|
+
}
|
|
191
|
+
setSelected(path);
|
|
192
|
+
onSelectedPathChange?.(path);
|
|
193
|
+
setEditMode(false);
|
|
194
|
+
setFocusLine(null);
|
|
195
|
+
setTreeRevealRequest(null);
|
|
196
|
+
setLiveRequestedPath(null);
|
|
197
|
+
setViewReloadRevision(0);
|
|
198
|
+
},
|
|
199
|
+
[onSelectedPathChange],
|
|
200
|
+
);
|
|
167
201
|
|
|
168
202
|
// A tree file is editable only when it is a real, fully-loaded text file: not
|
|
169
203
|
// binary (would corrupt on save) and not truncated (we only hold a PREFIX). The
|
|
@@ -264,6 +298,12 @@ export function SandboxFiles({
|
|
|
264
298
|
<FileBrowser
|
|
265
299
|
result={files}
|
|
266
300
|
selectedPath={selected ?? undefined}
|
|
301
|
+
{...(treeRevealRequest
|
|
302
|
+
? {
|
|
303
|
+
revealPath: treeRevealRequest.path,
|
|
304
|
+
revealPathRequestId: treeRevealRequest.requestId,
|
|
305
|
+
}
|
|
306
|
+
: {})}
|
|
267
307
|
onSelectFile={selectFile}
|
|
268
308
|
editable={editable}
|
|
269
309
|
emptyState="This directory is empty"
|
|
@@ -295,7 +335,11 @@ export function SandboxFiles({
|
|
|
295
335
|
data-opengeni-selected-file
|
|
296
336
|
className="min-w-0 truncate font-og-mono text-og-xs text-og-fg-muted"
|
|
297
337
|
>
|
|
298
|
-
{selected
|
|
338
|
+
{selected
|
|
339
|
+
? focusLine !== null
|
|
340
|
+
? `${selected}:${focusLine}`
|
|
341
|
+
: selected
|
|
342
|
+
: "No file selected"}
|
|
299
343
|
</span>
|
|
300
344
|
{/* View/Edit toggle — only for a real, fully-loaded text file the editor
|
|
301
345
|
can safely round-trip. Binary/truncated/read-only files never get an
|
|
@@ -311,7 +355,11 @@ export function SandboxFiles({
|
|
|
311
355
|
/>
|
|
312
356
|
)}
|
|
313
357
|
</div>
|
|
314
|
-
<div
|
|
358
|
+
<div
|
|
359
|
+
ref={viewerScrollRef}
|
|
360
|
+
className="min-h-0 flex-1 overflow-auto"
|
|
361
|
+
data-opengeni-file-viewer-scroll
|
|
362
|
+
>
|
|
315
363
|
{viewPath ? (
|
|
316
364
|
showEditor && fileView.content !== null ? (
|
|
317
365
|
<CodeEditor
|
|
@@ -379,8 +427,9 @@ export function SandboxFiles({
|
|
|
379
427
|
loaded). Editing is disabled to avoid corrupting the file.
|
|
380
428
|
</div>
|
|
381
429
|
)}
|
|
382
|
-
{usePierre ? (
|
|
430
|
+
{usePierre && focusLine === null ? (
|
|
383
431
|
<PierreFile
|
|
432
|
+
key={`${viewPath}:${viewReloadRevision}`}
|
|
384
433
|
path={viewPath}
|
|
385
434
|
contents={fileView.content}
|
|
386
435
|
themeType={resolvedTheme}
|
|
@@ -391,6 +440,12 @@ export function SandboxFiles({
|
|
|
391
440
|
}
|
|
392
441
|
className="p-1"
|
|
393
442
|
/>
|
|
443
|
+
) : focusLine !== null ? (
|
|
444
|
+
<LineNumberedFile
|
|
445
|
+
path={viewPath}
|
|
446
|
+
contents={fileView.content}
|
|
447
|
+
focusLine={focusLine}
|
|
448
|
+
/>
|
|
394
449
|
) : (
|
|
395
450
|
<pre className="overflow-auto whitespace-pre p-2 font-og-mono text-og-sm text-og-fg">
|
|
396
451
|
{fileView.content}
|
|
@@ -438,6 +493,65 @@ function WakeButton({ children, onClick }: { children: ReactNode; onClick: () =>
|
|
|
438
493
|
);
|
|
439
494
|
}
|
|
440
495
|
|
|
496
|
+
function LineNumberedFile({
|
|
497
|
+
path,
|
|
498
|
+
contents,
|
|
499
|
+
focusLine,
|
|
500
|
+
}: {
|
|
501
|
+
path: string;
|
|
502
|
+
contents: string;
|
|
503
|
+
focusLine: number;
|
|
504
|
+
}) {
|
|
505
|
+
const lines = contents.split("\n");
|
|
506
|
+
const inRange = focusLine >= 1 && focusLine <= lines.length;
|
|
507
|
+
const targetRef = useRef<HTMLDivElement | null>(null);
|
|
508
|
+
|
|
509
|
+
useEffect(() => {
|
|
510
|
+
targetRef.current?.scrollIntoView({
|
|
511
|
+
block: "center",
|
|
512
|
+
behavior: prefersReducedMotion() ? "auto" : "smooth",
|
|
513
|
+
});
|
|
514
|
+
}, [path, focusLine, contents]);
|
|
515
|
+
|
|
516
|
+
return (
|
|
517
|
+
<div
|
|
518
|
+
className="h-full min-h-0 overflow-auto font-og-mono text-og-sm text-og-fg"
|
|
519
|
+
role="region"
|
|
520
|
+
aria-label={inRange ? `${path} at line ${focusLine}` : path}
|
|
521
|
+
>
|
|
522
|
+
{!inRange ? (
|
|
523
|
+
<p
|
|
524
|
+
className="border-b border-og-border bg-og-surface-1 px-2 py-1 text-og-xs text-og-fg-muted"
|
|
525
|
+
role="status"
|
|
526
|
+
>
|
|
527
|
+
Line {focusLine} is past the end of this file.
|
|
528
|
+
</p>
|
|
529
|
+
) : null}
|
|
530
|
+
<div>
|
|
531
|
+
{lines.map((text, index) => {
|
|
532
|
+
const lineNumber = index + 1;
|
|
533
|
+
const focused = inRange && lineNumber === focusLine;
|
|
534
|
+
return (
|
|
535
|
+
<div
|
|
536
|
+
key={lineNumber}
|
|
537
|
+
ref={focused ? targetRef : undefined}
|
|
538
|
+
data-opengeni-file-line={lineNumber}
|
|
539
|
+
{...(focused ? { "data-opengeni-focus-line": "" } : {})}
|
|
540
|
+
aria-current={focused ? "location" : undefined}
|
|
541
|
+
className={cn("flex gap-3 px-2 py-0.5 leading-6", focused && "bg-og-accent-soft")}
|
|
542
|
+
>
|
|
543
|
+
<span className="w-11 shrink-0 select-none text-right tabular-nums text-og-fg-subtle">
|
|
544
|
+
{lineNumber}
|
|
545
|
+
</span>
|
|
546
|
+
<pre className="min-w-0 flex-1 whitespace-pre-wrap break-words">{text || " "}</pre>
|
|
547
|
+
</div>
|
|
548
|
+
);
|
|
549
|
+
})}
|
|
550
|
+
</div>
|
|
551
|
+
</div>
|
|
552
|
+
);
|
|
553
|
+
}
|
|
554
|
+
|
|
441
555
|
function GitHeader({ git, loading }: { git: SandboxFilesGitSummary; loading: boolean }) {
|
|
442
556
|
if (loading) {
|
|
443
557
|
return (
|
|
@@ -542,7 +656,7 @@ type FileViewState = {
|
|
|
542
656
|
function useFileView(
|
|
543
657
|
path: string | null,
|
|
544
658
|
readFile: UseSandboxFilesResult["readFile"],
|
|
545
|
-
reloadRevision = 0,
|
|
659
|
+
reloadRevision: string | number = 0,
|
|
546
660
|
): FileViewState {
|
|
547
661
|
const previousPathRef = useRef<string | null>(null);
|
|
548
662
|
const [state, setState] = useState<FileViewState>({
|
|
@@ -671,7 +785,9 @@ function Notice({
|
|
|
671
785
|
</span>
|
|
672
786
|
) : null}
|
|
673
787
|
{title ? <p className="font-medium text-og-fg">{title}</p> : null}
|
|
674
|
-
<div className="leading-5">
|
|
788
|
+
<div data-contrast-audited className="leading-5">
|
|
789
|
+
{children}
|
|
790
|
+
</div>
|
|
675
791
|
</div>
|
|
676
792
|
</div>
|
|
677
793
|
);
|