@opengeni/react 3.1.0 → 3.3.2
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/dist/accounts.js +87 -11
- package/dist/accounts.js.map +1 -1
- package/dist/{browser-viewer-AEFHUYZK.js → browser-viewer-MKKFRRRV.js} +3 -3
- package/dist/{chunk-W6NFJXSA.js → chunk-2DZVYHVO.js} +20 -4
- package/dist/chunk-2DZVYHVO.js.map +1 -0
- package/dist/{chunk-YJFGAFBD.js → chunk-3AGBGTRT.js} +226 -95
- package/dist/chunk-3AGBGTRT.js.map +1 -0
- package/dist/{chunk-YFKJ7VZH.js → chunk-7EAFGICR.js} +45 -11
- package/dist/chunk-7EAFGICR.js.map +1 -0
- package/dist/{chunk-MZ2G2XBA.js → chunk-IOLVN74D.js} +31 -4
- package/dist/chunk-IOLVN74D.js.map +1 -0
- package/dist/{chunk-D3UIAJKY.js → chunk-JAQ5K7W6.js} +3 -2
- package/dist/{chunk-D3UIAJKY.js.map → chunk-JAQ5K7W6.js.map} +1 -1
- package/dist/{chunk-LAKLF4PA.js → chunk-VTOREEXF.js} +8 -7
- package/dist/chunk-VTOREEXF.js.map +1 -0
- package/dist/{chunk-RGALCTWO.js → chunk-XYRDDSXF.js} +4 -4
- package/dist/{chunk-HQVCKCLG.js → chunk-YG4QEQF5.js} +354 -255
- package/dist/chunk-YG4QEQF5.js.map +1 -0
- package/dist/{chunk-XXWH2JLC.js → chunk-YVCHAQZY.js} +2 -2
- package/dist/{chunk-OAM4WAAM.js → chunk-ZPBTI3TE.js} +2 -2
- package/dist/components/fleet-tile.d.ts +2 -2
- package/dist/components/model-policy-picker.d.ts +9 -0
- package/dist/components/tip-follow.d.ts +16 -0
- package/dist/composer.js +5 -5
- package/dist/hooks/use-session-events.d.ts +3 -3
- package/dist/index.js +76 -68
- package/dist/index.js.map +1 -1
- package/dist/interaction.js +2 -2
- package/dist/machines.js +2 -2
- package/dist/realtime.js +46 -7
- package/dist/realtime.js.map +1 -1
- package/dist/session-ui.js +4 -4
- package/dist/session.js +4 -4
- package/dist/timeline/entrance.d.ts +13 -1
- package/package.json +3 -2
- package/src/accounts.tsx +116 -14
- package/src/components/fleet-tile.tsx +4 -15
- package/src/components/message-timeline.tsx +237 -101
- package/src/components/model-policy-picker.tsx +24 -3
- package/src/components/tip-follow.ts +52 -0
- package/src/components/tooltip.tsx +38 -5
- package/src/hooks/use-available-models.ts +20 -14
- package/src/hooks/use-composer.ts +49 -3
- package/src/hooks/use-goal.ts +31 -7
- package/src/hooks/use-last-started-turn-policy.ts +11 -5
- package/src/hooks/use-session-events.ts +186 -61
- package/src/hooks/use-session-lineage.ts +20 -16
- package/src/hooks/use-session.ts +23 -19
- package/src/hooks/use-workspace-sessions.ts +31 -27
- package/src/lib/format.ts +8 -6
- package/src/realtime/realtime-control.tsx +55 -6
- package/src/timeline/entrance.tsx +28 -3
- package/src/timeline/projection.ts +4 -0
- package/dist/chunk-HQVCKCLG.js.map +0 -1
- package/dist/chunk-LAKLF4PA.js.map +0 -1
- package/dist/chunk-MZ2G2XBA.js.map +0 -1
- package/dist/chunk-W6NFJXSA.js.map +0 -1
- package/dist/chunk-YFKJ7VZH.js.map +0 -1
- package/dist/chunk-YJFGAFBD.js.map +0 -1
- /package/dist/{browser-viewer-AEFHUYZK.js.map → browser-viewer-MKKFRRRV.js.map} +0 -0
- /package/dist/{chunk-RGALCTWO.js.map → chunk-XYRDDSXF.js.map} +0 -0
- /package/dist/{chunk-XXWH2JLC.js.map → chunk-YVCHAQZY.js.map} +0 -0
- /package/dist/{chunk-OAM4WAAM.js.map → chunk-ZPBTI3TE.js.map} +0 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SessionEvent, SessionStatus, StreamConnectionState } from "@opengeni/sdk";
|
|
2
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
2
|
+
import { startTransition, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
3
|
import { useEmbeddedSession, type EmbeddedSessionClientOverride } from "../session-context";
|
|
4
4
|
import { createOlderHistoryLoadReceipt, type OlderHistoryLoadReceipt } from "../older-history";
|
|
5
5
|
import { buildTimeline, groupTimeline, sessionStatusFromEvents } from "../timeline/projection";
|
|
@@ -283,7 +283,10 @@ export function useSessionEvents(
|
|
|
283
283
|
batchResumeSequence,
|
|
284
284
|
);
|
|
285
285
|
}
|
|
286
|
-
observeSessionStatus(batch, sessionStatusRef
|
|
286
|
+
const status = observeSessionStatus(batch, sessionStatusRef);
|
|
287
|
+
if (status !== undefined) {
|
|
288
|
+
setSessionStatusProjection(status);
|
|
289
|
+
}
|
|
287
290
|
const current = eventWindowRef.current;
|
|
288
291
|
const next = boundBrowserSessionEventWindow([...current.events, ...batch]);
|
|
289
292
|
const retained = {
|
|
@@ -293,7 +296,7 @@ export function useSessionEvents(
|
|
|
293
296
|
eventWindowRef.current = retained;
|
|
294
297
|
setEventWindow(retained);
|
|
295
298
|
oldestSequenceRef.current = retained.events[0]?.sequence ?? null;
|
|
296
|
-
newestSequenceRef.current = retained.events
|
|
299
|
+
newestSequenceRef.current = maxResumeSequenceOrNull(retained.events);
|
|
297
300
|
if (retained.truncated) {
|
|
298
301
|
hasOlderRef.current = true;
|
|
299
302
|
setHasOlder(true);
|
|
@@ -321,17 +324,21 @@ export function useSessionEvents(
|
|
|
321
324
|
if (!isCurrent()) {
|
|
322
325
|
return;
|
|
323
326
|
}
|
|
324
|
-
observeSessionStatus(window.events, sessionStatusRef
|
|
327
|
+
const status = observeSessionStatus(window.events, sessionStatusRef);
|
|
325
328
|
const retained = boundBrowserSessionEventWindow(window.events);
|
|
326
329
|
eventWindowRef.current = retained;
|
|
327
|
-
setEventWindow(retained);
|
|
328
330
|
oldestSequenceRef.current = retained.events[0]?.sequence ?? window.oldestSequence;
|
|
329
|
-
newestSequenceRef.current =
|
|
331
|
+
newestSequenceRef.current =
|
|
332
|
+
maxResumeSequenceOrNull(retained.events) ?? window.newestSequence;
|
|
330
333
|
hasOlderRef.current = window.hasOlder || retained.truncated;
|
|
331
334
|
hasNewerRef.current = false;
|
|
332
335
|
lastSequenceRef.current = window.newestSequence;
|
|
333
336
|
streamResumeSequenceRef.current = window.newestSequence;
|
|
334
337
|
initialWindowLoadedRef.current = true;
|
|
338
|
+
if (status !== undefined) {
|
|
339
|
+
setSessionStatusProjection(status);
|
|
340
|
+
}
|
|
341
|
+
setEventWindow(retained);
|
|
335
342
|
setHasOlder(window.hasOlder || retained.truncated);
|
|
336
343
|
setHasNewer(false);
|
|
337
344
|
setInitialLoading(false);
|
|
@@ -393,6 +400,10 @@ export function useSessionEvents(
|
|
|
393
400
|
flush();
|
|
394
401
|
setError(cause instanceof Error ? cause : new Error(String(cause)));
|
|
395
402
|
setConnectionState("error");
|
|
403
|
+
// A failed first compact-tail request has no later success path in
|
|
404
|
+
// this effect instance. End the loading gate so hosts can render the
|
|
405
|
+
// retryable error instead of an infinite initial spinner.
|
|
406
|
+
setInitialLoading(false);
|
|
396
407
|
}
|
|
397
408
|
}
|
|
398
409
|
})();
|
|
@@ -444,6 +455,7 @@ export function useSessionEvents(
|
|
|
444
455
|
const generation = generationRef.current;
|
|
445
456
|
loadingOlderRef.current = true;
|
|
446
457
|
setLoadingOlder(true);
|
|
458
|
+
let published = false;
|
|
447
459
|
try {
|
|
448
460
|
const window = await loadEventWindow(client, workspaceId, sessionId, {
|
|
449
461
|
before,
|
|
@@ -466,7 +478,7 @@ export function useSessionEvents(
|
|
|
466
478
|
// pending in the aborted iterator were never cursor-committed and will
|
|
467
479
|
// be replayed from the retained high-water mark below.
|
|
468
480
|
streamAbortRef.current?.abort();
|
|
469
|
-
observeSessionStatus(window.events, sessionStatusRef
|
|
481
|
+
const status = observeSessionStatus(window.events, sessionStatusRef);
|
|
470
482
|
const next = boundBrowserSessionEventWindow([...window.events, ...current.events], {
|
|
471
483
|
direction: "oldest",
|
|
472
484
|
});
|
|
@@ -478,10 +490,10 @@ export function useSessionEvents(
|
|
|
478
490
|
if (retainedOldest === null || retainedOldest >= before) {
|
|
479
491
|
throw new Error("@opengeni/react: loadOlder made no durable sequence progress");
|
|
480
492
|
}
|
|
481
|
-
const retainedNewest = retained.events
|
|
493
|
+
const retainedNewest = maxResumeSequenceOrNull(retained.events);
|
|
494
|
+
const previousNewest = maxResumeSequenceOrNull(current.events);
|
|
482
495
|
eventWindowRef.current = retained;
|
|
483
496
|
markCommitted();
|
|
484
|
-
setEventWindow(retained);
|
|
485
497
|
oldestSequenceRef.current = retainedOldest;
|
|
486
498
|
newestSequenceRef.current = retainedNewest;
|
|
487
499
|
streamResumeSequenceRef.current = maxResumeSequence(retained.events);
|
|
@@ -490,22 +502,52 @@ export function useSessionEvents(
|
|
|
490
502
|
// exist; only the backward DB page can answer hasOlder truthfully.
|
|
491
503
|
const olderStillAvailable = window.hasOlder;
|
|
492
504
|
hasOlderRef.current = olderStillAvailable;
|
|
493
|
-
|
|
494
|
-
|
|
505
|
+
const evictedLiveTail =
|
|
506
|
+
previousNewest !== null && retainedNewest !== null && retainedNewest < previousNewest;
|
|
507
|
+
let newer: boolean | null = null;
|
|
508
|
+
let enterHistory = false;
|
|
509
|
+
let reconnectLive = false;
|
|
510
|
+
if (viewModeRef.current === "history" || evictedLiveTail) {
|
|
495
511
|
const highWater = lastSequenceRef.current;
|
|
496
|
-
|
|
497
|
-
retainedNewest !== null && (retainedNewest < highWater || retained.truncated);
|
|
512
|
+
newer = retainedNewest !== null && (retainedNewest < highWater || retained.truncated);
|
|
498
513
|
hasNewerRef.current = newer;
|
|
499
|
-
|
|
514
|
+
if (evictedLiveTail) {
|
|
515
|
+
// A full oldest-directed browser window cannot also retain the
|
|
516
|
+
// live suffix. Keep the newly loaded history and page forward;
|
|
517
|
+
// reconnecting SSE here immediately newest-bounds the window and
|
|
518
|
+
// evicts every row the reader just requested.
|
|
519
|
+
viewModeRef.current = "history";
|
|
520
|
+
enterHistory = true;
|
|
521
|
+
}
|
|
500
522
|
} else {
|
|
501
|
-
//
|
|
502
|
-
//
|
|
503
|
-
|
|
523
|
+
// The retained merge is still one contiguous live suffix. Restart
|
|
524
|
+
// SSE from its cursor so events queued during the fetch are replayed.
|
|
525
|
+
reconnectLive = true;
|
|
504
526
|
}
|
|
527
|
+
loadingOlderRef.current = false;
|
|
528
|
+
startTransition(() => {
|
|
529
|
+
if (status !== undefined) {
|
|
530
|
+
setSessionStatusProjection(status);
|
|
531
|
+
}
|
|
532
|
+
setEventWindow(retained);
|
|
533
|
+
setHasOlder(olderStillAvailable);
|
|
534
|
+
if (newer !== null) {
|
|
535
|
+
setHasNewer(newer);
|
|
536
|
+
}
|
|
537
|
+
if (enterHistory) {
|
|
538
|
+
setViewMode("history");
|
|
539
|
+
} else if (reconnectLive) {
|
|
540
|
+
setStreamEpoch((epoch) => epoch + 1);
|
|
541
|
+
}
|
|
542
|
+
setLoadingOlder(false);
|
|
543
|
+
});
|
|
544
|
+
published = true;
|
|
505
545
|
return olderStillAvailable;
|
|
506
546
|
} finally {
|
|
507
|
-
|
|
508
|
-
|
|
547
|
+
if (!published) {
|
|
548
|
+
loadingOlderRef.current = false;
|
|
549
|
+
setLoadingOlder(false);
|
|
550
|
+
}
|
|
509
551
|
}
|
|
510
552
|
}),
|
|
511
553
|
[client, workspaceId, sessionId],
|
|
@@ -518,6 +560,7 @@ export function useSessionEvents(
|
|
|
518
560
|
const generation = generationRef.current;
|
|
519
561
|
loadingOldestRef.current = true;
|
|
520
562
|
setLoadingOldest(true);
|
|
563
|
+
let published = false;
|
|
521
564
|
try {
|
|
522
565
|
const window = await loadForwardEventWindow(client, workspaceId, sessionId, {
|
|
523
566
|
after: 0,
|
|
@@ -534,17 +577,15 @@ export function useSessionEvents(
|
|
|
534
577
|
return false;
|
|
535
578
|
}
|
|
536
579
|
streamAbortRef.current?.abort();
|
|
537
|
-
observeSessionStatus(window.events, sessionStatusRef
|
|
580
|
+
const status = observeSessionStatus(window.events, sessionStatusRef);
|
|
538
581
|
// Keep the oldest prefix — never the middle or tip.
|
|
539
582
|
const retained = boundBrowserSessionEventWindow(window.events, { direction: "oldest" });
|
|
540
583
|
const retainedOldest = retained.events[0]?.sequence ?? null;
|
|
541
|
-
const retainedNewest = retained.events
|
|
584
|
+
const retainedNewest = maxResumeSequenceOrNull(retained.events);
|
|
542
585
|
eventWindowRef.current = retained;
|
|
543
|
-
setEventWindow(retained);
|
|
544
586
|
oldestSequenceRef.current = retainedOldest;
|
|
545
587
|
newestSequenceRef.current = retainedNewest;
|
|
546
588
|
hasOlderRef.current = false;
|
|
547
|
-
setHasOlder(false);
|
|
548
589
|
const highWater = Math.max(lastSequenceRef.current, retainedNewest ?? 0);
|
|
549
590
|
lastSequenceRef.current = highWater;
|
|
550
591
|
const newer =
|
|
@@ -552,14 +593,26 @@ export function useSessionEvents(
|
|
|
552
593
|
retained.truncated ||
|
|
553
594
|
(retainedNewest !== null && retainedNewest < highWater);
|
|
554
595
|
hasNewerRef.current = newer;
|
|
555
|
-
setHasNewer(newer);
|
|
556
596
|
initialWindowLoadedRef.current = true;
|
|
557
597
|
viewModeRef.current = "history";
|
|
558
|
-
|
|
598
|
+
loadingOldestRef.current = false;
|
|
599
|
+
startTransition(() => {
|
|
600
|
+
if (status !== undefined) {
|
|
601
|
+
setSessionStatusProjection(status);
|
|
602
|
+
}
|
|
603
|
+
setEventWindow(retained);
|
|
604
|
+
setHasOlder(false);
|
|
605
|
+
setHasNewer(newer);
|
|
606
|
+
setViewMode("history");
|
|
607
|
+
setLoadingOldest(false);
|
|
608
|
+
});
|
|
609
|
+
published = true;
|
|
559
610
|
return newer;
|
|
560
611
|
} finally {
|
|
561
|
-
|
|
562
|
-
|
|
612
|
+
if (!published) {
|
|
613
|
+
loadingOldestRef.current = false;
|
|
614
|
+
setLoadingOldest(false);
|
|
615
|
+
}
|
|
563
616
|
}
|
|
564
617
|
}, [client, workspaceId, sessionId]);
|
|
565
618
|
|
|
@@ -576,6 +629,7 @@ export function useSessionEvents(
|
|
|
576
629
|
const generation = generationRef.current;
|
|
577
630
|
loadingNewerRef.current = true;
|
|
578
631
|
setLoadingNewer(true);
|
|
632
|
+
let published = false;
|
|
579
633
|
try {
|
|
580
634
|
const window = await loadForwardEventWindow(client, workspaceId, sessionId, {
|
|
581
635
|
after: afterSequence,
|
|
@@ -598,7 +652,7 @@ export function useSessionEvents(
|
|
|
598
652
|
}
|
|
599
653
|
const current = eventWindowRef.current;
|
|
600
654
|
assertAppendOrder(current.events, window.events);
|
|
601
|
-
observeSessionStatus(window.events, sessionStatusRef
|
|
655
|
+
const status = observeSessionStatus(window.events, sessionStatusRef);
|
|
602
656
|
const previousOldest = current.events[0]?.sequence ?? null;
|
|
603
657
|
// Moving forward through history: keep the newest suffix of the merge.
|
|
604
658
|
const next = boundBrowserSessionEventWindow([...current.events, ...window.events], {
|
|
@@ -609,12 +663,11 @@ export function useSessionEvents(
|
|
|
609
663
|
truncated: current.truncated || next.truncated || window.hasNewer,
|
|
610
664
|
};
|
|
611
665
|
const retainedOldest = retained.events[0]?.sequence ?? null;
|
|
612
|
-
const retainedNewest = retained.events
|
|
666
|
+
const retainedNewest = maxResumeSequenceOrNull(retained.events);
|
|
613
667
|
if (retainedNewest === null || retainedNewest <= afterSequence) {
|
|
614
668
|
throw new Error("@opengeni/react: loadNewer made no durable sequence progress");
|
|
615
669
|
}
|
|
616
670
|
eventWindowRef.current = retained;
|
|
617
|
-
setEventWindow(retained);
|
|
618
671
|
oldestSequenceRef.current = retainedOldest;
|
|
619
672
|
newestSequenceRef.current = retainedNewest;
|
|
620
673
|
// Evicting the start while paging forward re-arms hasOlder.
|
|
@@ -627,23 +680,39 @@ export function useSessionEvents(
|
|
|
627
680
|
retainedOldest > 1))
|
|
628
681
|
) {
|
|
629
682
|
hasOlderRef.current = true;
|
|
630
|
-
setHasOlder(true);
|
|
631
683
|
}
|
|
684
|
+
const rearmedOlder = hasOlderRef.current;
|
|
632
685
|
const highWater = Math.max(lastSequenceRef.current, retainedNewest);
|
|
633
686
|
lastSequenceRef.current = highWater;
|
|
634
687
|
const newer = window.hasNewer || retainedNewest < highWater;
|
|
635
688
|
hasNewerRef.current = newer;
|
|
636
|
-
|
|
689
|
+
let resumeLive = false;
|
|
637
690
|
if (!newer) {
|
|
638
691
|
streamResumeSequenceRef.current = retainedNewest;
|
|
639
692
|
viewModeRef.current = "live";
|
|
640
|
-
|
|
641
|
-
setStreamEpoch((epoch) => epoch + 1);
|
|
693
|
+
resumeLive = true;
|
|
642
694
|
}
|
|
695
|
+
loadingNewerRef.current = false;
|
|
696
|
+
startTransition(() => {
|
|
697
|
+
if (status !== undefined) {
|
|
698
|
+
setSessionStatusProjection(status);
|
|
699
|
+
}
|
|
700
|
+
setEventWindow(retained);
|
|
701
|
+
setHasOlder(rearmedOlder);
|
|
702
|
+
setHasNewer(newer);
|
|
703
|
+
if (resumeLive) {
|
|
704
|
+
setViewMode("live");
|
|
705
|
+
setStreamEpoch((epoch) => epoch + 1);
|
|
706
|
+
}
|
|
707
|
+
setLoadingNewer(false);
|
|
708
|
+
});
|
|
709
|
+
published = true;
|
|
643
710
|
return newer;
|
|
644
711
|
} finally {
|
|
645
|
-
|
|
646
|
-
|
|
712
|
+
if (!published) {
|
|
713
|
+
loadingNewerRef.current = false;
|
|
714
|
+
setLoadingNewer(false);
|
|
715
|
+
}
|
|
647
716
|
}
|
|
648
717
|
}, [client, workspaceId, sessionId]);
|
|
649
718
|
|
|
@@ -653,27 +722,35 @@ export function useSessionEvents(
|
|
|
653
722
|
}
|
|
654
723
|
loadingLatestRef.current = true;
|
|
655
724
|
setLoadingLatest(true);
|
|
725
|
+
let published = false;
|
|
656
726
|
try {
|
|
657
727
|
streamAbortRef.current?.abort();
|
|
658
728
|
hasNewerRef.current = false;
|
|
659
|
-
setHasNewer(false);
|
|
660
729
|
hasOlderRef.current = false;
|
|
661
|
-
setHasOlder(false);
|
|
662
730
|
newestSequenceRef.current = null;
|
|
663
731
|
oldestSequenceRef.current = null;
|
|
664
732
|
eventWindowRef.current = EMPTY_EVENT_WINDOW;
|
|
665
|
-
setEventWindow(EMPTY_EVENT_WINDOW);
|
|
666
733
|
initialWindowLoadedRef.current = false;
|
|
667
734
|
streamResumeSequenceRef.current = after;
|
|
668
|
-
setInitialLoading(true);
|
|
669
735
|
viewModeRef.current = "live";
|
|
670
|
-
|
|
671
|
-
|
|
736
|
+
loadingLatestRef.current = false;
|
|
737
|
+
startTransition(() => {
|
|
738
|
+
setHasNewer(false);
|
|
739
|
+
setHasOlder(false);
|
|
740
|
+
setEventWindow(EMPTY_EVENT_WINDOW);
|
|
741
|
+
setInitialLoading(true);
|
|
742
|
+
setViewMode("live");
|
|
743
|
+
setStreamEpoch((epoch) => epoch + 1);
|
|
744
|
+
setLoadingLatest(false);
|
|
745
|
+
});
|
|
746
|
+
published = true;
|
|
672
747
|
// Effect reloads the tip. The tip fetch itself is owned by the effect.
|
|
673
748
|
await Promise.resolve();
|
|
674
749
|
} finally {
|
|
675
|
-
|
|
676
|
-
|
|
750
|
+
if (!published) {
|
|
751
|
+
loadingLatestRef.current = false;
|
|
752
|
+
setLoadingLatest(false);
|
|
753
|
+
}
|
|
677
754
|
}
|
|
678
755
|
}, [after, sessionId]);
|
|
679
756
|
|
|
@@ -709,9 +786,9 @@ export function useSessionEvents(
|
|
|
709
786
|
* accumulation retains the newest suffix; backward paging retains the oldest
|
|
710
787
|
* prefix so newly fetched history cannot be immediately evicted. This is
|
|
711
788
|
* deliberately separate from durable history and transport paging: when a
|
|
712
|
-
* backward page evicts the live tail, the hook
|
|
713
|
-
*
|
|
714
|
-
*
|
|
789
|
+
* backward page evicts the live tail, the hook enters bounded history mode and
|
|
790
|
+
* preserves the highest-ever-observed sequence separately for forward paging.
|
|
791
|
+
* The source event remains durable in PostgreSQL throughout.
|
|
715
792
|
*/
|
|
716
793
|
export function boundBrowserSessionEventWindow(
|
|
717
794
|
events: readonly SessionEvent[],
|
|
@@ -993,8 +1070,7 @@ function isBrowserUtf8Continuation(value: number): boolean {
|
|
|
993
1070
|
function observeSessionStatus(
|
|
994
1071
|
events: readonly SessionEvent[],
|
|
995
1072
|
ref: { current: { sequence: number; status: SessionStatus | null } },
|
|
996
|
-
|
|
997
|
-
): void {
|
|
1073
|
+
): SessionStatus | undefined {
|
|
998
1074
|
let latest: { sequence: number; status: SessionStatus } | null = null;
|
|
999
1075
|
for (const event of events) {
|
|
1000
1076
|
const status = sessionStatusFromEvents([event]);
|
|
@@ -1002,9 +1078,9 @@ function observeSessionStatus(
|
|
|
1002
1078
|
latest = { sequence: event.sequence, status };
|
|
1003
1079
|
}
|
|
1004
1080
|
}
|
|
1005
|
-
if (!latest || latest.sequence < ref.current.sequence) return;
|
|
1081
|
+
if (!latest || latest.sequence < ref.current.sequence) return undefined;
|
|
1006
1082
|
ref.current = latest;
|
|
1007
|
-
|
|
1083
|
+
return latest.status;
|
|
1008
1084
|
}
|
|
1009
1085
|
|
|
1010
1086
|
type LoadedEventWindow = {
|
|
@@ -1097,6 +1173,8 @@ async function loadEventWindow(
|
|
|
1097
1173
|
buffer = buffer.slice(boundary);
|
|
1098
1174
|
}
|
|
1099
1175
|
}
|
|
1176
|
+
const densityWindow = trimBackwardToGroupTarget(buffer, options.targetGroups);
|
|
1177
|
+
buffer = densityWindow.events;
|
|
1100
1178
|
|
|
1101
1179
|
const oldest = buffer[0] ?? null;
|
|
1102
1180
|
const newest = buffer[buffer.length - 1] ?? null;
|
|
@@ -1104,21 +1182,26 @@ async function loadEventWindow(
|
|
|
1104
1182
|
events: buffer,
|
|
1105
1183
|
oldestSequence: oldest?.sequence ?? null,
|
|
1106
1184
|
newestSequence: newest ? maxResumeSequence(buffer) : 0,
|
|
1107
|
-
hasOlder:
|
|
1185
|
+
hasOlder:
|
|
1186
|
+
buffer.length > 0 &&
|
|
1187
|
+
(densityWindow.trimmed || (!reachedStart && oldest?.type !== "session.created")),
|
|
1108
1188
|
};
|
|
1109
1189
|
}
|
|
1110
1190
|
|
|
1111
1191
|
/** Index of the oldest clean turn start in the buffer, or -1. */
|
|
1112
1192
|
function findBoundaryIndex(events: SessionEvent[]): number {
|
|
1113
1193
|
for (let index = 0; index < events.length; index += 1) {
|
|
1114
|
-
|
|
1115
|
-
if (type === "session.created" || type === "user.message") {
|
|
1194
|
+
if (isTurnBoundary(events[index]!)) {
|
|
1116
1195
|
return index;
|
|
1117
1196
|
}
|
|
1118
1197
|
}
|
|
1119
1198
|
return -1;
|
|
1120
1199
|
}
|
|
1121
1200
|
|
|
1201
|
+
function isTurnBoundary(event: SessionEvent): boolean {
|
|
1202
|
+
return event.type === "session.created" || event.type === "user.message";
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1122
1205
|
/**
|
|
1123
1206
|
* Forward (oldest→newer) density-bounded window. Used for jump-to-start and
|
|
1124
1207
|
* loadNewer so the browser never walks the whole middle gap.
|
|
@@ -1155,12 +1238,15 @@ async function loadForwardEventWindow(
|
|
|
1155
1238
|
}
|
|
1156
1239
|
assertAscending(page);
|
|
1157
1240
|
buffer = [...buffer, ...page];
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1241
|
+
// Compact events retain their first raw sequence in `sequence` and expose
|
|
1242
|
+
// the covered high-water mark through `coalescedUntil`. Advancing by the
|
|
1243
|
+
// first sequence duplicates compacted text on the next page. A compact
|
|
1244
|
+
// response length is not an end-of-history signal either: thousands of raw
|
|
1245
|
+
// events may legitimately collapse into one returned event.
|
|
1246
|
+
cursor = maxResumeSequence(page);
|
|
1163
1247
|
}
|
|
1248
|
+
const densityWindow = trimForwardToGroupTarget(buffer, options.targetGroups);
|
|
1249
|
+
buffer = densityWindow.events;
|
|
1164
1250
|
|
|
1165
1251
|
const oldest = buffer[0] ?? null;
|
|
1166
1252
|
const newest = buffer[buffer.length - 1] ?? null;
|
|
@@ -1168,10 +1254,45 @@ async function loadForwardEventWindow(
|
|
|
1168
1254
|
events: buffer,
|
|
1169
1255
|
oldestSequence: oldest?.sequence ?? null,
|
|
1170
1256
|
newestSequence: newest ? maxResumeSequence(buffer) : 0,
|
|
1171
|
-
hasNewer: buffer.length > 0 && !reachedEnd,
|
|
1257
|
+
hasNewer: buffer.length > 0 && (densityWindow.trimmed || !reachedEnd),
|
|
1172
1258
|
};
|
|
1173
1259
|
}
|
|
1174
1260
|
|
|
1261
|
+
type DensityWindow = {
|
|
1262
|
+
events: SessionEvent[];
|
|
1263
|
+
trimmed: boolean;
|
|
1264
|
+
};
|
|
1265
|
+
|
|
1266
|
+
/** Keep the newest complete turn groups near an older-history seam. */
|
|
1267
|
+
function trimBackwardToGroupTarget(events: SessionEvent[], targetGroups: number): DensityWindow {
|
|
1268
|
+
if (!Number.isFinite(targetGroups) || targetGroups <= 0 || events.length === 0) {
|
|
1269
|
+
return { events, trimmed: false };
|
|
1270
|
+
}
|
|
1271
|
+
for (let index = events.length - 1; index > 0; index -= 1) {
|
|
1272
|
+
if (!isTurnBoundary(events[index]!)) continue;
|
|
1273
|
+
const candidate = events.slice(index);
|
|
1274
|
+
if (groupCount(candidate) >= targetGroups) {
|
|
1275
|
+
return { events: candidate, trimmed: true };
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
return { events, trimmed: false };
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
/** Keep the oldest complete turn groups near a forward-history seam. */
|
|
1282
|
+
function trimForwardToGroupTarget(events: SessionEvent[], targetGroups: number): DensityWindow {
|
|
1283
|
+
if (!Number.isFinite(targetGroups) || targetGroups <= 0 || events.length === 0) {
|
|
1284
|
+
return { events, trimmed: false };
|
|
1285
|
+
}
|
|
1286
|
+
for (let index = 1; index < events.length; index += 1) {
|
|
1287
|
+
if (!isTurnBoundary(events[index]!)) continue;
|
|
1288
|
+
const candidate = events.slice(0, index);
|
|
1289
|
+
if (groupCount(candidate) >= targetGroups) {
|
|
1290
|
+
return { events: candidate, trimmed: true };
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
return { events, trimmed: false };
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1175
1296
|
type PreviousPage = SessionEvent[] & { requested: number };
|
|
1176
1297
|
|
|
1177
1298
|
async function loadPreviousPage(
|
|
@@ -1248,10 +1369,14 @@ function isLogStart(event: SessionEvent): boolean {
|
|
|
1248
1369
|
return event.type === "session.created" || event.sequence <= 1;
|
|
1249
1370
|
}
|
|
1250
1371
|
|
|
1251
|
-
function maxResumeSequence(events: SessionEvent[]): number {
|
|
1372
|
+
function maxResumeSequence(events: readonly SessionEvent[]): number {
|
|
1252
1373
|
return events.reduce((max, event) => Math.max(max, eventResumeSequence(event)), 0);
|
|
1253
1374
|
}
|
|
1254
1375
|
|
|
1376
|
+
function maxResumeSequenceOrNull(events: readonly SessionEvent[]): number | null {
|
|
1377
|
+
return events.length > 0 ? maxResumeSequence(events) : null;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1255
1380
|
function eventResumeSequence(event: SessionEvent): number {
|
|
1256
1381
|
const payload = asRecord(event.payload);
|
|
1257
1382
|
const coalescedUntil = Number(payload.coalescedUntil);
|
|
@@ -69,24 +69,28 @@ export function useSessionLineage(
|
|
|
69
69
|
const enabled = (options.enabled ?? true) && Boolean(sessionId);
|
|
70
70
|
const nextReadGeneration = useRef(0);
|
|
71
71
|
const beginRead = options.beginRead;
|
|
72
|
-
const load = useCallback(
|
|
73
|
-
|
|
72
|
+
const load = useCallback(
|
|
73
|
+
async (signal?: AbortSignal) => {
|
|
74
|
+
if (!sessionId) {
|
|
75
|
+
return {
|
|
76
|
+
lineage: { ancestors: [], children: [], truncated: false },
|
|
77
|
+
readGeneration: 0,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
let readGeneration = 0;
|
|
81
|
+
const lineage = await client.getSessionLineage(workspaceId, sessionId, {
|
|
82
|
+
signal,
|
|
83
|
+
onRequestStart: (sharedReadGeneration) => {
|
|
84
|
+
readGeneration = sharedReadGeneration ?? beginRead?.() ?? ++nextReadGeneration.current;
|
|
85
|
+
},
|
|
86
|
+
});
|
|
74
87
|
return {
|
|
75
|
-
lineage
|
|
76
|
-
readGeneration
|
|
88
|
+
lineage,
|
|
89
|
+
readGeneration,
|
|
77
90
|
};
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
onRequestStart: (sharedReadGeneration) => {
|
|
82
|
-
readGeneration = sharedReadGeneration ?? beginRead?.() ?? ++nextReadGeneration.current;
|
|
83
|
-
},
|
|
84
|
-
});
|
|
85
|
-
return {
|
|
86
|
-
lineage,
|
|
87
|
-
readGeneration,
|
|
88
|
-
};
|
|
89
|
-
}, [beginRead, client, workspaceId, sessionId]);
|
|
91
|
+
},
|
|
92
|
+
[beginRead, client, workspaceId, sessionId],
|
|
93
|
+
);
|
|
90
94
|
const state = usePolledValue(load, { pollIntervalMs: options.pollIntervalMs, enabled });
|
|
91
95
|
const refresh = state.refresh;
|
|
92
96
|
useEffect(() => {
|
package/src/hooks/use-session.ts
CHANGED
|
@@ -51,25 +51,29 @@ export function useSession(
|
|
|
51
51
|
const nextReadGeneration = useRef(0);
|
|
52
52
|
const beginRead = options.beginRead;
|
|
53
53
|
const { run, mutating, mutationError, clearMutationError } = useMutationRunner();
|
|
54
|
-
const load = useCallback(
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
54
|
+
const load = useCallback(
|
|
55
|
+
async (signal?: AbortSignal) => {
|
|
56
|
+
if (!sessionId) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
let readGeneration = 0;
|
|
60
|
+
const fetched = await client.getSession(workspaceId, sessionId, {
|
|
61
|
+
fresh: true,
|
|
62
|
+
signal,
|
|
63
|
+
onRequestStart: () => {
|
|
64
|
+
readGeneration = beginRead?.() ?? ++nextReadGeneration.current;
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
// A fresh server read supersedes any optimistic/event-driven override.
|
|
68
|
+
setOverride(null);
|
|
69
|
+
return {
|
|
70
|
+
session: fetched,
|
|
71
|
+
revision: ++nextReadRevision.current,
|
|
72
|
+
readGeneration,
|
|
73
|
+
};
|
|
74
|
+
},
|
|
75
|
+
[beginRead, client, workspaceId, sessionId],
|
|
76
|
+
);
|
|
73
77
|
const { data, loading, error, refresh } = usePolledValue(load, {
|
|
74
78
|
pollIntervalMs: options.pollIntervalMs,
|
|
75
79
|
enabled,
|
|
@@ -67,34 +67,38 @@ export function useWorkspaceSessions(
|
|
|
67
67
|
useEffect(() => {
|
|
68
68
|
previousQueryKey.current = queryKey;
|
|
69
69
|
}, [queryKey]);
|
|
70
|
-
const load = useCallback(
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
70
|
+
const load = useCallback(
|
|
71
|
+
async (signal?: AbortSignal) => {
|
|
72
|
+
const readGeneration = beginRead?.() ?? ++nextReadGeneration.current;
|
|
73
|
+
const page = await client.listSessionPage(workspaceId, {
|
|
74
|
+
...(limit !== undefined ? { limit } : {}),
|
|
75
|
+
...(parentSessionId !== undefined ? { parentSessionId } : {}),
|
|
76
|
+
...(cursor !== undefined ? { cursor } : {}),
|
|
77
|
+
...(search !== undefined ? { search } : {}),
|
|
78
|
+
...(pinsOnly ? { pinsOnly: true } : {}),
|
|
79
|
+
...(archivedOnly ? { archivedOnly: true } : {}),
|
|
80
|
+
signal,
|
|
81
|
+
});
|
|
82
|
+
return {
|
|
83
|
+
queryKey,
|
|
84
|
+
page,
|
|
85
|
+
revision: ++nextReadRevision.current,
|
|
86
|
+
readGeneration,
|
|
87
|
+
};
|
|
88
|
+
},
|
|
89
|
+
[
|
|
90
|
+
beginRead,
|
|
91
|
+
client,
|
|
92
|
+
workspaceId,
|
|
93
|
+
limit,
|
|
94
|
+
parentSessionId,
|
|
95
|
+
cursor,
|
|
96
|
+
search,
|
|
97
|
+
pinsOnly,
|
|
98
|
+
archivedOnly,
|
|
81
99
|
queryKey,
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
readGeneration,
|
|
85
|
-
};
|
|
86
|
-
}, [
|
|
87
|
-
beginRead,
|
|
88
|
-
client,
|
|
89
|
-
workspaceId,
|
|
90
|
-
limit,
|
|
91
|
-
parentSessionId,
|
|
92
|
-
cursor,
|
|
93
|
-
search,
|
|
94
|
-
pinsOnly,
|
|
95
|
-
archivedOnly,
|
|
96
|
-
queryKey,
|
|
97
|
-
]);
|
|
100
|
+
],
|
|
101
|
+
);
|
|
98
102
|
const state = usePolledValue(load, {
|
|
99
103
|
pollIntervalMs: options.pollIntervalMs,
|
|
100
104
|
enabled,
|
package/src/lib/format.ts
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import { OpenGeniApiError } from "@opengeni/sdk";
|
|
2
2
|
|
|
3
|
+
const clockTimeFormatter = new Intl.DateTimeFormat(undefined, {
|
|
4
|
+
month: "short",
|
|
5
|
+
day: "numeric",
|
|
6
|
+
hour: "numeric",
|
|
7
|
+
minute: "2-digit",
|
|
8
|
+
});
|
|
9
|
+
|
|
3
10
|
/** Local finished-at stamp for message footers: "Aug 2, 3:42 PM" / locale-equivalent. */
|
|
4
11
|
export function formatClockTime(iso: string): string {
|
|
5
12
|
const then = new Date(iso);
|
|
6
13
|
if (Number.isNaN(then.getTime())) {
|
|
7
14
|
return "";
|
|
8
15
|
}
|
|
9
|
-
return
|
|
10
|
-
month: "short",
|
|
11
|
-
day: "numeric",
|
|
12
|
-
hour: "numeric",
|
|
13
|
-
minute: "2-digit",
|
|
14
|
-
});
|
|
16
|
+
return clockTimeFormatter.format(then);
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
/** Compact relative time: "now", "42s", "7m", "3h", "2d", then a date. */
|