@opengeni/react 0.15.0 → 0.20.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 +110 -10
- package/dist/{chunk-TOJR776I.js → chunk-6BXBGJ3B.js} +45 -369
- package/dist/chunk-6BXBGJ3B.js.map +1 -0
- package/dist/chunk-6UT5OC3P.js +2133 -0
- package/dist/chunk-6UT5OC3P.js.map +1 -0
- package/dist/chunk-EBTT3MYN.js +92 -0
- package/dist/chunk-EBTT3MYN.js.map +1 -0
- package/dist/chunk-HHFA4AFX.js +425 -0
- package/dist/chunk-HHFA4AFX.js.map +1 -0
- package/dist/chunk-LAZ2A743.js +2393 -0
- package/dist/chunk-LAZ2A743.js.map +1 -0
- package/dist/chunk-MRSECXRP.js +563 -0
- package/dist/chunk-MRSECXRP.js.map +1 -0
- package/dist/chunk-TK7G6XLT.js +18 -0
- package/dist/chunk-TK7G6XLT.js.map +1 -0
- package/dist/composer-DvUaa5ki.d.ts +585 -0
- package/dist/composer.d.ts +6 -0
- package/dist/composer.js +56 -0
- package/dist/composer.js.map +1 -0
- package/dist/index.d.ts +1288 -1785
- package/dist/index.js +7015 -8806
- package/dist/index.js.map +1 -1
- package/dist/machines.d.ts +412 -3
- package/dist/machines.js +3 -1
- package/dist/noto-sans-arabic-loader-IA2T4X2A.js +21 -0
- package/dist/noto-sans-arabic-loader-IA2T4X2A.js.map +1 -0
- package/dist/noto-sans-jp-loader.generated-HFGLYBR3.js +292 -0
- package/dist/noto-sans-jp-loader.generated-HFGLYBR3.js.map +1 -0
- package/dist/queue-surface-implementation-NQMV2ML3.js +890 -0
- package/dist/queue-surface-implementation-NQMV2ML3.js.map +1 -0
- package/dist/session-Gd4iVPYw.d.ts +425 -0
- package/dist/session-context-DIFFlXKc.d.ts +52 -0
- package/dist/session.d.ts +4 -0
- package/dist/session.js +50 -0
- package/dist/session.js.map +1 -0
- package/dist/use-composer-BCfm4mzX.d.ts +112 -0
- package/package.json +20 -4
- package/src/approvals.ts +5 -3
- package/src/client.ts +40 -0
- package/src/components/approval-surface.tsx +198 -0
- package/src/components/chat-composer.tsx +94 -993
- package/src/components/code-editor.tsx +104 -26
- package/src/components/command-palette.tsx +8 -2
- package/src/components/composer-transcription-control.tsx +211 -0
- package/src/components/composer.tsx +1536 -0
- package/src/components/desktop-viewer.tsx +3 -3
- package/src/components/enrollment-consent.tsx +2 -2
- package/src/components/file-browser.tsx +367 -29
- package/src/components/human-input-form.tsx +417 -0
- package/src/components/machine-card.tsx +1 -1
- package/src/components/machines-dashboard.tsx +3 -3
- package/src/components/message-timeline.tsx +15 -7
- package/src/components/model-picker.tsx +12 -3
- package/src/components/pierre-diff.tsx +32 -6
- package/src/components/queue-surface-implementation.tsx +1041 -0
- package/src/components/queue-surface-state.tsx +94 -0
- package/src/components/queue-surface.tsx +41 -559
- package/src/components/sandbox-files.tsx +138 -14
- package/src/components/sandbox-terminal.tsx +8 -2
- package/src/components/sandbox-workspace.tsx +339 -119
- package/src/components/workbench-changes.tsx +84 -33
- package/src/components/workspace-dock.tsx +188 -18
- package/src/composer.ts +60 -0
- package/src/hooks/internal.ts +115 -34
- package/src/hooks/use-composer.ts +335 -104
- package/src/hooks/use-human-input.ts +131 -0
- package/src/hooks/use-machine-chip.ts +2 -2
- package/src/hooks/use-machines.ts +27 -11
- package/src/hooks/use-sandbox-files.ts +310 -62
- package/src/hooks/use-sandbox-git.ts +154 -40
- package/src/hooks/use-sandbox-terminal.ts +28 -6
- package/src/hooks/use-session-capabilities.ts +38 -9
- package/src/hooks/use-session-control.ts +37 -8
- package/src/hooks/use-session-events.ts +522 -48
- package/src/hooks/use-slash-commands.ts +38 -38
- package/src/hooks/use-transcription.ts +757 -0
- package/src/hooks/use-turn-queue.ts +134 -66
- package/src/hooks/use-windowed-sections.ts +2 -2
- package/src/hooks/use-workspace-capture.ts +146 -40
- package/src/hooks/use-workspace-edit.ts +50 -14
- package/src/hooks/use-workspace-sessions.ts +3 -0
- package/src/human-input.ts +72 -0
- package/src/index.ts +76 -6
- package/src/lib/noto-sans-arabic-loader.ts +21 -0
- package/src/lib/noto-sans-jp-loader.generated.ts +295 -0
- package/src/lib/use-portal-token-style.ts +51 -0
- package/src/lib/use-unicode-fonts.ts +57 -0
- package/src/provider.tsx +4 -69
- package/src/session-context.ts +135 -0
- package/src/session.ts +79 -0
- package/src/timeline/parsers.ts +35 -0
- package/src/timeline/projection.ts +12 -4
- package/src/timeline/tool-renderers.tsx +41 -0
- package/src/timeline/turn-summary.tsx +2 -2
- package/src/timeline/types.ts +16 -8
- package/styles/index.css +56 -0
- package/styles/index.d.ts +2 -0
- package/styles/tokens.css +11 -6
- package/styles/tokens.d.ts +2 -0
- package/dist/chunk-TOJR776I.js.map +0 -1
- package/dist/machines-BpdwuQcD.d.ts +0 -449
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
import type { SessionEvent, SessionStatus, StreamConnectionState } from "@opengeni/sdk";
|
|
2
2
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
sessionStatusFromEvents,
|
|
8
|
-
type TimelineItem,
|
|
9
|
-
} from "../timeline";
|
|
10
|
-
import type { SessionClientLike } from "../client";
|
|
3
|
+
import { useEmbeddedSession, type EmbeddedSessionClientOverride } from "../session-context";
|
|
4
|
+
import { buildTimeline, groupTimeline, sessionStatusFromEvents } from "../timeline/projection";
|
|
5
|
+
import type { TimelineItem } from "../timeline/types";
|
|
6
|
+
import type { EmbeddedSessionClientLike } from "../client";
|
|
11
7
|
|
|
12
8
|
export type SessionEventsConnectionState = StreamConnectionState | "idle" | "ended" | "error";
|
|
13
9
|
|
|
14
|
-
export type UseSessionEventsOptions =
|
|
10
|
+
export type UseSessionEventsOptions = EmbeddedSessionClientOverride & {
|
|
15
11
|
/** Resume after this sequence (exclusive). Nonzero keeps full replay/resume semantics. */
|
|
16
12
|
after?: number | undefined;
|
|
17
13
|
/** Load a bounded tail by default, or opt back into full replay from `after`. */
|
|
@@ -30,6 +26,10 @@ export type UseSessionEventsResult = {
|
|
|
30
26
|
connectionState: SessionEventsConnectionState;
|
|
31
27
|
/** Highest sequence seen so far (0 before the first event). */
|
|
32
28
|
lastSequence: number;
|
|
29
|
+
/** Exact serialized bytes retained in the current browser event window. */
|
|
30
|
+
windowBytes: number;
|
|
31
|
+
/** Whether older delivered events were evicted from the browser window. */
|
|
32
|
+
windowTruncated: boolean;
|
|
33
33
|
/** True until the initial tail window has been applied (windowed mode). */
|
|
34
34
|
initialLoading: boolean;
|
|
35
35
|
/** Whether older durable events are available before the current window. */
|
|
@@ -48,6 +48,49 @@ const OLDER_GROUP_TARGET = 32;
|
|
|
48
48
|
const OLDER_FETCH_CAP = 2;
|
|
49
49
|
const BOUNDARY_PAGE_CAP = 4;
|
|
50
50
|
const EMPTY_EVENTS: SessionEvent[] = [];
|
|
51
|
+
const encoder = new TextEncoder();
|
|
52
|
+
const decoder = new TextDecoder();
|
|
53
|
+
|
|
54
|
+
const BROWSER_EVENT_TYPE_MAX_BYTES = 256;
|
|
55
|
+
const BROWSER_EVENT_ID_MAX_BYTES = 256;
|
|
56
|
+
const BROWSER_EVENT_CLIENT_ID_MAX_BYTES = 4 * 1024;
|
|
57
|
+
const BROWSER_EVENT_DUPLICATE_REASON_MAX_BYTES = 4 * 1024;
|
|
58
|
+
const BROWSER_EVENT_PAYLOAD_PREVIEW_MAX_BYTES = 48 * 1024;
|
|
59
|
+
const BROWSER_EVENT_PAYLOAD_IDENTITY_FIELDS = [
|
|
60
|
+
"id",
|
|
61
|
+
"callId",
|
|
62
|
+
"call_id",
|
|
63
|
+
"name",
|
|
64
|
+
"toolName",
|
|
65
|
+
"status",
|
|
66
|
+
"code",
|
|
67
|
+
"isError",
|
|
68
|
+
"stream",
|
|
69
|
+
"commandId",
|
|
70
|
+
"sequence",
|
|
71
|
+
"coalescedUntil",
|
|
72
|
+
"coalescedCount",
|
|
73
|
+
"firstSequence",
|
|
74
|
+
"lastSequence",
|
|
75
|
+
] as const;
|
|
76
|
+
|
|
77
|
+
export const SESSION_EVENT_BROWSER_MAX_BYTES = 8 * 1024 * 1024;
|
|
78
|
+
export const SESSION_EVENT_BROWSER_MAX_COUNT = 10_000;
|
|
79
|
+
export const SESSION_EVENT_BROWSER_SINGLE_EVENT_MAX_BYTES = 96 * 1024;
|
|
80
|
+
export const SESSION_EVENT_BROWSER_PENDING_MAX_BYTES = 1024 * 1024;
|
|
81
|
+
export const SESSION_EVENT_BROWSER_PENDING_MAX_COUNT = 256;
|
|
82
|
+
|
|
83
|
+
export type BrowserSessionEventWindow = {
|
|
84
|
+
events: SessionEvent[];
|
|
85
|
+
bytes: number;
|
|
86
|
+
truncated: boolean;
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const EMPTY_EVENT_WINDOW: BrowserSessionEventWindow = {
|
|
90
|
+
events: EMPTY_EVENTS,
|
|
91
|
+
bytes: 2,
|
|
92
|
+
truncated: false,
|
|
93
|
+
};
|
|
51
94
|
|
|
52
95
|
/**
|
|
53
96
|
* Live-stream a session's event log with replay-by-sequence, reconnect, and
|
|
@@ -58,25 +101,40 @@ export function useSessionEvents(
|
|
|
58
101
|
sessionId: string | null | undefined,
|
|
59
102
|
options: UseSessionEventsOptions = {},
|
|
60
103
|
): UseSessionEventsResult {
|
|
61
|
-
const { client, workspaceId, reconcileSession } =
|
|
104
|
+
const { client, workspaceId, reconcileSession } = useEmbeddedSession(options);
|
|
62
105
|
const enabled = options.enabled ?? true;
|
|
63
106
|
const after = options.after ?? 0;
|
|
64
107
|
const replay = options.replay ?? "windowed";
|
|
65
108
|
const fullReplay = replay === "full" || after !== 0;
|
|
66
109
|
const streamKey = `${workspaceId}\u0000${sessionId ?? ""}\u0000${after}\u0000${fullReplay ? "full" : "windowed"}`;
|
|
67
110
|
|
|
68
|
-
const [
|
|
111
|
+
const [eventWindow, setEventWindow] = useState<BrowserSessionEventWindow>(EMPTY_EVENT_WINDOW);
|
|
69
112
|
const [connectionState, setConnectionState] = useState<SessionEventsConnectionState>("idle");
|
|
70
113
|
const [error, setError] = useState<Error | null>(null);
|
|
71
114
|
const [hasOlder, setHasOlder] = useState(false);
|
|
115
|
+
const [sessionStatusProjection, setSessionStatusProjection] = useState<SessionStatus | null>(
|
|
116
|
+
null,
|
|
117
|
+
);
|
|
72
118
|
const [initialLoading, setInitialLoading] = useState(true);
|
|
73
119
|
const [loadingOlder, setLoadingOlder] = useState(false);
|
|
120
|
+
const [streamEpoch, setStreamEpoch] = useState(0);
|
|
74
121
|
const lastSequenceRef = useRef(after);
|
|
122
|
+
const streamResumeSequenceRef = useRef(after);
|
|
75
123
|
const oldestSequenceRef = useRef<number | null>(null);
|
|
76
124
|
const hasOlderRef = useRef(false);
|
|
77
125
|
const loadingOlderRef = useRef(false);
|
|
126
|
+
const initialWindowLoadedRef = useRef(false);
|
|
127
|
+
const streamAbortRef = useRef<AbortController | null>(null);
|
|
78
128
|
const streamKeyRef = useRef<string | null>(null);
|
|
79
129
|
const generationRef = useRef(0);
|
|
130
|
+
const eventWindowRef = useRef<BrowserSessionEventWindow>(EMPTY_EVENT_WINDOW);
|
|
131
|
+
const sessionStatusRef = useRef<{
|
|
132
|
+
sequence: number;
|
|
133
|
+
status: SessionStatus | null;
|
|
134
|
+
}>({
|
|
135
|
+
sequence: after,
|
|
136
|
+
status: null,
|
|
137
|
+
});
|
|
80
138
|
// Effects reset state after commit. Tag the state so the first render for a
|
|
81
139
|
// new stream identity cannot expose the previous session's event log.
|
|
82
140
|
const [stateStreamKey, setStateStreamKey] = useState(streamKey);
|
|
@@ -86,53 +144,102 @@ export function useSessionEvents(
|
|
|
86
144
|
// pausing via `enabled: false` keeps the timeline visible.
|
|
87
145
|
if (streamKeyRef.current !== streamKey) {
|
|
88
146
|
streamKeyRef.current = streamKey;
|
|
89
|
-
generationRef.current += 1;
|
|
90
147
|
setStateStreamKey(streamKey);
|
|
91
|
-
|
|
148
|
+
eventWindowRef.current = EMPTY_EVENT_WINDOW;
|
|
149
|
+
setEventWindow(EMPTY_EVENT_WINDOW);
|
|
92
150
|
setError(null);
|
|
93
151
|
setHasOlder(false);
|
|
152
|
+
sessionStatusRef.current = { sequence: after, status: null };
|
|
153
|
+
setSessionStatusProjection(null);
|
|
94
154
|
setLoadingOlder(false);
|
|
95
155
|
setInitialLoading(true);
|
|
96
156
|
lastSequenceRef.current = after;
|
|
157
|
+
streamResumeSequenceRef.current = after;
|
|
97
158
|
oldestSequenceRef.current = null;
|
|
98
159
|
hasOlderRef.current = false;
|
|
99
160
|
loadingOlderRef.current = false;
|
|
161
|
+
initialWindowLoadedRef.current = false;
|
|
162
|
+
}
|
|
163
|
+
// AbortController is advisory: custom SDK clients and async iterators may
|
|
164
|
+
// ignore it and resolve/yield after cleanup. Fence every effect instance so
|
|
165
|
+
// only the newest dependency generation can mutate refs or React state.
|
|
166
|
+
const generation = generationRef.current + 1;
|
|
167
|
+
generationRef.current = generation;
|
|
168
|
+
if (loadingOlderRef.current) {
|
|
169
|
+
loadingOlderRef.current = false;
|
|
170
|
+
setLoadingOlder(false);
|
|
100
171
|
}
|
|
101
172
|
if (!sessionId || !enabled) {
|
|
102
173
|
setConnectionState("idle");
|
|
103
174
|
return;
|
|
104
175
|
}
|
|
105
176
|
const controller = new AbortController();
|
|
177
|
+
const isCurrent = () => generationRef.current === generation && !controller.signal.aborted;
|
|
178
|
+
streamAbortRef.current = controller;
|
|
106
179
|
// Batch yielded events into one React update per flush window so a long
|
|
107
|
-
// replay (thousands of events) does not render per event.
|
|
180
|
+
// replay (thousands of events) does not render per event. Project every
|
|
181
|
+
// event before retaining it here and synchronously flush at independent
|
|
182
|
+
// count+byte high-water marks: a synchronously yielding async iterator can
|
|
183
|
+
// otherwise starve the 16 ms timer and grow this pre-React buffer without
|
|
184
|
+
// bound even though the final browser window is bounded.
|
|
108
185
|
let pending: SessionEvent[] = [];
|
|
186
|
+
let pendingBytes = 2; // []
|
|
109
187
|
let flushTimer: ReturnType<typeof setTimeout> | null = null;
|
|
110
188
|
const flush = () => {
|
|
111
|
-
|
|
189
|
+
// A synchronous count/byte high-water flush can run before the scheduled
|
|
190
|
+
// callback gets a macrotask. Cancel that callback before clearing its
|
|
191
|
+
// handle so a long synchronously yielding replay retains at most one
|
|
192
|
+
// timer rather than one stale callback per flushed batch.
|
|
193
|
+
if (flushTimer !== null) {
|
|
194
|
+
clearTimeout(flushTimer);
|
|
195
|
+
flushTimer = null;
|
|
196
|
+
}
|
|
197
|
+
if (!isCurrent()) {
|
|
198
|
+
pending = [];
|
|
199
|
+
pendingBytes = 2;
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
112
202
|
if (pending.length === 0) {
|
|
113
203
|
return;
|
|
114
204
|
}
|
|
115
205
|
const batch = pending;
|
|
116
206
|
pending = [];
|
|
207
|
+
pendingBytes = 2;
|
|
117
208
|
// The resume cursor only advances with delivered batches: events still
|
|
118
209
|
// sitting in `pending` when the stream is torn down are re-fetched on
|
|
119
210
|
// the next connect instead of being skipped.
|
|
120
211
|
const lastInBatch = batch[batch.length - 1];
|
|
121
212
|
if (lastInBatch) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
213
|
+
const batchResumeSequence = Math.max(...batch.map(eventResumeSequence));
|
|
214
|
+
lastSequenceRef.current = Math.max(lastSequenceRef.current, batchResumeSequence);
|
|
215
|
+
streamResumeSequenceRef.current = Math.max(
|
|
216
|
+
streamResumeSequenceRef.current,
|
|
217
|
+
batchResumeSequence,
|
|
125
218
|
);
|
|
126
219
|
}
|
|
127
|
-
|
|
220
|
+
observeSessionStatus(batch, sessionStatusRef, setSessionStatusProjection);
|
|
221
|
+
const current = eventWindowRef.current;
|
|
222
|
+
const next = boundBrowserSessionEventWindow([...current.events, ...batch]);
|
|
223
|
+
const retained = {
|
|
224
|
+
...next,
|
|
225
|
+
truncated: current.truncated || next.truncated,
|
|
226
|
+
};
|
|
227
|
+
eventWindowRef.current = retained;
|
|
228
|
+
setEventWindow(retained);
|
|
229
|
+
oldestSequenceRef.current = retained.events[0]?.sequence ?? null;
|
|
230
|
+
if (retained.truncated) {
|
|
231
|
+
hasOlderRef.current = true;
|
|
232
|
+
setHasOlder(true);
|
|
233
|
+
}
|
|
128
234
|
};
|
|
129
235
|
const scheduleFlush = () => {
|
|
236
|
+
if (!isCurrent()) return;
|
|
130
237
|
flushTimer ??= setTimeout(flush, 16);
|
|
131
238
|
};
|
|
132
239
|
|
|
133
240
|
void (async () => {
|
|
134
241
|
try {
|
|
135
|
-
if (!fullReplay) {
|
|
242
|
+
if (!fullReplay && !initialWindowLoadedRef.current) {
|
|
136
243
|
setConnectionState("connecting");
|
|
137
244
|
// First paint is ONE compact fetch — the newest window, revealed at
|
|
138
245
|
// the bottom in a few hundred ms. Deeper history loads only when the
|
|
@@ -144,39 +251,64 @@ export function useSessionEvents(
|
|
|
144
251
|
maxFetches: INITIAL_FETCH_CAP,
|
|
145
252
|
signal: controller.signal,
|
|
146
253
|
});
|
|
147
|
-
if (
|
|
254
|
+
if (!isCurrent()) {
|
|
148
255
|
return;
|
|
149
256
|
}
|
|
150
|
-
|
|
151
|
-
|
|
257
|
+
observeSessionStatus(window.events, sessionStatusRef, setSessionStatusProjection);
|
|
258
|
+
const retained = boundBrowserSessionEventWindow(window.events);
|
|
259
|
+
eventWindowRef.current = retained;
|
|
260
|
+
setEventWindow(retained);
|
|
261
|
+
oldestSequenceRef.current = retained.events[0]?.sequence ?? window.oldestSequence;
|
|
262
|
+
hasOlderRef.current = window.hasOlder || retained.truncated;
|
|
152
263
|
lastSequenceRef.current = window.newestSequence;
|
|
153
|
-
|
|
154
|
-
|
|
264
|
+
streamResumeSequenceRef.current = window.newestSequence;
|
|
265
|
+
initialWindowLoadedRef.current = true;
|
|
266
|
+
setHasOlder(window.hasOlder || retained.truncated);
|
|
155
267
|
setInitialLoading(false);
|
|
156
268
|
}
|
|
157
269
|
if (fullReplay) {
|
|
158
270
|
setInitialLoading(false);
|
|
159
271
|
}
|
|
160
272
|
const stream = client.streamEvents(workspaceId, sessionId, {
|
|
161
|
-
after:
|
|
273
|
+
after: streamResumeSequenceRef.current,
|
|
162
274
|
signal: controller.signal,
|
|
163
275
|
beforeLive: async () => await reconcileSession(sessionId),
|
|
164
276
|
onStateChange: (state) => {
|
|
165
|
-
if (
|
|
277
|
+
if (isCurrent()) {
|
|
166
278
|
setConnectionState(state);
|
|
167
279
|
}
|
|
168
280
|
},
|
|
169
281
|
});
|
|
170
282
|
for await (const event of stream) {
|
|
171
|
-
|
|
172
|
-
|
|
283
|
+
if (!isCurrent()) break;
|
|
284
|
+
const boundedEvent = boundBrowserLegacyEvent(event);
|
|
285
|
+
const boundedEventBytes = browserJsonBytes(boundedEvent);
|
|
286
|
+
const separatorBytes = pending.length === 0 ? 0 : 1;
|
|
287
|
+
if (
|
|
288
|
+
pending.length > 0 &&
|
|
289
|
+
(pending.length >= SESSION_EVENT_BROWSER_PENDING_MAX_COUNT ||
|
|
290
|
+
pendingBytes + separatorBytes + boundedEventBytes >
|
|
291
|
+
SESSION_EVENT_BROWSER_PENDING_MAX_BYTES)
|
|
292
|
+
) {
|
|
293
|
+
flush();
|
|
294
|
+
}
|
|
295
|
+
pending.push(boundedEvent);
|
|
296
|
+
pendingBytes += (pending.length === 1 ? 0 : 1) + boundedEventBytes;
|
|
297
|
+
if (
|
|
298
|
+
pending.length >= SESSION_EVENT_BROWSER_PENDING_MAX_COUNT ||
|
|
299
|
+
pendingBytes >= SESSION_EVENT_BROWSER_PENDING_MAX_BYTES
|
|
300
|
+
) {
|
|
301
|
+
flush();
|
|
302
|
+
} else {
|
|
303
|
+
scheduleFlush();
|
|
304
|
+
}
|
|
173
305
|
}
|
|
174
|
-
if (
|
|
306
|
+
if (isCurrent()) {
|
|
175
307
|
flush();
|
|
176
308
|
setConnectionState("ended");
|
|
177
309
|
}
|
|
178
310
|
} catch (cause) {
|
|
179
|
-
if (
|
|
311
|
+
if (isCurrent()) {
|
|
180
312
|
flush();
|
|
181
313
|
setError(cause instanceof Error ? cause : new Error(String(cause)));
|
|
182
314
|
setConnectionState("error");
|
|
@@ -186,14 +318,30 @@ export function useSessionEvents(
|
|
|
186
318
|
|
|
187
319
|
return () => {
|
|
188
320
|
controller.abort();
|
|
321
|
+
if (generationRef.current === generation) {
|
|
322
|
+
generationRef.current = generation + 1;
|
|
323
|
+
}
|
|
324
|
+
if (streamAbortRef.current === controller) {
|
|
325
|
+
streamAbortRef.current = null;
|
|
326
|
+
}
|
|
189
327
|
if (flushTimer !== null) {
|
|
190
328
|
clearTimeout(flushTimer);
|
|
191
329
|
}
|
|
192
330
|
};
|
|
193
|
-
}, [
|
|
331
|
+
}, [
|
|
332
|
+
client,
|
|
333
|
+
workspaceId,
|
|
334
|
+
sessionId,
|
|
335
|
+
after,
|
|
336
|
+
enabled,
|
|
337
|
+
fullReplay,
|
|
338
|
+
streamKey,
|
|
339
|
+
streamEpoch,
|
|
340
|
+
reconcileSession,
|
|
341
|
+
]);
|
|
194
342
|
|
|
195
343
|
const loadOlder = useCallback(async (): Promise<boolean> => {
|
|
196
|
-
if (!sessionId ||
|
|
344
|
+
if (!sessionId || loadingOlderRef.current || !hasOlderRef.current) {
|
|
197
345
|
return false;
|
|
198
346
|
}
|
|
199
347
|
const before = oldestSequenceRef.current;
|
|
@@ -221,41 +369,367 @@ export function useSessionEvents(
|
|
|
221
369
|
setHasOlder(false);
|
|
222
370
|
return false;
|
|
223
371
|
}
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
372
|
+
const current = eventWindowRef.current;
|
|
373
|
+
assertPrependOrder(current.events, window.events);
|
|
374
|
+
// Freeze the live iterator before replacing its in-memory window. Rows
|
|
375
|
+
// pending in the aborted iterator were never cursor-committed and will
|
|
376
|
+
// be replayed from the retained high-water mark below.
|
|
377
|
+
streamAbortRef.current?.abort();
|
|
378
|
+
observeSessionStatus(window.events, sessionStatusRef, setSessionStatusProjection);
|
|
379
|
+
const next = boundBrowserSessionEventWindow([...window.events, ...current.events], {
|
|
380
|
+
direction: "oldest",
|
|
230
381
|
});
|
|
231
|
-
|
|
382
|
+
const retained = {
|
|
383
|
+
...next,
|
|
384
|
+
truncated: current.truncated || next.truncated,
|
|
385
|
+
};
|
|
386
|
+
const retainedOldest = retained.events[0]?.sequence ?? null;
|
|
387
|
+
if (retainedOldest === null || retainedOldest >= before) {
|
|
388
|
+
throw new Error("@opengeni/react: loadOlder made no durable sequence progress");
|
|
389
|
+
}
|
|
390
|
+
eventWindowRef.current = retained;
|
|
391
|
+
setEventWindow(retained);
|
|
392
|
+
oldestSequenceRef.current = retainedOldest;
|
|
393
|
+
streamResumeSequenceRef.current = maxResumeSequence(retained.events);
|
|
394
|
+
// Oldest-directed eviction can discard newer in-memory rows. That fact
|
|
395
|
+
// keeps windowTruncated true, but it does not imply older durable rows
|
|
396
|
+
// exist; only the backward DB page can answer hasOlder truthfully.
|
|
397
|
+
const olderStillAvailable = window.hasOlder;
|
|
398
|
+
hasOlderRef.current = olderStillAvailable;
|
|
399
|
+
setHasOlder(olderStillAvailable);
|
|
400
|
+
setStreamEpoch((epoch) => epoch + 1);
|
|
401
|
+
return olderStillAvailable;
|
|
232
402
|
} finally {
|
|
233
403
|
if (generationRef.current === generation) {
|
|
234
404
|
loadingOlderRef.current = false;
|
|
235
405
|
setLoadingOlder(false);
|
|
236
406
|
}
|
|
237
407
|
}
|
|
238
|
-
}, [client, workspaceId, sessionId
|
|
408
|
+
}, [client, workspaceId, sessionId]);
|
|
239
409
|
|
|
240
410
|
const identityMatches = stateStreamKey === streamKey;
|
|
241
|
-
const visibleEvents = identityMatches ? events : EMPTY_EVENTS;
|
|
411
|
+
const visibleEvents = identityMatches ? eventWindow.events : EMPTY_EVENTS;
|
|
242
412
|
const timeline = useMemo(() => buildTimeline(visibleEvents), [visibleEvents]);
|
|
243
|
-
const sessionStatus = useMemo(() => sessionStatusFromEvents(visibleEvents), [visibleEvents]);
|
|
244
413
|
|
|
245
414
|
return {
|
|
246
415
|
events: visibleEvents,
|
|
247
416
|
timeline,
|
|
248
|
-
sessionStatus,
|
|
417
|
+
sessionStatus: identityMatches ? sessionStatusProjection : null,
|
|
249
418
|
connectionState: identityMatches ? connectionState : "idle",
|
|
250
419
|
lastSequence: identityMatches ? lastSequenceRef.current : after,
|
|
420
|
+
windowBytes: identityMatches ? eventWindow.bytes : 2,
|
|
421
|
+
windowTruncated: identityMatches ? eventWindow.truncated : false,
|
|
251
422
|
initialLoading: fullReplay ? false : identityMatches ? initialLoading : true,
|
|
252
|
-
hasOlder:
|
|
253
|
-
loadingOlder:
|
|
423
|
+
hasOlder: !identityMatches ? false : hasOlder,
|
|
424
|
+
loadingOlder: !identityMatches ? false : loadingOlder,
|
|
254
425
|
loadOlder,
|
|
255
426
|
error: identityMatches ? error : null,
|
|
256
427
|
};
|
|
257
428
|
}
|
|
258
429
|
|
|
430
|
+
/**
|
|
431
|
+
* Keep one direction-aware count+byte-bounded browser window. Live/default
|
|
432
|
+
* accumulation retains the newest suffix; backward paging retains the oldest
|
|
433
|
+
* prefix so newly fetched history cannot be immediately evicted. This is
|
|
434
|
+
* deliberately separate from durable history and transport paging: when a
|
|
435
|
+
* backward page evicts the live tail, the hook reconnects from the retained
|
|
436
|
+
* high-water mark while preserving the highest-ever-observed sequence
|
|
437
|
+
* separately. The source event remains durable in PostgreSQL throughout.
|
|
438
|
+
*/
|
|
439
|
+
export function boundBrowserSessionEventWindow(
|
|
440
|
+
events: readonly SessionEvent[],
|
|
441
|
+
options: {
|
|
442
|
+
maxBytes?: number;
|
|
443
|
+
maxCount?: number;
|
|
444
|
+
direction?: "newest" | "oldest";
|
|
445
|
+
} = {},
|
|
446
|
+
): BrowserSessionEventWindow {
|
|
447
|
+
const maxBytes = Math.max(1024, options.maxBytes ?? SESSION_EVENT_BROWSER_MAX_BYTES);
|
|
448
|
+
const maxCount = Math.max(1, Math.floor(options.maxCount ?? SESSION_EVENT_BROWSER_MAX_COUNT));
|
|
449
|
+
const safe = events.map(boundBrowserLegacyEvent);
|
|
450
|
+
const selected: SessionEvent[] = [];
|
|
451
|
+
let bytes = 2; // []
|
|
452
|
+
const direction = options.direction ?? "newest";
|
|
453
|
+
const start = direction === "newest" ? safe.length - 1 : 0;
|
|
454
|
+
const end = direction === "newest" ? Math.max(-1, safe.length - maxCount - 1) : safe.length;
|
|
455
|
+
const step = direction === "newest" ? -1 : 1;
|
|
456
|
+
for (let index = start; index !== end && selected.length < maxCount; index += step) {
|
|
457
|
+
const event = safe[index]!;
|
|
458
|
+
const eventBytes = browserJsonBytes(event);
|
|
459
|
+
const separator = selected.length === 0 ? 0 : 1;
|
|
460
|
+
if (bytes + separator + eventBytes > maxBytes) break;
|
|
461
|
+
selected.push(event);
|
|
462
|
+
bytes += separator + eventBytes;
|
|
463
|
+
}
|
|
464
|
+
if (direction === "newest") selected.reverse();
|
|
465
|
+
return {
|
|
466
|
+
events: selected,
|
|
467
|
+
bytes,
|
|
468
|
+
truncated: selected.length < safe.length,
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
function boundBrowserLegacyEvent(event: SessionEvent): SessionEvent {
|
|
473
|
+
// The server-side canonical projection lives in @opengeni/contracts. The
|
|
474
|
+
// publishable React package may depend only on the zero-runtime-dependency
|
|
475
|
+
// SDK, so this is intentionally a last-resort client guard rather than a
|
|
476
|
+
// second durable representation. Reconstructing the SDK wire shape prevents
|
|
477
|
+
// legacy/malformed extra properties from bypassing the browser byte cap.
|
|
478
|
+
const serialized = browserSerialize(event);
|
|
479
|
+
const originalBytes = serialized.serializable
|
|
480
|
+
? encoder.encode(serialized.value).byteLength
|
|
481
|
+
: null;
|
|
482
|
+
const typeIsSafe =
|
|
483
|
+
browserUtf8Bytes(event.type) <= BROWSER_EVENT_TYPE_MAX_BYTES &&
|
|
484
|
+
!event.type.includes("\n") &&
|
|
485
|
+
!event.type.includes("\r");
|
|
486
|
+
const clientEventId = boundBrowserOptionalText(
|
|
487
|
+
event.clientEventId,
|
|
488
|
+
BROWSER_EVENT_CLIENT_ID_MAX_BYTES,
|
|
489
|
+
);
|
|
490
|
+
const duplicateReason = boundBrowserOptionalText(
|
|
491
|
+
event.duplicateReason,
|
|
492
|
+
BROWSER_EVENT_DUPLICATE_REASON_MAX_BYTES,
|
|
493
|
+
);
|
|
494
|
+
|
|
495
|
+
if (
|
|
496
|
+
serialized.serializable &&
|
|
497
|
+
originalBytes !== null &&
|
|
498
|
+
originalBytes <= SESSION_EVENT_BROWSER_SINGLE_EVENT_MAX_BYTES &&
|
|
499
|
+
typeIsSafe &&
|
|
500
|
+
clientEventId === event.clientEventId &&
|
|
501
|
+
duplicateReason === event.duplicateReason
|
|
502
|
+
) {
|
|
503
|
+
return event;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const envelopeProjection = [
|
|
507
|
+
!typeIsSafe
|
|
508
|
+
? browserEnvelopeFieldProjection("type", event.type, "session.event.envelope_omitted")
|
|
509
|
+
: null,
|
|
510
|
+
clientEventId !== event.clientEventId
|
|
511
|
+
? browserEnvelopeFieldProjection("clientEventId", event.clientEventId, clientEventId)
|
|
512
|
+
: null,
|
|
513
|
+
duplicateReason !== event.duplicateReason
|
|
514
|
+
? browserEnvelopeFieldProjection("duplicateReason", event.duplicateReason, duplicateReason)
|
|
515
|
+
: null,
|
|
516
|
+
].filter((field) => field !== null);
|
|
517
|
+
const payloadSerialization = browserSerialize(event.payload);
|
|
518
|
+
const payloadBytes = payloadSerialization.serializable
|
|
519
|
+
? encoder.encode(payloadSerialization.value).byteLength
|
|
520
|
+
: null;
|
|
521
|
+
const preview = truncateBrowserUtf8Middle(
|
|
522
|
+
payloadSerialization.value,
|
|
523
|
+
BROWSER_EVENT_PAYLOAD_PREVIEW_MAX_BYTES,
|
|
524
|
+
);
|
|
525
|
+
const truncation = {
|
|
526
|
+
truncated: true as const,
|
|
527
|
+
surface: "browser_legacy_guard" as const,
|
|
528
|
+
reason: serialized.serializable ? "event_envelope_bytes_exceeded" : "event_not_serializable",
|
|
529
|
+
originalBytes,
|
|
530
|
+
deliveredBytes: 0,
|
|
531
|
+
omittedBytes: originalBytes,
|
|
532
|
+
estimatedOriginalTokens: originalBytes === null ? null : Math.ceil(originalBytes / 4),
|
|
533
|
+
estimatedDeliveredTokens: 0,
|
|
534
|
+
fullEvidence: { available: false as const, reason: "not_retained" as const },
|
|
535
|
+
details: [
|
|
536
|
+
{
|
|
537
|
+
path: "$.payload",
|
|
538
|
+
kind: "payload_preview",
|
|
539
|
+
originalBytes: payloadBytes,
|
|
540
|
+
deliveredBytes: browserUtf8Bytes(preview),
|
|
541
|
+
},
|
|
542
|
+
],
|
|
543
|
+
};
|
|
544
|
+
const payload: Record<string, unknown> = {
|
|
545
|
+
...browserPayloadIdentity(event.payload),
|
|
546
|
+
preview,
|
|
547
|
+
...(envelopeProjection.length > 0
|
|
548
|
+
? {
|
|
549
|
+
originalType: boundBrowserText(event.type, BROWSER_EVENT_TYPE_MAX_BYTES),
|
|
550
|
+
envelopeProjection: {
|
|
551
|
+
truncated: true,
|
|
552
|
+
surface: "browser_legacy_guard",
|
|
553
|
+
fields: envelopeProjection,
|
|
554
|
+
},
|
|
555
|
+
}
|
|
556
|
+
: {}),
|
|
557
|
+
truncation,
|
|
558
|
+
};
|
|
559
|
+
const bounded: SessionEvent = {
|
|
560
|
+
id: boundBrowserText(event.id, BROWSER_EVENT_ID_MAX_BYTES),
|
|
561
|
+
workspaceId: boundBrowserText(event.workspaceId, BROWSER_EVENT_ID_MAX_BYTES),
|
|
562
|
+
sessionId: boundBrowserText(event.sessionId, BROWSER_EVENT_ID_MAX_BYTES),
|
|
563
|
+
sequence: event.sequence,
|
|
564
|
+
type: typeIsSafe ? event.type : "session.event.envelope_omitted",
|
|
565
|
+
payload,
|
|
566
|
+
occurredAt: boundBrowserText(event.occurredAt, BROWSER_EVENT_ID_MAX_BYTES),
|
|
567
|
+
clientEventId,
|
|
568
|
+
turnId: boundBrowserOptionalText(event.turnId, BROWSER_EVENT_ID_MAX_BYTES),
|
|
569
|
+
turnGeneration: event.turnGeneration,
|
|
570
|
+
turnAttemptId: boundBrowserOptionalText(event.turnAttemptId, BROWSER_EVENT_ID_MAX_BYTES),
|
|
571
|
+
turnAssociation: event.turnAssociation,
|
|
572
|
+
duplicateOfEventId: boundBrowserOptionalText(
|
|
573
|
+
event.duplicateOfEventId,
|
|
574
|
+
BROWSER_EVENT_ID_MAX_BYTES,
|
|
575
|
+
),
|
|
576
|
+
duplicateReason,
|
|
577
|
+
};
|
|
578
|
+
|
|
579
|
+
settleBrowserEventTruncation(bounded, truncation);
|
|
580
|
+
if (browserJsonBytes(bounded) > SESSION_EVENT_BROWSER_SINGLE_EVENT_MAX_BYTES) {
|
|
581
|
+
payload.preview = truncateBrowserUtf8Middle(String(payload.preview), 4 * 1024);
|
|
582
|
+
truncation.details = truncation.details.slice(0, 1);
|
|
583
|
+
settleBrowserEventTruncation(bounded, truncation);
|
|
584
|
+
}
|
|
585
|
+
if (browserJsonBytes(bounded) > SESSION_EVENT_BROWSER_SINGLE_EVENT_MAX_BYTES) {
|
|
586
|
+
bounded.clientEventId = null;
|
|
587
|
+
bounded.duplicateReason = null;
|
|
588
|
+
payload.preview = "[legacy event omitted at the browser byte boundary]";
|
|
589
|
+
settleBrowserEventTruncation(bounded, truncation);
|
|
590
|
+
}
|
|
591
|
+
return bounded;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function browserJsonBytes(value: unknown): number {
|
|
595
|
+
return encoder.encode(browserSerialize(value).value).byteLength;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function browserSerialize(value: unknown): { value: string; serializable: boolean } {
|
|
599
|
+
try {
|
|
600
|
+
const serialized = JSON.stringify(value);
|
|
601
|
+
return { value: serialized === undefined ? "null" : serialized, serializable: true };
|
|
602
|
+
} catch {
|
|
603
|
+
return {
|
|
604
|
+
value: '"[unserializable event payload omitted at browser boundary]"',
|
|
605
|
+
serializable: false,
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
function browserPayloadIdentity(payload: unknown): Record<string, unknown> {
|
|
611
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return {};
|
|
612
|
+
const record = payload as Record<string, unknown>;
|
|
613
|
+
const identity: Record<string, unknown> = {};
|
|
614
|
+
for (const field of BROWSER_EVENT_PAYLOAD_IDENTITY_FIELDS) {
|
|
615
|
+
const value = record[field];
|
|
616
|
+
if (typeof value === "string") {
|
|
617
|
+
identity[field] = boundBrowserText(value, BROWSER_EVENT_ID_MAX_BYTES);
|
|
618
|
+
} else if (typeof value === "number" || typeof value === "boolean" || value === null) {
|
|
619
|
+
identity[field] = value;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
return identity;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
function browserEnvelopeFieldProjection(
|
|
626
|
+
field: string,
|
|
627
|
+
original: string | null | undefined,
|
|
628
|
+
delivered: string | null | undefined,
|
|
629
|
+
): { field: string; originalBytes: number; deliveredBytes: number } {
|
|
630
|
+
return {
|
|
631
|
+
field,
|
|
632
|
+
originalBytes: typeof original === "string" ? browserUtf8Bytes(original) : 0,
|
|
633
|
+
deliveredBytes: typeof delivered === "string" ? browserUtf8Bytes(delivered) : 0,
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function boundBrowserOptionalText<T extends string | null | undefined>(
|
|
638
|
+
value: T,
|
|
639
|
+
maxBytes: number,
|
|
640
|
+
): T {
|
|
641
|
+
return (typeof value === "string" ? boundBrowserText(value, maxBytes) : value) as T;
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function boundBrowserText(value: string, maxBytes: number): string {
|
|
645
|
+
const bytes = encoder.encode(value);
|
|
646
|
+
if (bytes.byteLength <= maxBytes) return value;
|
|
647
|
+
const marker = "…[truncated]";
|
|
648
|
+
const prefixBudget = Math.max(0, maxBytes - browserUtf8Bytes(marker));
|
|
649
|
+
let prefixEnd = Math.min(prefixBudget, bytes.byteLength);
|
|
650
|
+
while (
|
|
651
|
+
prefixEnd > 0 &&
|
|
652
|
+
prefixEnd < bytes.byteLength &&
|
|
653
|
+
isBrowserUtf8Continuation(bytes[prefixEnd]!)
|
|
654
|
+
) {
|
|
655
|
+
prefixEnd -= 1;
|
|
656
|
+
}
|
|
657
|
+
return `${decoder.decode(bytes.subarray(0, prefixEnd))}${marker}`;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
function truncateBrowserUtf8Middle(value: string, maxBytes: number): string {
|
|
661
|
+
const bytes = encoder.encode(value);
|
|
662
|
+
if (bytes.byteLength <= maxBytes) return value;
|
|
663
|
+
const marker = `…[${bytes.byteLength - maxBytes} bytes omitted]…`;
|
|
664
|
+
const contentBudget = Math.max(0, maxBytes - browserUtf8Bytes(marker));
|
|
665
|
+
const leftBudget = Math.floor(contentBudget / 2);
|
|
666
|
+
const rightBudget = contentBudget - leftBudget;
|
|
667
|
+
let leftEnd = Math.min(leftBudget, bytes.byteLength);
|
|
668
|
+
while (leftEnd > 0 && leftEnd < bytes.byteLength && isBrowserUtf8Continuation(bytes[leftEnd]!)) {
|
|
669
|
+
leftEnd -= 1;
|
|
670
|
+
}
|
|
671
|
+
let rightStart = Math.max(0, bytes.byteLength - rightBudget);
|
|
672
|
+
while (rightStart < bytes.byteLength && isBrowserUtf8Continuation(bytes[rightStart]!)) {
|
|
673
|
+
rightStart += 1;
|
|
674
|
+
}
|
|
675
|
+
return `${decoder.decode(bytes.subarray(0, leftEnd))}${marker}${decoder.decode(bytes.subarray(rightStart))}`;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function settleBrowserEventTruncation(
|
|
679
|
+
event: SessionEvent,
|
|
680
|
+
truncation: {
|
|
681
|
+
originalBytes: number | null;
|
|
682
|
+
deliveredBytes: number;
|
|
683
|
+
omittedBytes: number | null;
|
|
684
|
+
estimatedDeliveredTokens: number;
|
|
685
|
+
},
|
|
686
|
+
): void {
|
|
687
|
+
for (let attempt = 0; attempt < 16; attempt += 1) {
|
|
688
|
+
const deliveredBytes = browserJsonBytes(event);
|
|
689
|
+
const omittedBytes =
|
|
690
|
+
truncation.originalBytes === null
|
|
691
|
+
? null
|
|
692
|
+
: Math.max(0, truncation.originalBytes - deliveredBytes);
|
|
693
|
+
const estimatedDeliveredTokens = Math.ceil(deliveredBytes / 4);
|
|
694
|
+
if (
|
|
695
|
+
truncation.deliveredBytes === deliveredBytes &&
|
|
696
|
+
truncation.omittedBytes === omittedBytes &&
|
|
697
|
+
truncation.estimatedDeliveredTokens === estimatedDeliveredTokens
|
|
698
|
+
) {
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
truncation.deliveredBytes = deliveredBytes;
|
|
702
|
+
truncation.omittedBytes = omittedBytes;
|
|
703
|
+
truncation.estimatedDeliveredTokens = estimatedDeliveredTokens;
|
|
704
|
+
}
|
|
705
|
+
throw new RangeError("Browser event byte accounting did not converge");
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function browserUtf8Bytes(value: string): number {
|
|
709
|
+
return encoder.encode(value).byteLength;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function isBrowserUtf8Continuation(value: number): boolean {
|
|
713
|
+
return (value & 0xc0) === 0x80;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
function observeSessionStatus(
|
|
717
|
+
events: readonly SessionEvent[],
|
|
718
|
+
ref: { current: { sequence: number; status: SessionStatus | null } },
|
|
719
|
+
setStatus: (status: SessionStatus | null) => void,
|
|
720
|
+
): void {
|
|
721
|
+
let latest: { sequence: number; status: SessionStatus } | null = null;
|
|
722
|
+
for (const event of events) {
|
|
723
|
+
const status = sessionStatusFromEvents([event]);
|
|
724
|
+
if (status && (!latest || event.sequence >= latest.sequence)) {
|
|
725
|
+
latest = { sequence: event.sequence, status };
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
if (!latest || latest.sequence < ref.current.sequence) return;
|
|
729
|
+
ref.current = latest;
|
|
730
|
+
setStatus(latest.status);
|
|
731
|
+
}
|
|
732
|
+
|
|
259
733
|
type LoadedEventWindow = {
|
|
260
734
|
events: SessionEvent[];
|
|
261
735
|
oldestSequence: number | null;
|
|
@@ -264,7 +738,7 @@ type LoadedEventWindow = {
|
|
|
264
738
|
};
|
|
265
739
|
|
|
266
740
|
async function loadEventWindow(
|
|
267
|
-
client:
|
|
741
|
+
client: EmbeddedSessionClientLike,
|
|
268
742
|
workspaceId: string,
|
|
269
743
|
sessionId: string,
|
|
270
744
|
options: {
|
|
@@ -364,7 +838,7 @@ function findBoundaryIndex(events: SessionEvent[]): number {
|
|
|
364
838
|
type PreviousPage = SessionEvent[] & { requested: number };
|
|
365
839
|
|
|
366
840
|
async function loadPreviousPage(
|
|
367
|
-
client:
|
|
841
|
+
client: EmbeddedSessionClientLike,
|
|
368
842
|
workspaceId: string,
|
|
369
843
|
sessionId: string,
|
|
370
844
|
before: number,
|