@opengeni/react 2.5.0-canary.1 → 3.0.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 +15 -0
- package/dist/accounts.d.ts +64 -0
- package/dist/accounts.js +746 -0
- package/dist/accounts.js.map +1 -0
- package/dist/{browser-viewer-GXUICIAG.js → browser-viewer-AEFHUYZK.js} +3 -3
- package/dist/{chunk-MXJGO7LE.js → chunk-D3UIAJKY.js} +52 -2
- package/dist/chunk-D3UIAJKY.js.map +1 -0
- package/dist/{chunk-QGBJ6GHG.js → chunk-ELIYWBZR.js} +4 -4
- package/dist/{chunk-6H6S5V4Z.js → chunk-HQVCKCLG.js} +758 -871
- package/dist/chunk-HQVCKCLG.js.map +1 -0
- package/dist/{chunk-T7VAYG2I.js → chunk-MZ2G2XBA.js} +54 -2
- package/dist/chunk-MZ2G2XBA.js.map +1 -0
- package/dist/{chunk-X4JAFD5R.js → chunk-OAM4WAAM.js} +4 -4
- package/dist/{chunk-KSDXER4X.js → chunk-RGALCTWO.js} +64 -15
- package/dist/chunk-RGALCTWO.js.map +1 -0
- package/dist/chunk-YFKJ7VZH.js +268 -0
- package/dist/chunk-YFKJ7VZH.js.map +1 -0
- package/dist/{chunk-ATQJEWCA.js → chunk-YJFGAFBD.js} +124 -50
- package/dist/chunk-YJFGAFBD.js.map +1 -0
- package/dist/components/composer.d.ts +8 -0
- package/dist/components/file-browser.d.ts +4 -1
- package/dist/components/sandbox-files.d.ts +4 -1
- package/dist/components/sandbox-workspace.d.ts +5 -0
- package/dist/composer.js +3 -3
- package/dist/{computer-viewer-ZOP4Q7A7.js → computer-viewer-FWEZISW4.js} +3 -3
- package/dist/embedded-session-client.d.ts +23 -0
- package/dist/file-node-visibility.d.ts +15 -0
- package/dist/hooks/use-file-attachments.d.ts +9 -2
- package/dist/human-input.d.ts +3 -1
- package/dist/index.d.ts +5 -1
- package/dist/index.js +139 -53
- package/dist/index.js.map +1 -1
- package/dist/interaction.js +3 -3
- package/dist/session-ui.js +4 -4
- package/dist/session.d.ts +2 -0
- package/dist/session.js +7 -5
- package/dist/timeline/index.d.ts +1 -0
- package/dist/timeline/screenshot-lightbox.d.ts +16 -1
- package/package.json +6 -2
- package/src/accounts.tsx +970 -0
- package/src/components/chat-composer.tsx +88 -84
- package/src/components/composer.tsx +61 -9
- package/src/components/file-browser.tsx +33 -20
- package/src/components/human-input-surface.tsx +10 -6
- package/src/components/message-timeline.tsx +37 -9
- package/src/components/sandbox-files.tsx +42 -2
- package/src/components/sandbox-workspace.tsx +10 -0
- package/src/embedded-session-client.ts +104 -0
- package/src/file-node-visibility.ts +66 -0
- package/src/hooks/use-composer.ts +64 -2
- package/src/hooks/use-file-attachments.ts +38 -6
- package/src/hooks/use-human-input.ts +31 -4
- package/src/human-input.ts +12 -1
- package/src/index.ts +12 -0
- package/src/session.ts +5 -0
- package/src/timeline/index.ts +1 -0
- package/src/timeline/screenshot-lightbox.tsx +93 -29
- package/styles/compiled.css +11 -1
- package/styles/tokens.css +3 -1
- package/dist/chunk-6H6S5V4Z.js.map +0 -1
- package/dist/chunk-ATQJEWCA.js.map +0 -1
- package/dist/chunk-FK6VG4LL.js +0 -90
- package/dist/chunk-FK6VG4LL.js.map +0 -1
- package/dist/chunk-KSDXER4X.js.map +0 -1
- package/dist/chunk-MXJGO7LE.js.map +0 -1
- package/dist/chunk-T7VAYG2I.js.map +0 -1
- /package/dist/{browser-viewer-GXUICIAG.js.map → browser-viewer-AEFHUYZK.js.map} +0 -0
- /package/dist/{chunk-QGBJ6GHG.js.map → chunk-ELIYWBZR.js.map} +0 -0
- /package/dist/{chunk-X4JAFD5R.js.map → chunk-OAM4WAAM.js.map} +0 -0
- /package/dist/{computer-viewer-ZOP4Q7A7.js.map → computer-viewer-FWEZISW4.js.map} +0 -0
|
@@ -148,90 +148,94 @@ export function ChatComposer({
|
|
|
148
148
|
const stackActions = hasControls && Boolean(actionsStart);
|
|
149
149
|
|
|
150
150
|
return (
|
|
151
|
-
|
|
152
|
-
<
|
|
153
|
-
<
|
|
154
|
-
|
|
155
|
-
<
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
<
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
.
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
) : null}
|
|
195
|
-
{models ? (
|
|
196
|
-
<motion.span
|
|
197
|
-
layout
|
|
198
|
-
transition={{ duration: 0.36, ease: [0.22, 1, 0.36, 1] }}
|
|
199
|
-
className="inline-flex min-w-0"
|
|
200
|
-
>
|
|
201
|
-
<ModelPicker
|
|
202
|
-
models={models}
|
|
203
|
-
value={selectedModel}
|
|
204
|
-
onChange={onSelectModel}
|
|
151
|
+
<>
|
|
152
|
+
<Root controller={controller} responsiveBasis={responsiveBasis} className={className}>
|
|
153
|
+
<Frame>
|
|
154
|
+
<CommandPalette />
|
|
155
|
+
<Surface>
|
|
156
|
+
<PausedState />
|
|
157
|
+
<RestoredResources />
|
|
158
|
+
<Attachments />
|
|
159
|
+
{header}
|
|
160
|
+
{composer.annotations && composer.annotations.length > 0 ? (
|
|
161
|
+
<div className="px-3 pt-2 sm:px-4">
|
|
162
|
+
<TimelineAnnotationsChip
|
|
163
|
+
annotations={composer.annotations}
|
|
164
|
+
editable
|
|
165
|
+
focusAnnotationId={composer.annotationReviewTargetId}
|
|
166
|
+
onFocusConsumed={composer.clearAnnotationReviewTarget}
|
|
167
|
+
onUpdate={composer.updateAnnotation}
|
|
168
|
+
onRemove={composer.removeAnnotation}
|
|
169
|
+
/>
|
|
170
|
+
</div>
|
|
171
|
+
) : null}
|
|
172
|
+
<Input placeholder={placeholder} autoFocus={autoFocus} />
|
|
173
|
+
{controller.confirmState ? (
|
|
174
|
+
<Confirmation />
|
|
175
|
+
) : (
|
|
176
|
+
<Footer
|
|
177
|
+
data-og-stack-actions={stackActions ? "" : undefined}
|
|
178
|
+
className={stackActions ? "max-sm:flex-nowrap sm:flex-wrap" : undefined}
|
|
179
|
+
>
|
|
180
|
+
<LayoutGroup id="og-composer-footer">
|
|
181
|
+
{hasControls ? (
|
|
182
|
+
<Controls
|
|
183
|
+
className={stackActions ? "min-w-0 max-sm:flex-1 sm:w-auto" : undefined}
|
|
184
|
+
>
|
|
185
|
+
{controlsLeading}
|
|
186
|
+
<AttachButton className={attachButtonClassName} />
|
|
187
|
+
{transcription ? (
|
|
188
|
+
<ComposerTranscriptionControl
|
|
189
|
+
{...transcription}
|
|
190
|
+
suppressed={transcriptionSuppressed}
|
|
191
|
+
className={[transcription.className, transcriptionClassName]
|
|
192
|
+
.filter(Boolean)
|
|
193
|
+
.join(" ")}
|
|
205
194
|
/>
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
195
|
+
) : null}
|
|
196
|
+
{models ? (
|
|
197
|
+
<motion.span
|
|
198
|
+
layout
|
|
199
|
+
transition={{ duration: 0.36, ease: [0.22, 1, 0.36, 1] }}
|
|
200
|
+
className="inline-flex min-w-0"
|
|
201
|
+
>
|
|
202
|
+
<ModelPicker
|
|
203
|
+
models={models}
|
|
204
|
+
value={selectedModel}
|
|
205
|
+
onChange={onSelectModel}
|
|
206
|
+
/>
|
|
207
|
+
</motion.span>
|
|
208
|
+
) : null}
|
|
209
|
+
{controlsStart ? (
|
|
210
|
+
<motion.span
|
|
211
|
+
layout
|
|
212
|
+
transition={{ duration: 0.36, ease: [0.22, 1, 0.36, 1] }}
|
|
213
|
+
className="inline-flex min-w-0 flex-1 items-center gap-1.5"
|
|
214
|
+
>
|
|
215
|
+
{controlsStart}
|
|
216
|
+
</motion.span>
|
|
217
|
+
) : null}
|
|
218
|
+
</Controls>
|
|
219
|
+
) : (
|
|
220
|
+
<Hint>{hint}</Hint>
|
|
221
|
+
)}
|
|
222
|
+
<Actions
|
|
223
|
+
className={
|
|
224
|
+
stackActions ? "max-sm:shrink-0 sm:w-auto sm:justify-end" : undefined
|
|
225
|
+
}
|
|
226
|
+
>
|
|
227
|
+
{actionsStart}
|
|
228
|
+
<PauseButton />
|
|
229
|
+
<SendButton />
|
|
230
|
+
</Actions>
|
|
231
|
+
</LayoutGroup>
|
|
232
|
+
</Footer>
|
|
233
|
+
)}
|
|
234
|
+
</Surface>
|
|
235
|
+
</Frame>
|
|
236
|
+
<Help />
|
|
237
|
+
<Status />
|
|
238
|
+
</Root>
|
|
239
|
+
</>
|
|
236
240
|
);
|
|
237
241
|
}
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
useContext,
|
|
21
21
|
useEffect,
|
|
22
22
|
useId,
|
|
23
|
+
useLayoutEffect,
|
|
23
24
|
useMemo,
|
|
24
25
|
useRef,
|
|
25
26
|
useState,
|
|
@@ -51,6 +52,7 @@ import {
|
|
|
51
52
|
} from "../lib/composer-responsive-context";
|
|
52
53
|
import { composerSubmissionErrorMessage, formatBytes, formatRelativeTime } from "../lib/format";
|
|
53
54
|
import type { PickerModelRow } from "../model-policy";
|
|
55
|
+
import { useLightboxOptional } from "../timeline/screenshot-lightbox";
|
|
54
56
|
import { OPEN_WORKSTREAM_CONTROL_EVENT } from "../workstream-control-event";
|
|
55
57
|
import { CommandPalette as CommandPaletteView } from "./command-palette";
|
|
56
58
|
import { ModelPicker as ModelPickerView } from "./model-picker";
|
|
@@ -134,6 +136,10 @@ export type ChatComposerMessages = {
|
|
|
134
136
|
removeRestoredResource: (index: number) => string;
|
|
135
137
|
uploading: string;
|
|
136
138
|
uploadFailed: string;
|
|
139
|
+
previewAttachment: (name: string) => string;
|
|
140
|
+
attachmentPreviewLabel: string;
|
|
141
|
+
downloadAttachment: (name: string) => string;
|
|
142
|
+
closeAttachmentPreview: string;
|
|
137
143
|
retryAttachment: (name: string) => string;
|
|
138
144
|
retryUpload: string;
|
|
139
145
|
removeAttachment: (name: string) => string;
|
|
@@ -197,6 +203,10 @@ export const defaultChatComposerMessages: ChatComposerMessages = {
|
|
|
197
203
|
removeRestoredResource: (index) => `Remove restored resource ${index + 1}`,
|
|
198
204
|
uploading: "Uploading",
|
|
199
205
|
uploadFailed: "Upload failed",
|
|
206
|
+
previewAttachment: (name) => `Preview ${name}`,
|
|
207
|
+
attachmentPreviewLabel: "Attachment preview",
|
|
208
|
+
downloadAttachment: (name) => `Download ${name}`,
|
|
209
|
+
closeAttachmentPreview: "Close",
|
|
200
210
|
retryAttachment: (name) => `Retry ${name}`,
|
|
201
211
|
retryUpload: "Retry upload",
|
|
202
212
|
removeAttachment: (name) => `Remove ${name}`,
|
|
@@ -366,21 +376,32 @@ export function useChatComposerController({
|
|
|
366
376
|
const [submitting, setSubmitting] = useState(false);
|
|
367
377
|
const submittingRef = useRef(false);
|
|
368
378
|
const controlOperationRef = useRef(false);
|
|
379
|
+
const deliveryValue = delivery.value;
|
|
380
|
+
const setDeliveryValue = delivery.setValue;
|
|
369
381
|
const mountedRef = useRef(true);
|
|
370
|
-
const deliveredValueRef = useRef(
|
|
371
|
-
const liveValueRef = useRef(
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
382
|
+
const deliveredValueRef = useRef(deliveryValue);
|
|
383
|
+
const liveValueRef = useRef(deliveryValue);
|
|
384
|
+
const [renderedValue, setRenderedValue] = useState(deliveryValue);
|
|
385
|
+
const deliveryChanged = deliveryValue !== deliveredValueRef.current;
|
|
386
|
+
if (deliveryChanged) {
|
|
387
|
+
deliveredValueRef.current = deliveryValue;
|
|
388
|
+
liveValueRef.current = deliveryValue;
|
|
375
389
|
}
|
|
376
390
|
const setComposerValue = useCallback(
|
|
377
391
|
(next: string) => {
|
|
378
392
|
liveValueRef.current = next;
|
|
379
|
-
|
|
393
|
+
setRenderedValue(next);
|
|
394
|
+
setDeliveryValue(next);
|
|
380
395
|
},
|
|
381
|
-
[
|
|
396
|
+
[setDeliveryValue],
|
|
382
397
|
);
|
|
383
|
-
const visibleValue =
|
|
398
|
+
const visibleValue = deliveryChanged ? deliveryValue : renderedValue;
|
|
399
|
+
|
|
400
|
+
useLayoutEffect(() => {
|
|
401
|
+
if (renderedValue !== liveValueRef.current) {
|
|
402
|
+
setRenderedValue(liveValueRef.current);
|
|
403
|
+
}
|
|
404
|
+
}, [deliveryValue, renderedValue]);
|
|
384
405
|
|
|
385
406
|
useEffect(() => {
|
|
386
407
|
mountedRef.current = true;
|
|
@@ -925,6 +946,7 @@ export function Attachments() {
|
|
|
925
946
|
messages={controller.messages}
|
|
926
947
|
onRemove={controller.attachments.remove}
|
|
927
948
|
onRetry={controller.attachments.retry}
|
|
949
|
+
onRetainPreview={controller.attachments.retainPreview}
|
|
928
950
|
/>
|
|
929
951
|
);
|
|
930
952
|
}
|
|
@@ -1615,12 +1637,15 @@ function AttachmentChips({
|
|
|
1615
1637
|
messages,
|
|
1616
1638
|
onRemove,
|
|
1617
1639
|
onRetry,
|
|
1640
|
+
onRetainPreview,
|
|
1618
1641
|
}: {
|
|
1619
1642
|
attachments: UseFileAttachmentsResult["attachments"];
|
|
1620
1643
|
messages: ChatComposerMessages;
|
|
1621
1644
|
onRemove: (id: string) => void;
|
|
1622
1645
|
onRetry?: ((id: string) => void) | undefined;
|
|
1646
|
+
onRetainPreview: UseFileAttachmentsResult["retainPreview"];
|
|
1623
1647
|
}) {
|
|
1648
|
+
const lightbox = useLightboxOptional();
|
|
1624
1649
|
return (
|
|
1625
1650
|
<div className="flex flex-wrap gap-2 px-3 py-2">
|
|
1626
1651
|
{attachments.map((attachment) => {
|
|
@@ -1641,7 +1666,34 @@ function AttachmentChips({
|
|
|
1641
1666
|
: "border-og-border bg-og-surface-2",
|
|
1642
1667
|
)}
|
|
1643
1668
|
>
|
|
1644
|
-
{attachment.previewUrl ? (
|
|
1669
|
+
{attachment.previewUrl && lightbox ? (
|
|
1670
|
+
<button
|
|
1671
|
+
type="button"
|
|
1672
|
+
className="size-8 shrink-0 overflow-hidden rounded outline-hidden focus-visible:ring-2 focus-visible:ring-og-accent"
|
|
1673
|
+
aria-label={messages.previewAttachment(attachment.name)}
|
|
1674
|
+
onClick={(event) => {
|
|
1675
|
+
const releasePreview = onRetainPreview(attachment.id);
|
|
1676
|
+
lightbox.open(
|
|
1677
|
+
attachment.previewUrl!,
|
|
1678
|
+
attachment.name,
|
|
1679
|
+
event.currentTarget,
|
|
1680
|
+
messages.attachmentPreviewLabel,
|
|
1681
|
+
attachment.name,
|
|
1682
|
+
{
|
|
1683
|
+
download: messages.downloadAttachment(attachment.name),
|
|
1684
|
+
close: messages.closeAttachmentPreview,
|
|
1685
|
+
},
|
|
1686
|
+
releasePreview,
|
|
1687
|
+
);
|
|
1688
|
+
}}
|
|
1689
|
+
>
|
|
1690
|
+
<img
|
|
1691
|
+
src={attachment.previewUrl}
|
|
1692
|
+
alt=""
|
|
1693
|
+
className="h-full w-full object-cover transition-opacity hover:opacity-80"
|
|
1694
|
+
/>
|
|
1695
|
+
</button>
|
|
1696
|
+
) : attachment.previewUrl ? (
|
|
1645
1697
|
<img
|
|
1646
1698
|
src={attachment.previewUrl}
|
|
1647
1699
|
alt=""
|
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
} from "../lib/use-portal-token-style";
|
|
32
32
|
import { useUnicodeFallbackFonts } from "../lib/use-unicode-fonts";
|
|
33
33
|
import type { FileTreeNode, UseSandboxFilesResult } from "../hooks/use-sandbox-files";
|
|
34
|
+
import { visibleFileTree, type FileNodeVisibilityPredicate } from "../file-node-visibility";
|
|
34
35
|
|
|
35
36
|
export type FileBrowserProps = {
|
|
36
37
|
/** From `useSandboxFiles(...)`. */
|
|
@@ -43,6 +44,8 @@ export type FileBrowserProps = {
|
|
|
43
44
|
/** Selection callback for the preview pane. */
|
|
44
45
|
onSelectFile?: ((path: string) => void) | undefined;
|
|
45
46
|
selectedPath?: string | undefined;
|
|
47
|
+
/** Presentation-only node filter. Hidden parents never expose their children. */
|
|
48
|
+
isNodeVisible?: FileNodeVisibilityPredicate | undefined;
|
|
46
49
|
/** Deliberately reveal this selected path by lazily opening every loaded
|
|
47
50
|
* ancestor and scrolling its row into view. The path is opaque and is never
|
|
48
51
|
* normalized or re-rooted here. */
|
|
@@ -183,6 +186,7 @@ export function FileBrowser({
|
|
|
183
186
|
fallback,
|
|
184
187
|
onSelectFile,
|
|
185
188
|
selectedPath,
|
|
189
|
+
isNodeVisible,
|
|
186
190
|
revealPath,
|
|
187
191
|
revealPathRequestId,
|
|
188
192
|
renderNode,
|
|
@@ -220,6 +224,10 @@ export function FileBrowser({
|
|
|
220
224
|
() => (typeof document === "undefined" ? null : document.getElementById(treeId)),
|
|
221
225
|
[treeId],
|
|
222
226
|
);
|
|
227
|
+
const visibleTree = useMemo(
|
|
228
|
+
() => visibleFileTree(result.tree, isNodeVisible),
|
|
229
|
+
[isNodeVisible, result.tree],
|
|
230
|
+
);
|
|
223
231
|
|
|
224
232
|
useBrowserLayoutEffect(() => {
|
|
225
233
|
if (typeof window === "undefined" || typeof window.matchMedia !== "function") return;
|
|
@@ -255,11 +263,16 @@ export function FileBrowser({
|
|
|
255
263
|
|
|
256
264
|
// The keyboard cursor: an explicitly-navigated node falls back to the
|
|
257
265
|
// externally-selected file so arrow keys pick up where the preview pane is.
|
|
258
|
-
const
|
|
266
|
+
const visibleActive = active && findNode(visibleTree, active) ? active : null;
|
|
267
|
+
const cursor =
|
|
268
|
+
visibleActive ?? (selectedPath && findNode(visibleTree, selectedPath) ? selectedPath : null);
|
|
269
|
+
useEffect(() => {
|
|
270
|
+
if (active !== null && visibleActive === null) setActive(null);
|
|
271
|
+
}, [active, visibleActive]);
|
|
259
272
|
|
|
260
273
|
const expand = useCallback(
|
|
261
274
|
async (path: string) => {
|
|
262
|
-
const node = findNode(
|
|
275
|
+
const node = findNode(visibleTree, path);
|
|
263
276
|
if (node && node.kind === "dir" && node.children === undefined) {
|
|
264
277
|
setLoadingPaths((prev) => new Set(prev).add(path));
|
|
265
278
|
try {
|
|
@@ -273,7 +286,7 @@ export function FileBrowser({
|
|
|
273
286
|
}
|
|
274
287
|
}
|
|
275
288
|
},
|
|
276
|
-
[result],
|
|
289
|
+
[result, visibleTree],
|
|
277
290
|
);
|
|
278
291
|
|
|
279
292
|
const open = useCallback(
|
|
@@ -286,7 +299,7 @@ export function FileBrowser({
|
|
|
286
299
|
|
|
287
300
|
useEffect(() => {
|
|
288
301
|
if (!revealRequest) return;
|
|
289
|
-
const trace = traceRevealPath(
|
|
302
|
+
const trace = traceRevealPath(visibleTree, revealRequest.path);
|
|
290
303
|
if (trace.directories.length > 0) {
|
|
291
304
|
setExpanded((previous) => {
|
|
292
305
|
const next = new Set(previous);
|
|
@@ -310,7 +323,7 @@ export function FileBrowser({
|
|
|
310
323
|
}
|
|
311
324
|
attemptedRevealPathsRef.current.add(frontier);
|
|
312
325
|
void expand(frontier);
|
|
313
|
-
}, [expand, loadingPaths, result.expandingPaths,
|
|
326
|
+
}, [expand, loadingPaths, result.expandingPaths, revealRequest, visibleTree]);
|
|
314
327
|
|
|
315
328
|
const toggle = useCallback(
|
|
316
329
|
async (node: FileTreeNode) => {
|
|
@@ -362,9 +375,9 @@ export function FileBrowser({
|
|
|
362
375
|
}
|
|
363
376
|
}
|
|
364
377
|
};
|
|
365
|
-
walk(
|
|
378
|
+
walk(visibleTree, 0);
|
|
366
379
|
return items;
|
|
367
|
-
}, [
|
|
380
|
+
}, [visibleTree, expanded, draftCreate, loadingPaths, result.expandingPaths, cold]);
|
|
368
381
|
const unicodeFontPaths = useMemo(
|
|
369
382
|
() => renderItems.flatMap((item) => (item.type === "node" ? [item.node.path] : [])),
|
|
370
383
|
[renderItems],
|
|
@@ -391,9 +404,9 @@ export function FileBrowser({
|
|
|
391
404
|
if (node.children) visit(node.children);
|
|
392
405
|
});
|
|
393
406
|
};
|
|
394
|
-
visit(
|
|
407
|
+
visit(visibleTree);
|
|
395
408
|
return positions;
|
|
396
|
-
}, [
|
|
409
|
+
}, [visibleTree]);
|
|
397
410
|
const cursorIndex = cursor ? (rowIndexByPath.get(cursor) ?? -1) : -1;
|
|
398
411
|
const activeDescendantId = cursorIndex >= 0 ? `${treeId}-item-${cursorIndex}` : undefined;
|
|
399
412
|
|
|
@@ -485,7 +498,7 @@ export function FileBrowser({
|
|
|
485
498
|
const draft = draftRename;
|
|
486
499
|
setDraftRename(null);
|
|
487
500
|
if (!draft || !supportsMutation) return;
|
|
488
|
-
const node = findNode(
|
|
501
|
+
const node = findNode(visibleTree, draft.path);
|
|
489
502
|
const trimmed = name.trim().replace(/\/+$/, "");
|
|
490
503
|
if (!node || !trimmed || trimmed === node.name) return;
|
|
491
504
|
const newPath = joinPath(parentOf(draft.path), trimmed);
|
|
@@ -502,21 +515,21 @@ export function FileBrowser({
|
|
|
502
515
|
setBusy(false);
|
|
503
516
|
}
|
|
504
517
|
},
|
|
505
|
-
[draftRename, supportsMutation, result, selectedPath, active, onSelectFile],
|
|
518
|
+
[draftRename, supportsMutation, result, selectedPath, active, onSelectFile, visibleTree],
|
|
506
519
|
);
|
|
507
520
|
|
|
508
521
|
// Begin a new file/folder under the active node's directory (or root).
|
|
509
522
|
const startCreate = useCallback(
|
|
510
523
|
(kind: "file" | "dir") => {
|
|
511
524
|
if (!supportsMutation) return;
|
|
512
|
-
const anchor = cursor ? findNode(
|
|
525
|
+
const anchor = cursor ? findNode(visibleTree, cursor) : undefined;
|
|
513
526
|
const parent = anchor ? (anchor.kind === "dir" ? anchor.path : parentOf(anchor.path)) : "";
|
|
514
527
|
if (parent) open(parent);
|
|
515
528
|
setDraftRename(null);
|
|
516
529
|
setMenu(null);
|
|
517
530
|
setDraftCreate({ parent, kind });
|
|
518
531
|
},
|
|
519
|
-
[supportsMutation, cursor,
|
|
532
|
+
[supportsMutation, cursor, open, visibleTree],
|
|
520
533
|
);
|
|
521
534
|
|
|
522
535
|
const startRename = useCallback(
|
|
@@ -534,7 +547,7 @@ export function FileBrowser({
|
|
|
534
547
|
async (targetDir: string, sourcePath: string) => {
|
|
535
548
|
setDragOver(null);
|
|
536
549
|
if (!supportsMutation || !sourcePath) return;
|
|
537
|
-
const src = findNode(
|
|
550
|
+
const src = findNode(visibleTree, sourcePath);
|
|
538
551
|
if (!src) return;
|
|
539
552
|
// No-op drops: onto its own current parent, onto itself, or into its own subtree.
|
|
540
553
|
if (parentOf(sourcePath) === targetDir) return;
|
|
@@ -554,7 +567,7 @@ export function FileBrowser({
|
|
|
554
567
|
setBusy(false);
|
|
555
568
|
}
|
|
556
569
|
},
|
|
557
|
-
[supportsMutation, result, selectedPath, active, onSelectFile],
|
|
570
|
+
[supportsMutation, result, selectedPath, active, onSelectFile, visibleTree],
|
|
558
571
|
);
|
|
559
572
|
|
|
560
573
|
const onKeyDown = useCallback(
|
|
@@ -691,9 +704,9 @@ export function FileBrowser({
|
|
|
691
704
|
};
|
|
692
705
|
}, [menu]);
|
|
693
706
|
|
|
694
|
-
const showErrorEmpty = result.error &&
|
|
695
|
-
const showDegradedTree = result.error &&
|
|
696
|
-
const showEmpty = !result.loading &&
|
|
707
|
+
const showErrorEmpty = result.error && visibleTree.length === 0 && !draftCreate;
|
|
708
|
+
const showDegradedTree = result.error && visibleTree.length > 0;
|
|
709
|
+
const showEmpty = !result.loading && visibleTree.length === 0 && !draftCreate;
|
|
697
710
|
// Retry lives next to the state it explains (the degraded/empty notice), so do
|
|
698
711
|
// not render a second toolbar retry for the same failure.
|
|
699
712
|
const showToolbar = supportsMutation || result.loading;
|
|
@@ -934,7 +947,7 @@ export function FileBrowser({
|
|
|
934
947
|
<ToolbarButton
|
|
935
948
|
label="Rename"
|
|
936
949
|
onClick={() => {
|
|
937
|
-
const node = cursor ? findNode(
|
|
950
|
+
const node = cursor ? findNode(visibleTree, cursor) : undefined;
|
|
938
951
|
if (node) startRename(node);
|
|
939
952
|
}}
|
|
940
953
|
disabled={busy || !cursor}
|
|
@@ -944,7 +957,7 @@ export function FileBrowser({
|
|
|
944
957
|
<ToolbarButton
|
|
945
958
|
label="Delete"
|
|
946
959
|
onClick={(event) => {
|
|
947
|
-
const node = cursor ? findNode(
|
|
960
|
+
const node = cursor ? findNode(visibleTree, cursor) : undefined;
|
|
948
961
|
if (node) void runDelete(node, event.currentTarget);
|
|
949
962
|
}}
|
|
950
963
|
disabled={busy || !cursor}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { SessionHumanInputRequest, SubmitHumanInputResponseRequest } from "@opengeni/sdk";
|
|
2
2
|
import { useEffect, useMemo, useRef } from "react";
|
|
3
|
+
import { isActionableHumanInputRequest } from "../human-input";
|
|
3
4
|
import { cn } from "../lib/cn";
|
|
4
5
|
import {
|
|
5
6
|
HumanInputForm,
|
|
@@ -36,12 +37,15 @@ export function HumanInputSurface({
|
|
|
36
37
|
const batchTotalRef = useRef(0);
|
|
37
38
|
|
|
38
39
|
const ordered = useMemo(() => {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
40
|
+
const now = Date.now();
|
|
41
|
+
return requests
|
|
42
|
+
.filter((request) => isActionableHumanInputRequest(request, now))
|
|
43
|
+
.sort((a, b) => {
|
|
44
|
+
const aAt = a.createdAt ? Date.parse(a.createdAt) : 0;
|
|
45
|
+
const bAt = b.createdAt ? Date.parse(b.createdAt) : 0;
|
|
46
|
+
if (aAt !== bAt) return aAt - bAt;
|
|
47
|
+
return a.id.localeCompare(b.id);
|
|
48
|
+
});
|
|
45
49
|
}, [requests]);
|
|
46
50
|
|
|
47
51
|
useEffect(() => {
|
|
@@ -237,6 +237,13 @@ export type MessageTimelineProps = {
|
|
|
237
237
|
* alone is wrong. Late layout while unpinned stays browser-owned.
|
|
238
238
|
*/
|
|
239
239
|
const PIN_THRESHOLD_PX = 48;
|
|
240
|
+
/**
|
|
241
|
+
* A pinned timeline can still have meaningful pre-existing debt while the
|
|
242
|
+
* tip-follow camera catches up. Surface the existing explicit jump once that
|
|
243
|
+
* debt is large enough to be useful, without flashing the control for ordinary
|
|
244
|
+
* line-sized streaming movement.
|
|
245
|
+
*/
|
|
246
|
+
const JUMP_TO_LATEST_CATCHUP_DEBT_PX = 240;
|
|
240
247
|
/**
|
|
241
248
|
* Prefetch older history when the top sentinel is this far from the viewport.
|
|
242
249
|
* After a page loads we stay cool until the reader leaves this band (scrolls
|
|
@@ -432,6 +439,8 @@ export function MessageTimeline({
|
|
|
432
439
|
const bottomSentinelRef = useRef<HTMLDivElement | null>(null);
|
|
433
440
|
const previousBulkFirstKeyRef = useRef<string | null | undefined>(undefined);
|
|
434
441
|
const [pinned, setPinned] = useState(true);
|
|
442
|
+
const [canSkipTipCatchup, setCanSkipTipCatchup] = useState(false);
|
|
443
|
+
const canSkipTipCatchupRef = useRef(false);
|
|
435
444
|
const [bulkActive, setBulkActive] = useState(true);
|
|
436
445
|
// Older history prefetch is user-driven: a window shorter than the viewport
|
|
437
446
|
// + rootMargin would otherwise keep the top sentinel intersecting and fetch
|
|
@@ -545,15 +554,28 @@ export function MessageTimeline({
|
|
|
545
554
|
previousBulkFirstKeyRef.current !== firstGroupKey;
|
|
546
555
|
const bulkRender = allGroups.length > 0 && (bulkActive || firstKeyChangedForBulk);
|
|
547
556
|
|
|
557
|
+
const applyCanSkipTipCatchup = useCallback((value: boolean) => {
|
|
558
|
+
if (canSkipTipCatchupRef.current !== value) {
|
|
559
|
+
canSkipTipCatchupRef.current = value;
|
|
560
|
+
setCanSkipTipCatchup(value);
|
|
561
|
+
}
|
|
562
|
+
}, []);
|
|
563
|
+
|
|
548
564
|
// The ONLY writer of the pinned flag. Ref and state move together, so
|
|
549
565
|
// behavior (refs read by rAF callbacks) and rendering (the anchor class,
|
|
550
566
|
// the Jump-to-latest button) can never desync.
|
|
551
|
-
const applyPinned = useCallback(
|
|
552
|
-
|
|
553
|
-
pinnedRef.current
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
567
|
+
const applyPinned = useCallback(
|
|
568
|
+
(value: boolean) => {
|
|
569
|
+
if (pinnedRef.current !== value) {
|
|
570
|
+
pinnedRef.current = value;
|
|
571
|
+
setPinned(value);
|
|
572
|
+
}
|
|
573
|
+
if (!value) {
|
|
574
|
+
applyCanSkipTipCatchup(false);
|
|
575
|
+
}
|
|
576
|
+
},
|
|
577
|
+
[applyCanSkipTipCatchup],
|
|
578
|
+
);
|
|
557
579
|
|
|
558
580
|
const rearmOlderPrefetchAfterLeavingTop = useCallback((node: HTMLElement) => {
|
|
559
581
|
if (node.scrollTop > OLDER_PREFETCH_MARGIN_PX) {
|
|
@@ -757,8 +779,9 @@ export function MessageTimeline({
|
|
|
757
779
|
lastClientHeight: node.clientHeight,
|
|
758
780
|
cameraTop: null,
|
|
759
781
|
};
|
|
782
|
+
applyCanSkipTipCatchup(false);
|
|
760
783
|
},
|
|
761
|
-
[cancelLeaveFallback, stopFollow, syncScrollBaseline, writeScrollTop],
|
|
784
|
+
[applyCanSkipTipCatchup, cancelLeaveFallback, stopFollow, syncScrollBaseline, writeScrollTop],
|
|
762
785
|
);
|
|
763
786
|
|
|
764
787
|
const beginUserMessageDisclosureChange = useCallback(
|
|
@@ -965,6 +988,10 @@ export function MessageTimeline({
|
|
|
965
988
|
followRef.current = result.state;
|
|
966
989
|
writeScrollTop(node, result.scrollTop);
|
|
967
990
|
syncScrollBaseline(node);
|
|
991
|
+
applyCanSkipTipCatchup(
|
|
992
|
+
result.state.running &&
|
|
993
|
+
maxScrollOf(node) - node.scrollTop >= JUMP_TO_LATEST_CATCHUP_DEBT_PX,
|
|
994
|
+
);
|
|
968
995
|
if (result.state.running) {
|
|
969
996
|
cancelLeaveFallback();
|
|
970
997
|
if (followFrameRef.current == null) {
|
|
@@ -981,7 +1008,7 @@ export function MessageTimeline({
|
|
|
981
1008
|
followFrameRef.current = null;
|
|
982
1009
|
}
|
|
983
1010
|
},
|
|
984
|
-
[cancelLeaveFallback, stopFollow, syncScrollBaseline, writeScrollTop],
|
|
1011
|
+
[applyCanSkipTipCatchup, cancelLeaveFallback, stopFollow, syncScrollBaseline, writeScrollTop],
|
|
985
1012
|
);
|
|
986
1013
|
driveFollowRef.current = driveFollow;
|
|
987
1014
|
|
|
@@ -1825,9 +1852,10 @@ export function MessageTimeline({
|
|
|
1825
1852
|
) : null}
|
|
1826
1853
|
</AnimatePresence>
|
|
1827
1854
|
<AnimatePresence>
|
|
1828
|
-
{((!pinned && autoFollow) || hasNewer) && autoFollow ? (
|
|
1855
|
+
{((!pinned && autoFollow) || hasNewer || canSkipTipCatchup) && autoFollow ? (
|
|
1829
1856
|
<motion.button
|
|
1830
1857
|
type="button"
|
|
1858
|
+
data-og-jump-to-latest=""
|
|
1831
1859
|
initial={{ opacity: 0, y: 8 }}
|
|
1832
1860
|
animate={{ opacity: 1, y: 0 }}
|
|
1833
1861
|
exit={{ opacity: 0, y: 8 }}
|