@opengeni/react 0.28.2 → 0.32.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/approvals.d.ts +14 -0
- package/dist/{chunk-M7X4JZOD.js → chunk-4QOQ2DSR.js} +8 -1
- package/dist/chunk-4QOQ2DSR.js.map +1 -0
- package/dist/{chunk-HQFPO7VX.js → chunk-7CJOAW3V.js} +24 -3
- package/dist/{chunk-HQFPO7VX.js.map → chunk-7CJOAW3V.js.map} +1 -1
- package/dist/{chunk-PMSGE233.js → chunk-A2WITZTI.js} +377 -113
- package/dist/chunk-A2WITZTI.js.map +1 -0
- package/dist/{chunk-BC6RKIHS.js → chunk-GZ4KR67U.js} +11 -11
- package/dist/{chunk-4F2YLLHP.js → chunk-KZRUYCL4.js} +14 -17
- package/dist/chunk-KZRUYCL4.js.map +1 -0
- package/dist/{chunk-OCHBZ2X7.js → chunk-TCCE5ZU5.js} +192 -28
- package/dist/chunk-TCCE5ZU5.js.map +1 -0
- package/dist/chunk-U255M5EZ.js +178 -0
- package/dist/chunk-U255M5EZ.js.map +1 -0
- package/dist/{chunk-XVVQP7A4.js → chunk-WY2QN2CX.js} +2539 -2174
- package/dist/chunk-WY2QN2CX.js.map +1 -0
- package/dist/chunk-ZNNE2VES.js +6413 -0
- package/dist/chunk-ZNNE2VES.js.map +1 -0
- package/dist/client.d.ts +33 -0
- package/dist/commands/index.d.ts +3 -0
- package/dist/commands/registry.d.ts +41 -0
- package/dist/commands/types.d.ts +85 -0
- package/dist/components/approval-surface.d.ts +28 -0
- package/dist/components/chat-composer.d.ts +51 -0
- package/dist/components/code-editor.d.ts +43 -0
- package/dist/components/command-palette.d.ts +25 -0
- package/dist/components/composer-transcription-control.d.ts +36 -0
- package/dist/components/composer.d.ts +277 -0
- package/dist/components/copy-button.d.ts +26 -0
- package/dist/components/desktop-viewer.d.ts +85 -0
- package/dist/components/diff-view.d.ts +38 -0
- package/dist/components/enrollment-consent.d.ts +33 -0
- package/dist/components/enrollment-device-flow.d.ts +27 -0
- package/dist/components/file-browser.d.ts +47 -0
- package/dist/components/fleet-tile.d.ts +18 -0
- package/dist/components/human-input-form.d.ts +47 -0
- package/dist/components/machine-card.d.ts +21 -0
- package/dist/components/machine-dock-bar.d.ts +31 -0
- package/dist/components/machine-health-pill.d.ts +9 -0
- package/dist/components/machine-metrics.d.ts +15 -0
- package/dist/components/machine-status-pill.d.ts +54 -0
- package/dist/components/machines/health.d.ts +25 -0
- package/dist/components/machines/machine-detail.d.ts +14 -0
- package/dist/components/machines/metric-history-chart.d.ts +24 -0
- package/dist/components/machines/metric-sparkline.d.ts +9 -0
- package/dist/components/machines/series.d.ts +24 -0
- package/dist/components/machines-dashboard.d.ts +30 -0
- package/dist/components/markdown.d.ts +31 -0
- package/dist/components/message-timeline.d.ts +98 -0
- package/dist/components/model-picker.d.ts +19 -0
- package/dist/components/pierre-diff.d.ts +34 -0
- package/dist/components/pierre-file.d.ts +27 -0
- package/dist/components/queue-draft-policy.d.ts +7 -0
- package/dist/components/queue-surface-implementation.d.ts +20 -0
- package/dist/components/queue-surface-state.d.ts +12 -0
- package/dist/components/queue-surface.d.ts +12 -0
- package/dist/components/sandbox-files.d.ts +49 -0
- package/dist/components/sandbox-terminal.d.ts +123 -0
- package/dist/components/sandbox-workspace.d.ts +128 -0
- package/dist/components/session-chrome.d.ts +70 -0
- package/dist/components/session-status.d.ts +25 -0
- package/dist/components/soften-streaming-markdown.d.ts +13 -0
- package/dist/components/stream-reveal.d.ts +37 -0
- package/dist/components/tip-follow.d.ts +139 -0
- package/dist/components/tooltip.d.ts +16 -0
- package/dist/components/workbench-changes.d.ts +34 -0
- package/dist/components/workspace-dock.d.ts +35 -0
- package/dist/composer.d.ts +11 -7
- package/dist/composer.js +9 -6
- package/dist/hooks/internal.d.ts +46 -0
- package/dist/hooks/use-available-models.d.ts +37 -0
- package/dist/hooks/use-billing-usage.d.ts +26 -0
- package/dist/hooks/use-codex-accounts.d.ts +69 -0
- package/dist/{use-composer-ClHy1rkL.d.ts → hooks/use-composer.d.ts} +16 -24
- package/dist/hooks/use-desktop-stream.d.ts +43 -0
- package/dist/hooks/use-environments.d.ts +35 -0
- package/dist/{use-file-attachments-BmT8WtqO.d.ts → hooks/use-file-attachments.d.ts} +6 -9
- package/dist/hooks/use-goal.d.ts +46 -0
- package/dist/hooks/use-human-input.d.ts +26 -0
- package/dist/hooks/use-last-started-turn-policy.d.ts +27 -0
- package/dist/hooks/use-machine-chip.d.ts +54 -0
- package/dist/hooks/use-machines.d.ts +81 -0
- package/dist/hooks/use-packs.d.ts +27 -0
- package/dist/hooks/use-relay-frame-stream.d.ts +53 -0
- package/dist/hooks/use-rigs.d.ts +87 -0
- package/dist/hooks/use-sandbox-files.d.ts +108 -0
- package/dist/hooks/use-sandbox-git.d.ts +58 -0
- package/dist/hooks/use-sandbox-terminal.d.ts +62 -0
- package/dist/hooks/use-scheduled-tasks.d.ts +15 -0
- package/dist/hooks/use-session-capabilities.d.ts +83 -0
- package/dist/hooks/use-session-control.d.ts +22 -0
- package/dist/hooks/use-session-events.d.ts +92 -0
- package/dist/hooks/use-session-lineage.d.ts +17 -0
- package/dist/hooks/use-session-mcp-approval-policy.d.ts +23 -0
- package/dist/hooks/use-session.d.ts +23 -0
- package/dist/hooks/use-slash-commands.d.ts +66 -0
- package/dist/hooks/use-terminal-stream.d.ts +46 -0
- package/dist/hooks/use-transcription.d.ts +73 -0
- package/dist/hooks/use-turn-queue.d.ts +44 -0
- package/dist/hooks/use-voice-input.d.ts +25 -0
- package/dist/hooks/use-windowed-sections.d.ts +42 -0
- package/dist/hooks/use-workspace-capture.d.ts +49 -0
- package/dist/hooks/use-workspace-edit.d.ts +73 -0
- package/dist/hooks/use-workspace-sessions.d.ts +30 -0
- package/dist/hooks/use-workspaces.d.ts +22 -0
- package/dist/human-input.d.ts +18 -0
- package/dist/index.d.ts +144 -2119
- package/dist/index.js +421 -324
- package/dist/index.js.map +1 -1
- package/dist/lib/clipboard.d.ts +7 -0
- package/dist/lib/cn.d.ts +3 -0
- package/dist/lib/format.d.ts +48 -0
- package/dist/lib/git-patch.d.ts +8 -0
- package/dist/lib/motion-inspect.d.ts +5 -0
- package/dist/lib/motion.d.ts +14 -0
- package/dist/lib/noto-sans-arabic-loader.d.ts +1 -0
- package/dist/lib/noto-sans-jp-loader.generated.d.ts +1 -0
- package/dist/lib/relay-wire.d.ts +30 -0
- package/dist/lib/use-portal-token-style.d.ts +9 -0
- package/dist/lib/use-theme-type.d.ts +5 -0
- package/dist/lib/use-unicode-fonts.d.ts +12 -0
- package/dist/lib/xterm-renderer.d.ts +19 -0
- package/dist/lib/xterm-theme.d.ts +75 -0
- package/dist/machines.d.ts +30 -413
- package/dist/machines.js +3 -3
- package/dist/model-policy.d.ts +32 -0
- package/dist/model-policy.js +33 -0
- package/dist/model-policy.js.map +1 -0
- package/dist/provider.d.ts +16 -0
- package/dist/{queue-surface-implementation-UQ535OKQ.js → queue-surface-implementation-THPAKGMD.js} +2 -2
- package/dist/{queue-surface-implementation-UQ535OKQ.js.map → queue-surface-implementation-THPAKGMD.js.map} +1 -1
- package/dist/session-context.d.ts +82 -0
- package/dist/session-ui.d.ts +10 -7
- package/dist/session-ui.js +9 -5
- package/dist/session.d.ts +28 -6
- package/dist/session.js +11 -11
- package/dist/timeline/activity-rail.d.ts +19 -0
- package/dist/timeline/disclosure-context.d.ts +14 -0
- package/dist/timeline/entrance.d.ts +14 -0
- package/dist/timeline/fleet-decision-projection.d.ts +9 -0
- package/dist/timeline/fleet-decision-row.d.ts +11 -0
- package/dist/timeline/fold-memory.d.ts +15 -0
- package/dist/timeline/index.d.ts +18 -0
- package/dist/timeline/parsers.d.ts +96 -0
- package/dist/timeline/projection.d.ts +38 -0
- package/dist/timeline/registry.d.ts +40 -0
- package/dist/timeline/screenshot-lightbox.d.ts +26 -0
- package/dist/timeline/seen-activity-ids.d.ts +2 -0
- package/dist/timeline/shared.d.ts +125 -0
- package/dist/timeline/tool-diff.d.ts +8 -0
- package/dist/timeline/tool-display-name.d.ts +10 -0
- package/dist/timeline/tool-renderers.d.ts +5 -0
- package/dist/timeline/turn-summary.d.ts +116 -0
- package/dist/{use-turn-queue-Drm_ypue.d.ts → timeline/types.d.ts} +57 -67
- package/dist/types/machines.d.ts +11 -0
- package/package.json +8 -4
- package/src/client.ts +1 -0
- package/src/components/composer-transcription-control.tsx +278 -130
- package/src/components/composer.tsx +20 -15
- package/src/components/copy-button.tsx +127 -0
- package/src/components/markdown.tsx +231 -32
- package/src/components/message-timeline.tsx +1382 -378
- package/src/components/model-picker.tsx +83 -43
- package/src/components/queue-surface-implementation.tsx +1 -1
- package/src/components/session-chrome.tsx +942 -0
- package/src/components/soften-streaming-markdown.ts +205 -0
- package/src/components/stream-reveal.ts +195 -0
- package/src/components/tip-follow.ts +459 -0
- package/src/components/tooltip.tsx +103 -0
- package/src/composer.ts +6 -0
- package/src/hooks/internal.ts +30 -4
- package/src/hooks/use-available-models.ts +51 -4
- package/src/hooks/use-composer.ts +24 -18
- package/src/hooks/use-last-started-turn-policy.ts +95 -0
- package/src/hooks/use-session-events.ts +359 -5
- package/src/hooks/use-voice-input.ts +232 -0
- package/src/index.ts +49 -1
- package/src/lib/clipboard.ts +58 -0
- package/src/lib/format.ts +18 -0
- package/src/lib/motion-inspect.ts +5 -0
- package/src/lib/motion.ts +80 -0
- package/src/model-policy.ts +210 -0
- package/src/session-ui.ts +7 -0
- package/src/session.ts +2 -0
- package/src/timeline/activity-rail.tsx +46 -7
- package/src/timeline/entrance.tsx +10 -0
- package/src/timeline/fold-memory.ts +52 -0
- package/src/timeline/index.ts +11 -1
- package/src/timeline/projection.ts +252 -47
- package/src/timeline/seen-activity-ids.ts +19 -0
- package/src/timeline/shared.tsx +1 -1
- package/src/timeline/tool-renderers.tsx +75 -6
- package/src/timeline/turn-summary.tsx +387 -97
- package/src/timeline/types.ts +47 -0
- package/styles/index.css +99 -7
- package/styles/tokens.css +44 -3
- package/dist/chunk-4F2YLLHP.js.map +0 -1
- package/dist/chunk-M7X4JZOD.js.map +0 -1
- package/dist/chunk-OCHBZ2X7.js.map +0 -1
- package/dist/chunk-PMSGE233.js.map +0 -1
- package/dist/chunk-WAA247AB.js +0 -3858
- package/dist/chunk-WAA247AB.js.map +0 -1
- package/dist/chunk-XVVQP7A4.js.map +0 -1
- package/dist/composer-BMzimGTO.d.ts +0 -528
- package/dist/session-C3tIXLim.d.ts +0 -264
- package/dist/session-context-CAjlaeZb.d.ts +0 -84
- package/dist/session-ui-B_7cfN5u.d.ts +0 -259
- /package/dist/{chunk-BC6RKIHS.js.map → chunk-GZ4KR67U.js.map} +0 -0
|
@@ -38,14 +38,46 @@ export type UseSessionEventsResult = {
|
|
|
38
38
|
loadingOlder: boolean;
|
|
39
39
|
/** Prepend an older density-bounded window; resolves true when more remain. */
|
|
40
40
|
loadOlder: () => Promise<boolean>;
|
|
41
|
+
/**
|
|
42
|
+
* Durable events exist after the current window (history view jumped away
|
|
43
|
+
* from the live tip, or a forward page is incomplete).
|
|
44
|
+
*/
|
|
45
|
+
hasNewer: boolean;
|
|
46
|
+
/** True while a newer history page is being fetched. */
|
|
47
|
+
loadingNewer: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Append one density-bounded newer page without pulling the whole gap.
|
|
50
|
+
* Resolves true when more newer history remains.
|
|
51
|
+
*/
|
|
52
|
+
loadNewer: () => Promise<boolean>;
|
|
53
|
+
/** True while replacing the window with the session start. */
|
|
54
|
+
loadingOldest: boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Jump to the durable session start: one bounded oldest window, no middle.
|
|
57
|
+
* Leaves live streaming until {@link jumpToLatest} (or loadNewer catches up).
|
|
58
|
+
*/
|
|
59
|
+
loadOldest: () => Promise<boolean>;
|
|
60
|
+
/** True while reloading the live tip window. */
|
|
61
|
+
loadingLatest: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* Reload the newest bounded tip and resume live streaming. Use for
|
|
64
|
+
* "Jump to latest" when the tip is not in memory (history view).
|
|
65
|
+
*/
|
|
66
|
+
jumpToLatest: () => Promise<void>;
|
|
41
67
|
error: Error | null;
|
|
42
68
|
};
|
|
43
69
|
|
|
44
70
|
const INITIAL_TAIL_PAGE_SIZE = 1000;
|
|
45
71
|
const OLDER_PAGE_SIZE = 5000;
|
|
72
|
+
const NEWER_PAGE_SIZE = 5000;
|
|
73
|
+
const OLDEST_PAGE_SIZE = 1000;
|
|
46
74
|
const INITIAL_FETCH_CAP = 1;
|
|
47
75
|
const OLDER_GROUP_TARGET = 32;
|
|
48
76
|
const OLDER_FETCH_CAP = 2;
|
|
77
|
+
const NEWER_GROUP_TARGET = 32;
|
|
78
|
+
const NEWER_FETCH_CAP = 2;
|
|
79
|
+
const OLDEST_GROUP_TARGET = 32;
|
|
80
|
+
const OLDEST_FETCH_CAP = 2;
|
|
49
81
|
const BOUNDARY_PAGE_CAP = 4;
|
|
50
82
|
const EMPTY_EVENTS: SessionEvent[] = [];
|
|
51
83
|
const encoder = new TextEncoder();
|
|
@@ -112,17 +144,29 @@ export function useSessionEvents(
|
|
|
112
144
|
const [connectionState, setConnectionState] = useState<SessionEventsConnectionState>("idle");
|
|
113
145
|
const [error, setError] = useState<Error | null>(null);
|
|
114
146
|
const [hasOlder, setHasOlder] = useState(false);
|
|
147
|
+
const [hasNewer, setHasNewer] = useState(false);
|
|
115
148
|
const [sessionStatusProjection, setSessionStatusProjection] = useState<SessionStatus | null>(
|
|
116
149
|
null,
|
|
117
150
|
);
|
|
118
151
|
const [initialLoading, setInitialLoading] = useState(true);
|
|
119
152
|
const [loadingOlder, setLoadingOlder] = useState(false);
|
|
153
|
+
const [loadingNewer, setLoadingNewer] = useState(false);
|
|
154
|
+
const [loadingOldest, setLoadingOldest] = useState(false);
|
|
155
|
+
const [loadingLatest, setLoadingLatest] = useState(false);
|
|
120
156
|
const [streamEpoch, setStreamEpoch] = useState(0);
|
|
157
|
+
// "history" = reading a non-tip window; live SSE must not fill the gap.
|
|
158
|
+
const [viewMode, setViewMode] = useState<"live" | "history">("live");
|
|
121
159
|
const lastSequenceRef = useRef(after);
|
|
122
160
|
const streamResumeSequenceRef = useRef(after);
|
|
123
161
|
const oldestSequenceRef = useRef<number | null>(null);
|
|
162
|
+
const newestSequenceRef = useRef<number | null>(null);
|
|
124
163
|
const hasOlderRef = useRef(false);
|
|
164
|
+
const hasNewerRef = useRef(false);
|
|
125
165
|
const loadingOlderRef = useRef(false);
|
|
166
|
+
const loadingNewerRef = useRef(false);
|
|
167
|
+
const loadingOldestRef = useRef(false);
|
|
168
|
+
const loadingLatestRef = useRef(false);
|
|
169
|
+
const viewModeRef = useRef<"live" | "history">("live");
|
|
126
170
|
const initialWindowLoadedRef = useRef(false);
|
|
127
171
|
const streamAbortRef = useRef<AbortController | null>(null);
|
|
128
172
|
const streamKeyRef = useRef<string | null>(null);
|
|
@@ -149,15 +193,26 @@ export function useSessionEvents(
|
|
|
149
193
|
setEventWindow(EMPTY_EVENT_WINDOW);
|
|
150
194
|
setError(null);
|
|
151
195
|
setHasOlder(false);
|
|
196
|
+
setHasNewer(false);
|
|
152
197
|
sessionStatusRef.current = { sequence: after, status: null };
|
|
153
198
|
setSessionStatusProjection(null);
|
|
154
199
|
setLoadingOlder(false);
|
|
200
|
+
setLoadingNewer(false);
|
|
201
|
+
setLoadingOldest(false);
|
|
202
|
+
setLoadingLatest(false);
|
|
155
203
|
setInitialLoading(true);
|
|
156
204
|
lastSequenceRef.current = after;
|
|
157
205
|
streamResumeSequenceRef.current = after;
|
|
158
206
|
oldestSequenceRef.current = null;
|
|
207
|
+
newestSequenceRef.current = null;
|
|
159
208
|
hasOlderRef.current = false;
|
|
209
|
+
hasNewerRef.current = false;
|
|
160
210
|
loadingOlderRef.current = false;
|
|
211
|
+
loadingNewerRef.current = false;
|
|
212
|
+
loadingOldestRef.current = false;
|
|
213
|
+
loadingLatestRef.current = false;
|
|
214
|
+
viewModeRef.current = "live";
|
|
215
|
+
setViewMode("live");
|
|
161
216
|
initialWindowLoadedRef.current = false;
|
|
162
217
|
}
|
|
163
218
|
// AbortController is advisory: custom SDK clients and async iterators may
|
|
@@ -173,6 +228,13 @@ export function useSessionEvents(
|
|
|
173
228
|
setConnectionState("idle");
|
|
174
229
|
return;
|
|
175
230
|
}
|
|
231
|
+
// History view owns a non-tip window; do not open SSE (it would replay the
|
|
232
|
+
// entire gap into the browser). jumpToLatest / catching loadNewer resume live.
|
|
233
|
+
if (viewMode === "history") {
|
|
234
|
+
setConnectionState("idle");
|
|
235
|
+
setInitialLoading(false);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
176
238
|
const controller = new AbortController();
|
|
177
239
|
const isCurrent = () => generationRef.current === generation && !controller.signal.aborted;
|
|
178
240
|
streamAbortRef.current = controller;
|
|
@@ -227,6 +289,7 @@ export function useSessionEvents(
|
|
|
227
289
|
eventWindowRef.current = retained;
|
|
228
290
|
setEventWindow(retained);
|
|
229
291
|
oldestSequenceRef.current = retained.events[0]?.sequence ?? null;
|
|
292
|
+
newestSequenceRef.current = retained.events.at(-1)?.sequence ?? null;
|
|
230
293
|
if (retained.truncated) {
|
|
231
294
|
hasOlderRef.current = true;
|
|
232
295
|
setHasOlder(true);
|
|
@@ -259,11 +322,14 @@ export function useSessionEvents(
|
|
|
259
322
|
eventWindowRef.current = retained;
|
|
260
323
|
setEventWindow(retained);
|
|
261
324
|
oldestSequenceRef.current = retained.events[0]?.sequence ?? window.oldestSequence;
|
|
325
|
+
newestSequenceRef.current = retained.events.at(-1)?.sequence ?? window.newestSequence;
|
|
262
326
|
hasOlderRef.current = window.hasOlder || retained.truncated;
|
|
327
|
+
hasNewerRef.current = false;
|
|
263
328
|
lastSequenceRef.current = window.newestSequence;
|
|
264
329
|
streamResumeSequenceRef.current = window.newestSequence;
|
|
265
330
|
initialWindowLoadedRef.current = true;
|
|
266
331
|
setHasOlder(window.hasOlder || retained.truncated);
|
|
332
|
+
setHasNewer(false);
|
|
267
333
|
setInitialLoading(false);
|
|
268
334
|
}
|
|
269
335
|
if (fullReplay) {
|
|
@@ -337,11 +403,18 @@ export function useSessionEvents(
|
|
|
337
403
|
fullReplay,
|
|
338
404
|
streamKey,
|
|
339
405
|
streamEpoch,
|
|
406
|
+
viewMode,
|
|
340
407
|
reconcileSession,
|
|
341
408
|
]);
|
|
342
409
|
|
|
410
|
+
const navigationBusy = (): boolean =>
|
|
411
|
+
loadingOlderRef.current ||
|
|
412
|
+
loadingNewerRef.current ||
|
|
413
|
+
loadingOldestRef.current ||
|
|
414
|
+
loadingLatestRef.current;
|
|
415
|
+
|
|
343
416
|
const loadOlder = useCallback(async (): Promise<boolean> => {
|
|
344
|
-
if (!sessionId ||
|
|
417
|
+
if (!sessionId || navigationBusy() || !hasOlderRef.current) {
|
|
345
418
|
return false;
|
|
346
419
|
}
|
|
347
420
|
const before = oldestSequenceRef.current;
|
|
@@ -387,9 +460,11 @@ export function useSessionEvents(
|
|
|
387
460
|
if (retainedOldest === null || retainedOldest >= before) {
|
|
388
461
|
throw new Error("@opengeni/react: loadOlder made no durable sequence progress");
|
|
389
462
|
}
|
|
463
|
+
const retainedNewest = retained.events.at(-1)?.sequence ?? null;
|
|
390
464
|
eventWindowRef.current = retained;
|
|
391
465
|
setEventWindow(retained);
|
|
392
466
|
oldestSequenceRef.current = retainedOldest;
|
|
467
|
+
newestSequenceRef.current = retainedNewest;
|
|
393
468
|
streamResumeSequenceRef.current = maxResumeSequence(retained.events);
|
|
394
469
|
// Oldest-directed eviction can discard newer in-memory rows. That fact
|
|
395
470
|
// keeps windowTruncated true, but it does not imply older durable rows
|
|
@@ -397,16 +472,189 @@ export function useSessionEvents(
|
|
|
397
472
|
const olderStillAvailable = window.hasOlder;
|
|
398
473
|
hasOlderRef.current = olderStillAvailable;
|
|
399
474
|
setHasOlder(olderStillAvailable);
|
|
400
|
-
|
|
475
|
+
if (viewModeRef.current === "history") {
|
|
476
|
+
const highWater = lastSequenceRef.current;
|
|
477
|
+
const newer = retainedNewest !== null && (retainedNewest < highWater || retained.truncated);
|
|
478
|
+
hasNewerRef.current = newer;
|
|
479
|
+
setHasNewer(newer);
|
|
480
|
+
} else {
|
|
481
|
+
// Live mode keeps the historical contract: reconnect and replay the
|
|
482
|
+
// evicted tip through SSE so the window stays one contiguous suffix.
|
|
483
|
+
setStreamEpoch((epoch) => epoch + 1);
|
|
484
|
+
}
|
|
401
485
|
return olderStillAvailable;
|
|
402
486
|
} finally {
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
487
|
+
loadingOlderRef.current = false;
|
|
488
|
+
setLoadingOlder(false);
|
|
489
|
+
}
|
|
490
|
+
}, [client, workspaceId, sessionId]);
|
|
491
|
+
|
|
492
|
+
const loadOldest = useCallback(async (): Promise<boolean> => {
|
|
493
|
+
if (!sessionId || navigationBusy() || !hasOlderRef.current) {
|
|
494
|
+
return false;
|
|
495
|
+
}
|
|
496
|
+
const generation = generationRef.current;
|
|
497
|
+
loadingOldestRef.current = true;
|
|
498
|
+
setLoadingOldest(true);
|
|
499
|
+
try {
|
|
500
|
+
const window = await loadForwardEventWindow(client, workspaceId, sessionId, {
|
|
501
|
+
after: 0,
|
|
502
|
+
pageSize: OLDEST_PAGE_SIZE,
|
|
503
|
+
targetGroups: OLDEST_GROUP_TARGET,
|
|
504
|
+
maxFetches: OLDEST_FETCH_CAP,
|
|
505
|
+
});
|
|
506
|
+
if (generationRef.current !== generation) {
|
|
507
|
+
return false;
|
|
508
|
+
}
|
|
509
|
+
if (window.events.length === 0) {
|
|
510
|
+
hasOlderRef.current = false;
|
|
511
|
+
setHasOlder(false);
|
|
512
|
+
return false;
|
|
406
513
|
}
|
|
514
|
+
streamAbortRef.current?.abort();
|
|
515
|
+
observeSessionStatus(window.events, sessionStatusRef, setSessionStatusProjection);
|
|
516
|
+
// Keep the oldest prefix — never the middle or tip.
|
|
517
|
+
const retained = boundBrowserSessionEventWindow(window.events, { direction: "oldest" });
|
|
518
|
+
const retainedOldest = retained.events[0]?.sequence ?? null;
|
|
519
|
+
const retainedNewest = retained.events.at(-1)?.sequence ?? null;
|
|
520
|
+
eventWindowRef.current = retained;
|
|
521
|
+
setEventWindow(retained);
|
|
522
|
+
oldestSequenceRef.current = retainedOldest;
|
|
523
|
+
newestSequenceRef.current = retainedNewest;
|
|
524
|
+
hasOlderRef.current = false;
|
|
525
|
+
setHasOlder(false);
|
|
526
|
+
const highWater = Math.max(lastSequenceRef.current, retainedNewest ?? 0);
|
|
527
|
+
lastSequenceRef.current = highWater;
|
|
528
|
+
const newer =
|
|
529
|
+
window.hasNewer ||
|
|
530
|
+
retained.truncated ||
|
|
531
|
+
(retainedNewest !== null && retainedNewest < highWater);
|
|
532
|
+
hasNewerRef.current = newer;
|
|
533
|
+
setHasNewer(newer);
|
|
534
|
+
initialWindowLoadedRef.current = true;
|
|
535
|
+
viewModeRef.current = "history";
|
|
536
|
+
setViewMode("history");
|
|
537
|
+
return newer;
|
|
538
|
+
} finally {
|
|
539
|
+
loadingOldestRef.current = false;
|
|
540
|
+
setLoadingOldest(false);
|
|
407
541
|
}
|
|
408
542
|
}, [client, workspaceId, sessionId]);
|
|
409
543
|
|
|
544
|
+
const loadNewer = useCallback(async (): Promise<boolean> => {
|
|
545
|
+
if (!sessionId || navigationBusy() || !hasNewerRef.current) {
|
|
546
|
+
return false;
|
|
547
|
+
}
|
|
548
|
+
const afterSequence = newestSequenceRef.current;
|
|
549
|
+
if (afterSequence === null) {
|
|
550
|
+
hasNewerRef.current = false;
|
|
551
|
+
setHasNewer(false);
|
|
552
|
+
return false;
|
|
553
|
+
}
|
|
554
|
+
const generation = generationRef.current;
|
|
555
|
+
loadingNewerRef.current = true;
|
|
556
|
+
setLoadingNewer(true);
|
|
557
|
+
try {
|
|
558
|
+
const window = await loadForwardEventWindow(client, workspaceId, sessionId, {
|
|
559
|
+
after: afterSequence,
|
|
560
|
+
pageSize: NEWER_PAGE_SIZE,
|
|
561
|
+
targetGroups: NEWER_GROUP_TARGET,
|
|
562
|
+
maxFetches: NEWER_FETCH_CAP,
|
|
563
|
+
});
|
|
564
|
+
if (generationRef.current !== generation) {
|
|
565
|
+
return false;
|
|
566
|
+
}
|
|
567
|
+
if (window.events.length === 0) {
|
|
568
|
+
hasNewerRef.current = false;
|
|
569
|
+
setHasNewer(false);
|
|
570
|
+
// Caught up with durable history — resume the live tip stream.
|
|
571
|
+
streamResumeSequenceRef.current = afterSequence;
|
|
572
|
+
viewModeRef.current = "live";
|
|
573
|
+
setViewMode("live");
|
|
574
|
+
setStreamEpoch((epoch) => epoch + 1);
|
|
575
|
+
return false;
|
|
576
|
+
}
|
|
577
|
+
const current = eventWindowRef.current;
|
|
578
|
+
assertAppendOrder(current.events, window.events);
|
|
579
|
+
observeSessionStatus(window.events, sessionStatusRef, setSessionStatusProjection);
|
|
580
|
+
const previousOldest = current.events[0]?.sequence ?? null;
|
|
581
|
+
// Moving forward through history: keep the newest suffix of the merge.
|
|
582
|
+
const next = boundBrowserSessionEventWindow([...current.events, ...window.events], {
|
|
583
|
+
direction: "newest",
|
|
584
|
+
});
|
|
585
|
+
const retained = {
|
|
586
|
+
...next,
|
|
587
|
+
truncated: current.truncated || next.truncated || window.hasNewer,
|
|
588
|
+
};
|
|
589
|
+
const retainedOldest = retained.events[0]?.sequence ?? null;
|
|
590
|
+
const retainedNewest = retained.events.at(-1)?.sequence ?? null;
|
|
591
|
+
if (retainedNewest === null || retainedNewest <= afterSequence) {
|
|
592
|
+
throw new Error("@opengeni/react: loadNewer made no durable sequence progress");
|
|
593
|
+
}
|
|
594
|
+
eventWindowRef.current = retained;
|
|
595
|
+
setEventWindow(retained);
|
|
596
|
+
oldestSequenceRef.current = retainedOldest;
|
|
597
|
+
newestSequenceRef.current = retainedNewest;
|
|
598
|
+
// Evicting the start while paging forward re-arms hasOlder.
|
|
599
|
+
if (
|
|
600
|
+
retainedOldest !== null &&
|
|
601
|
+
(previousOldest === null ||
|
|
602
|
+
retainedOldest > previousOldest ||
|
|
603
|
+
(retained.truncated &&
|
|
604
|
+
retained.events[0]?.type !== "session.created" &&
|
|
605
|
+
retainedOldest > 1))
|
|
606
|
+
) {
|
|
607
|
+
hasOlderRef.current = true;
|
|
608
|
+
setHasOlder(true);
|
|
609
|
+
}
|
|
610
|
+
const highWater = Math.max(lastSequenceRef.current, retainedNewest);
|
|
611
|
+
lastSequenceRef.current = highWater;
|
|
612
|
+
const newer = window.hasNewer || retainedNewest < highWater;
|
|
613
|
+
hasNewerRef.current = newer;
|
|
614
|
+
setHasNewer(newer);
|
|
615
|
+
if (!newer) {
|
|
616
|
+
streamResumeSequenceRef.current = retainedNewest;
|
|
617
|
+
viewModeRef.current = "live";
|
|
618
|
+
setViewMode("live");
|
|
619
|
+
setStreamEpoch((epoch) => epoch + 1);
|
|
620
|
+
}
|
|
621
|
+
return newer;
|
|
622
|
+
} finally {
|
|
623
|
+
loadingNewerRef.current = false;
|
|
624
|
+
setLoadingNewer(false);
|
|
625
|
+
}
|
|
626
|
+
}, [client, workspaceId, sessionId]);
|
|
627
|
+
|
|
628
|
+
const jumpToLatest = useCallback(async (): Promise<void> => {
|
|
629
|
+
if (!sessionId || navigationBusy()) {
|
|
630
|
+
return;
|
|
631
|
+
}
|
|
632
|
+
loadingLatestRef.current = true;
|
|
633
|
+
setLoadingLatest(true);
|
|
634
|
+
try {
|
|
635
|
+
streamAbortRef.current?.abort();
|
|
636
|
+
hasNewerRef.current = false;
|
|
637
|
+
setHasNewer(false);
|
|
638
|
+
hasOlderRef.current = false;
|
|
639
|
+
setHasOlder(false);
|
|
640
|
+
newestSequenceRef.current = null;
|
|
641
|
+
oldestSequenceRef.current = null;
|
|
642
|
+
eventWindowRef.current = EMPTY_EVENT_WINDOW;
|
|
643
|
+
setEventWindow(EMPTY_EVENT_WINDOW);
|
|
644
|
+
initialWindowLoadedRef.current = false;
|
|
645
|
+
streamResumeSequenceRef.current = after;
|
|
646
|
+
setInitialLoading(true);
|
|
647
|
+
viewModeRef.current = "live";
|
|
648
|
+
setViewMode("live");
|
|
649
|
+
setStreamEpoch((epoch) => epoch + 1);
|
|
650
|
+
// Effect reloads the tip. The tip fetch itself is owned by the effect.
|
|
651
|
+
await Promise.resolve();
|
|
652
|
+
} finally {
|
|
653
|
+
loadingLatestRef.current = false;
|
|
654
|
+
setLoadingLatest(false);
|
|
655
|
+
}
|
|
656
|
+
}, [after, sessionId]);
|
|
657
|
+
|
|
410
658
|
const identityMatches = stateStreamKey === streamKey;
|
|
411
659
|
const visibleEvents = identityMatches ? eventWindow.events : EMPTY_EVENTS;
|
|
412
660
|
const timeline = useMemo(() => buildTimeline(visibleEvents), [visibleEvents]);
|
|
@@ -423,6 +671,13 @@ export function useSessionEvents(
|
|
|
423
671
|
hasOlder: !identityMatches ? false : hasOlder,
|
|
424
672
|
loadingOlder: !identityMatches ? false : loadingOlder,
|
|
425
673
|
loadOlder,
|
|
674
|
+
hasNewer: !identityMatches ? false : hasNewer,
|
|
675
|
+
loadingNewer: !identityMatches ? false : loadingNewer,
|
|
676
|
+
loadNewer,
|
|
677
|
+
loadingOldest: !identityMatches ? false : loadingOldest,
|
|
678
|
+
loadOldest,
|
|
679
|
+
loadingLatest: !identityMatches ? false : loadingLatest,
|
|
680
|
+
jumpToLatest,
|
|
426
681
|
error: identityMatches ? error : null,
|
|
427
682
|
};
|
|
428
683
|
}
|
|
@@ -737,6 +992,13 @@ type LoadedEventWindow = {
|
|
|
737
992
|
hasOlder: boolean;
|
|
738
993
|
};
|
|
739
994
|
|
|
995
|
+
type LoadedForwardEventWindow = {
|
|
996
|
+
events: SessionEvent[];
|
|
997
|
+
oldestSequence: number | null;
|
|
998
|
+
newestSequence: number;
|
|
999
|
+
hasNewer: boolean;
|
|
1000
|
+
};
|
|
1001
|
+
|
|
740
1002
|
async function loadEventWindow(
|
|
741
1003
|
client: EmbeddedSessionClientLike,
|
|
742
1004
|
workspaceId: string,
|
|
@@ -835,6 +1097,59 @@ function findBoundaryIndex(events: SessionEvent[]): number {
|
|
|
835
1097
|
return -1;
|
|
836
1098
|
}
|
|
837
1099
|
|
|
1100
|
+
/**
|
|
1101
|
+
* Forward (oldest→newer) density-bounded window. Used for jump-to-start and
|
|
1102
|
+
* loadNewer so the browser never walks the whole middle gap.
|
|
1103
|
+
*/
|
|
1104
|
+
async function loadForwardEventWindow(
|
|
1105
|
+
client: EmbeddedSessionClientLike,
|
|
1106
|
+
workspaceId: string,
|
|
1107
|
+
sessionId: string,
|
|
1108
|
+
options: {
|
|
1109
|
+
after: number;
|
|
1110
|
+
pageSize: number;
|
|
1111
|
+
targetGroups: number;
|
|
1112
|
+
maxFetches: number;
|
|
1113
|
+
signal?: AbortSignal;
|
|
1114
|
+
},
|
|
1115
|
+
): Promise<LoadedForwardEventWindow> {
|
|
1116
|
+
let cursor = options.after;
|
|
1117
|
+
let buffer: SessionEvent[] = [];
|
|
1118
|
+
let fetches = 0;
|
|
1119
|
+
let reachedEnd = false;
|
|
1120
|
+
|
|
1121
|
+
while (fetches < options.maxFetches) {
|
|
1122
|
+
if (buffer.length > 0 && groupCount(buffer) >= options.targetGroups) {
|
|
1123
|
+
break;
|
|
1124
|
+
}
|
|
1125
|
+
const page = await loadNextPage(client, workspaceId, sessionId, cursor, {
|
|
1126
|
+
pageSize: options.pageSize,
|
|
1127
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
1128
|
+
});
|
|
1129
|
+
fetches += 1;
|
|
1130
|
+
if (page.length === 0) {
|
|
1131
|
+
reachedEnd = true;
|
|
1132
|
+
break;
|
|
1133
|
+
}
|
|
1134
|
+
assertAscending(page);
|
|
1135
|
+
buffer = [...buffer, ...page];
|
|
1136
|
+
cursor = page[page.length - 1]!.sequence;
|
|
1137
|
+
if (page.length < options.pageSize) {
|
|
1138
|
+
reachedEnd = true;
|
|
1139
|
+
break;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
const oldest = buffer[0] ?? null;
|
|
1144
|
+
const newest = buffer[buffer.length - 1] ?? null;
|
|
1145
|
+
return {
|
|
1146
|
+
events: buffer,
|
|
1147
|
+
oldestSequence: oldest?.sequence ?? null,
|
|
1148
|
+
newestSequence: newest ? maxResumeSequence(buffer) : 0,
|
|
1149
|
+
hasNewer: buffer.length > 0 && !reachedEnd,
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1152
|
+
|
|
838
1153
|
type PreviousPage = SessionEvent[] & { requested: number };
|
|
839
1154
|
|
|
840
1155
|
async function loadPreviousPage(
|
|
@@ -870,6 +1185,36 @@ async function loadPreviousPage(
|
|
|
870
1185
|
return Object.assign(page, { requested });
|
|
871
1186
|
}
|
|
872
1187
|
|
|
1188
|
+
async function loadNextPage(
|
|
1189
|
+
client: EmbeddedSessionClientLike,
|
|
1190
|
+
workspaceId: string,
|
|
1191
|
+
sessionId: string,
|
|
1192
|
+
after: number,
|
|
1193
|
+
options: {
|
|
1194
|
+
pageSize: number;
|
|
1195
|
+
signal?: AbortSignal;
|
|
1196
|
+
},
|
|
1197
|
+
): Promise<PreviousPage> {
|
|
1198
|
+
if (options.signal?.aborted) {
|
|
1199
|
+
throw abortError();
|
|
1200
|
+
}
|
|
1201
|
+
const requested = options.pageSize;
|
|
1202
|
+
if (requested === 0) {
|
|
1203
|
+
return Object.assign([], { requested });
|
|
1204
|
+
}
|
|
1205
|
+
const page = await client.listEvents(workspaceId, sessionId, {
|
|
1206
|
+
after,
|
|
1207
|
+
limit: requested,
|
|
1208
|
+
compact: true,
|
|
1209
|
+
direction: "after",
|
|
1210
|
+
payloadMode: "full",
|
|
1211
|
+
});
|
|
1212
|
+
if (options.signal?.aborted) {
|
|
1213
|
+
throw abortError();
|
|
1214
|
+
}
|
|
1215
|
+
return Object.assign(page, { requested });
|
|
1216
|
+
}
|
|
1217
|
+
|
|
873
1218
|
function groupCount(events: SessionEvent[]): number {
|
|
874
1219
|
if (events.length === 0) {
|
|
875
1220
|
return 0;
|
|
@@ -912,6 +1257,15 @@ function assertPrependOrder(existing: SessionEvent[], older: SessionEvent[]): vo
|
|
|
912
1257
|
}
|
|
913
1258
|
}
|
|
914
1259
|
|
|
1260
|
+
function assertAppendOrder(existing: SessionEvent[], newer: SessionEvent[]): void {
|
|
1261
|
+
if (!shouldAssertDevelopment() || existing.length === 0 || newer.length === 0) {
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
if (newer[0]!.sequence <= existing[existing.length - 1]!.sequence) {
|
|
1265
|
+
throw new Error("@opengeni/react: loadNewer returned overlapping session events");
|
|
1266
|
+
}
|
|
1267
|
+
}
|
|
1268
|
+
|
|
915
1269
|
function shouldAssertDevelopment(): boolean {
|
|
916
1270
|
const processEnv = (globalThis as { process?: { env?: { NODE_ENV?: string } } }).process?.env;
|
|
917
1271
|
return processEnv !== undefined && processEnv.NODE_ENV !== "production";
|