@opengeni/react 3.5.1-canary.0 → 3.7.0-canary.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/artifacts.d.ts +1 -1
- package/dist/artifacts.js +248 -6
- package/dist/artifacts.js.map +1 -1
- package/dist/{browser-viewer-E5DDEFQ4.js → browser-viewer-NTD6UIPR.js} +3 -3
- package/dist/{chunk-W2RGYBCV.js → chunk-24M4YBCL.js} +4 -4
- package/dist/{chunk-WY3MELVJ.js → chunk-5IXTJTK3.js} +1438 -397
- package/dist/chunk-5IXTJTK3.js.map +1 -0
- package/dist/chunk-5PQJOCX5.js +902 -0
- package/dist/chunk-5PQJOCX5.js.map +1 -0
- package/dist/{chunk-UNCXXIQR.js → chunk-INGB2N5R.js} +4310 -3982
- package/dist/chunk-INGB2N5R.js.map +1 -0
- package/dist/{chunk-TJFMMDQU.js → chunk-MOWYEBCQ.js} +1839 -17
- package/dist/chunk-MOWYEBCQ.js.map +1 -0
- package/dist/chunk-PAJU5KKL.js +331 -0
- package/dist/chunk-PAJU5KKL.js.map +1 -0
- package/dist/{chunk-VTOREEXF.js → chunk-PLYRL5ER.js} +24 -2
- package/dist/{chunk-VTOREEXF.js.map → chunk-PLYRL5ER.js.map} +1 -1
- package/dist/{chunk-YVCHAQZY.js → chunk-ZFOFY5ST.js} +27 -349
- package/dist/chunk-ZFOFY5ST.js.map +1 -0
- package/dist/client.d.ts +1 -1
- package/dist/components/artifacts/index.d.ts +1 -1
- package/dist/components/artifacts/published-html-artifact-frame.d.ts +46 -3
- package/dist/components/composer.d.ts +2 -0
- package/dist/components/session-chrome.d.ts +38 -5
- package/dist/components/session-commands-panel.d.ts +6 -0
- package/dist/components/session-conversation.d.ts +14 -0
- package/dist/components/timeline-anchor.d.ts +21 -0
- package/dist/composer.js +2 -3
- package/dist/conversation-timeline.d.ts +5 -0
- package/dist/{fleet-decision-row-GGCAT4E4.js → fleet-decision-row-YZ3ZKWMM.js} +2 -1
- package/dist/fleet-decision-row-YZ3ZKWMM.js.map +1 -0
- package/dist/hooks/use-file-attachments.d.ts +6 -0
- package/dist/hooks/use-sandbox-files.d.ts +4 -1
- package/dist/hooks/use-workspace-edit.d.ts +4 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +823 -965
- package/dist/index.js.map +1 -1
- package/dist/interaction.js +2 -2
- package/dist/lib/format.d.ts +5 -0
- package/dist/machines.js +3 -2
- package/dist/session-ui.d.ts +3 -0
- package/dist/session-ui.js +14 -4
- package/dist/session.js +19 -19
- package/dist/timeline/registry.d.ts +2 -0
- package/dist/timeline/types.d.ts +4 -2
- package/package.json +2 -2
- package/src/artifacts.ts +3 -0
- package/src/client.ts +2 -1
- package/src/components/artifacts/index.ts +3 -0
- package/src/components/artifacts/published-html-artifact-frame.tsx +307 -4
- package/src/components/composer.tsx +89 -25
- package/src/components/machine-input-display.ts +6 -0
- package/src/components/message-timeline.tsx +351 -204
- package/src/components/sandbox-workspace.tsx +11 -1
- package/src/components/session-chrome.tsx +501 -128
- package/src/components/session-commands-panel.tsx +113 -0
- package/src/components/session-conversation.tsx +141 -0
- package/src/components/timeline-anchor.tsx +91 -0
- package/src/conversation-timeline.ts +83 -0
- package/src/hooks/use-file-attachments.ts +34 -0
- package/src/hooks/use-sandbox-files.ts +68 -20
- package/src/hooks/use-session-background-commands.ts +21 -10
- package/src/hooks/use-session-events.ts +56 -39
- package/src/hooks/use-session.ts +23 -2
- package/src/hooks/use-workspace-edit.ts +23 -5
- package/src/index.ts +4 -0
- package/src/lib/format.ts +32 -0
- package/src/session-ui.ts +3 -0
- package/src/timeline/fleet-decision-projection.ts +2 -1
- package/src/timeline/fleet-decision-row.tsx +1 -0
- package/src/timeline/projection.ts +157 -19
- package/src/timeline/registry.ts +15 -3
- package/src/timeline/tool-renderers.tsx +117 -1
- package/src/timeline/types.ts +12 -1
- package/styles/compiled.css +495 -411
- package/styles/index.css +23 -0
- package/dist/chunk-7EAFGICR.js +0 -302
- package/dist/chunk-7EAFGICR.js.map +0 -1
- package/dist/chunk-IB27C53Y.js +0 -2582
- package/dist/chunk-IB27C53Y.js.map +0 -1
- package/dist/chunk-TJFMMDQU.js.map +0 -1
- package/dist/chunk-UNCXXIQR.js.map +0 -1
- package/dist/chunk-WY3MELVJ.js.map +0 -1
- package/dist/chunk-YVCHAQZY.js.map +0 -1
- package/dist/fleet-decision-row-GGCAT4E4.js.map +0 -1
- /package/dist/{browser-viewer-E5DDEFQ4.js.map → browser-viewer-NTD6UIPR.js.map} +0 -0
- /package/dist/{chunk-W2RGYBCV.js.map → chunk-24M4YBCL.js.map} +0 -0
package/dist/chunk-IB27C53Y.js
DELETED
|
@@ -1,2582 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
useDebouncedCallback,
|
|
3
|
-
useMutationRunner,
|
|
4
|
-
usePageLiveActivity,
|
|
5
|
-
usePolledValue,
|
|
6
|
-
useSessionEventTrigger
|
|
7
|
-
} from "./chunk-KZ73RL76.js";
|
|
8
|
-
import {
|
|
9
|
-
useEmbeddedFileAttachments,
|
|
10
|
-
useEmbeddedGoal,
|
|
11
|
-
useEmbeddedHumanInputSession,
|
|
12
|
-
useEmbeddedSession,
|
|
13
|
-
useEmbeddedSessionLineage,
|
|
14
|
-
useEmbeddedSessionMcpApprovalPolicy,
|
|
15
|
-
useEmbeddedSessionRead
|
|
16
|
-
} from "./chunk-NEXLLTP4.js";
|
|
17
|
-
import {
|
|
18
|
-
buildTimeline,
|
|
19
|
-
createOlderHistoryLoadReceipt,
|
|
20
|
-
groupTimeline,
|
|
21
|
-
isActionableHumanInputRequest,
|
|
22
|
-
sessionStatusFromEvents
|
|
23
|
-
} from "./chunk-TJFMMDQU.js";
|
|
24
|
-
|
|
25
|
-
// src/embedded-session-client.ts
|
|
26
|
-
var requiredMethods = [
|
|
27
|
-
"getSession",
|
|
28
|
-
"listEvents",
|
|
29
|
-
"streamEvents",
|
|
30
|
-
"getComposerDraft",
|
|
31
|
-
"saveComposerDraft",
|
|
32
|
-
"submitComposerDraft",
|
|
33
|
-
"sendMessage",
|
|
34
|
-
"steerMessage",
|
|
35
|
-
"getQueue",
|
|
36
|
-
"moveQueueItem",
|
|
37
|
-
"editQueueItem",
|
|
38
|
-
"steerQueueItem",
|
|
39
|
-
"deleteQueueItem",
|
|
40
|
-
"pauseSession",
|
|
41
|
-
"resumeSession",
|
|
42
|
-
"sendApprovalDecision"
|
|
43
|
-
];
|
|
44
|
-
function createEmbeddedSessionClient(base, options = {}) {
|
|
45
|
-
const overrides = options.overrides ?? {};
|
|
46
|
-
const client = {};
|
|
47
|
-
for (const key of requiredMethods) {
|
|
48
|
-
const override = overrides[key];
|
|
49
|
-
const method = override ?? base[key];
|
|
50
|
-
if (typeof method !== "function") {
|
|
51
|
-
throw new TypeError(`Embedded session client method is required: ${key}`);
|
|
52
|
-
}
|
|
53
|
-
client[key] = method.bind(override === void 0 ? base : overrides);
|
|
54
|
-
}
|
|
55
|
-
const inferenceOverride = overrides.setWorkspaceInferenceState;
|
|
56
|
-
const inferenceBase = base.setWorkspaceInferenceState;
|
|
57
|
-
const inferenceMethod = inferenceOverride ?? inferenceBase;
|
|
58
|
-
if (typeof inferenceMethod === "function") {
|
|
59
|
-
client.setWorkspaceInferenceState = inferenceMethod.bind(
|
|
60
|
-
inferenceOverride === void 0 ? base : overrides
|
|
61
|
-
);
|
|
62
|
-
}
|
|
63
|
-
const mapDraft = options.mapComposerDraft;
|
|
64
|
-
if (!mapDraft) return client;
|
|
65
|
-
const getComposerDraft = client.getComposerDraft;
|
|
66
|
-
client.getComposerDraft = async (workspaceId, sessionId) => mapDraft(await getComposerDraft(workspaceId, sessionId), {
|
|
67
|
-
operation: "read",
|
|
68
|
-
workspaceId,
|
|
69
|
-
sessionId
|
|
70
|
-
});
|
|
71
|
-
const saveComposerDraft = client.saveComposerDraft;
|
|
72
|
-
client.saveComposerDraft = async (workspaceId, sessionId, request) => mapDraft(await saveComposerDraft(workspaceId, sessionId, request), {
|
|
73
|
-
operation: "save",
|
|
74
|
-
workspaceId,
|
|
75
|
-
sessionId
|
|
76
|
-
});
|
|
77
|
-
const submitComposerDraft = client.submitComposerDraft;
|
|
78
|
-
client.submitComposerDraft = async (workspaceId, sessionId, request) => {
|
|
79
|
-
const response = await submitComposerDraft(workspaceId, sessionId, request);
|
|
80
|
-
return {
|
|
81
|
-
...response,
|
|
82
|
-
draft: mapDraft(response.draft, {
|
|
83
|
-
operation: "submit",
|
|
84
|
-
workspaceId,
|
|
85
|
-
sessionId
|
|
86
|
-
})
|
|
87
|
-
};
|
|
88
|
-
};
|
|
89
|
-
return client;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// src/hooks/use-session.ts
|
|
93
|
-
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
94
|
-
function isTitleEvent(event) {
|
|
95
|
-
return event.type === "session.title_set";
|
|
96
|
-
}
|
|
97
|
-
function useSession(sessionId, options = {}) {
|
|
98
|
-
const { client, workspaceId, workspaceControlEvent, registerSessionReconciler } = useEmbeddedSessionRead(options);
|
|
99
|
-
const enabled = (options.enabled ?? true) && Boolean(sessionId);
|
|
100
|
-
const [override, setOverride] = useState(null);
|
|
101
|
-
const nextReadRevision = useRef(0);
|
|
102
|
-
const nextReadGeneration = useRef(0);
|
|
103
|
-
const beginRead = options.beginRead;
|
|
104
|
-
const { run, mutating, mutationError, clearMutationError } = useMutationRunner();
|
|
105
|
-
const load = useCallback(
|
|
106
|
-
async (signal) => {
|
|
107
|
-
if (!sessionId) {
|
|
108
|
-
return null;
|
|
109
|
-
}
|
|
110
|
-
let readGeneration2 = 0;
|
|
111
|
-
const fetched = await client.getSession(workspaceId, sessionId, {
|
|
112
|
-
fresh: true,
|
|
113
|
-
signal,
|
|
114
|
-
onRequestStart: () => {
|
|
115
|
-
readGeneration2 = beginRead?.() ?? ++nextReadGeneration.current;
|
|
116
|
-
}
|
|
117
|
-
});
|
|
118
|
-
setOverride(null);
|
|
119
|
-
return {
|
|
120
|
-
session: fetched,
|
|
121
|
-
revision: ++nextReadRevision.current,
|
|
122
|
-
readGeneration: readGeneration2
|
|
123
|
-
};
|
|
124
|
-
},
|
|
125
|
-
[beginRead, client, workspaceId, sessionId]
|
|
126
|
-
);
|
|
127
|
-
const { data, loading, error, refresh } = usePolledValue(load, {
|
|
128
|
-
pollIntervalMs: options.pollIntervalMs,
|
|
129
|
-
enabled
|
|
130
|
-
});
|
|
131
|
-
useEffect(() => {
|
|
132
|
-
if (enabled && workspaceControlEvent) void refresh();
|
|
133
|
-
}, [enabled, refresh, workspaceControlEvent]);
|
|
134
|
-
useEffect(() => {
|
|
135
|
-
if (!sessionId || !enabled) return;
|
|
136
|
-
return registerSessionReconciler(sessionId, "session", refresh);
|
|
137
|
-
}, [enabled, refresh, registerSessionReconciler, sessionId]);
|
|
138
|
-
const base = data?.session ?? null;
|
|
139
|
-
const readRevision = data?.revision ?? 0;
|
|
140
|
-
const readGeneration = data?.readGeneration ?? 0;
|
|
141
|
-
const session = useMemo(
|
|
142
|
-
() => base && override && override.id === base.id ? { ...base, title: override.title, titleSource: override.titleSource } : base,
|
|
143
|
-
[base, override]
|
|
144
|
-
);
|
|
145
|
-
const onTitleEvent = useCallback(
|
|
146
|
-
(event) => {
|
|
147
|
-
if (!base || event.sequence <= base.lastSequence) {
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
150
|
-
const payload = event.payload ?? {};
|
|
151
|
-
const title = payload.title;
|
|
152
|
-
if (typeof title !== "string") {
|
|
153
|
-
return;
|
|
154
|
-
}
|
|
155
|
-
const source = payload.source === "user" || payload.source === "agent" ? payload.source : null;
|
|
156
|
-
setOverride((current) => {
|
|
157
|
-
const next = current ?? base;
|
|
158
|
-
return { ...next, title, titleSource: source };
|
|
159
|
-
});
|
|
160
|
-
},
|
|
161
|
-
[base]
|
|
162
|
-
);
|
|
163
|
-
useSessionEventTrigger(client, workspaceId, sessionId, isTitleEvent, onTitleEvent, {
|
|
164
|
-
enabled,
|
|
165
|
-
...options.events !== void 0 ? { events: options.events } : {}
|
|
166
|
-
});
|
|
167
|
-
const updateTitle = useCallback(
|
|
168
|
-
async (title) => {
|
|
169
|
-
if (!sessionId) {
|
|
170
|
-
return null;
|
|
171
|
-
}
|
|
172
|
-
const result = await run(() => client.updateSession(workspaceId, sessionId, { title }));
|
|
173
|
-
if (result) {
|
|
174
|
-
setOverride(result);
|
|
175
|
-
}
|
|
176
|
-
return result;
|
|
177
|
-
},
|
|
178
|
-
[client, workspaceId, sessionId, run]
|
|
179
|
-
);
|
|
180
|
-
return {
|
|
181
|
-
session,
|
|
182
|
-
loading,
|
|
183
|
-
error,
|
|
184
|
-
readRevision,
|
|
185
|
-
readGeneration,
|
|
186
|
-
refresh,
|
|
187
|
-
updateTitle,
|
|
188
|
-
updating: mutating,
|
|
189
|
-
mutationError,
|
|
190
|
-
clearMutationError
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
// src/hooks/use-session-events.ts
|
|
195
|
-
import {
|
|
196
|
-
sessionEventStreamCoveredThrough
|
|
197
|
-
} from "@opengeni/sdk";
|
|
198
|
-
import { startTransition, useCallback as useCallback2, useEffect as useEffect2, useMemo as useMemo2, useRef as useRef2, useState as useState2 } from "react";
|
|
199
|
-
var INITIAL_TAIL_PAGE_SIZE = 1e3;
|
|
200
|
-
var OLDER_PAGE_SIZE = 5e3;
|
|
201
|
-
var NEWER_PAGE_SIZE = 5e3;
|
|
202
|
-
var OLDEST_PAGE_SIZE = 1e3;
|
|
203
|
-
var INITIAL_FETCH_CAP = 1;
|
|
204
|
-
var OLDER_GROUP_TARGET = 32;
|
|
205
|
-
var OLDER_FETCH_CAP = 2;
|
|
206
|
-
var NEWER_GROUP_TARGET = 32;
|
|
207
|
-
var NEWER_FETCH_CAP = 2;
|
|
208
|
-
var OLDEST_GROUP_TARGET = 32;
|
|
209
|
-
var OLDEST_FETCH_CAP = 2;
|
|
210
|
-
var BOUNDARY_PAGE_CAP = 4;
|
|
211
|
-
var FOREGROUND_DIRECT_REPLAY_MAX_SEQUENCES = 16;
|
|
212
|
-
var FOREGROUND_COMPACT_PROBE_MAX_SEQUENCES = 5e3;
|
|
213
|
-
var FOREGROUND_COMPACT_CATCHUP_MAX_EVENTS = 128;
|
|
214
|
-
var FOREGROUND_COMPACT_CATCHUP_MAX_GROUPS = 16;
|
|
215
|
-
var FOREGROUND_COMPACT_CATCHUP_MAX_BYTES = 512 * 1024;
|
|
216
|
-
var EMPTY_EVENTS = [];
|
|
217
|
-
var encoder = new TextEncoder();
|
|
218
|
-
var decoder = new TextDecoder();
|
|
219
|
-
var BROWSER_EVENT_TYPE_MAX_BYTES = 256;
|
|
220
|
-
var BROWSER_EVENT_ID_MAX_BYTES = 256;
|
|
221
|
-
var BROWSER_EVENT_CLIENT_ID_MAX_BYTES = 4 * 1024;
|
|
222
|
-
var BROWSER_EVENT_DUPLICATE_REASON_MAX_BYTES = 4 * 1024;
|
|
223
|
-
var BROWSER_EVENT_PAYLOAD_PREVIEW_MAX_BYTES = 48 * 1024;
|
|
224
|
-
var BROWSER_EVENT_PAYLOAD_IDENTITY_FIELDS = [
|
|
225
|
-
"id",
|
|
226
|
-
"callId",
|
|
227
|
-
"call_id",
|
|
228
|
-
"name",
|
|
229
|
-
"toolName",
|
|
230
|
-
"status",
|
|
231
|
-
"code",
|
|
232
|
-
"isError",
|
|
233
|
-
"stream",
|
|
234
|
-
"commandId",
|
|
235
|
-
"sequence",
|
|
236
|
-
"coalescedUntil",
|
|
237
|
-
"coalescedCount",
|
|
238
|
-
"firstSequence",
|
|
239
|
-
"lastSequence"
|
|
240
|
-
];
|
|
241
|
-
var SESSION_EVENT_BROWSER_MAX_BYTES = 8 * 1024 * 1024;
|
|
242
|
-
var SESSION_EVENT_BROWSER_MAX_COUNT = 1e4;
|
|
243
|
-
var SESSION_EVENT_BROWSER_SINGLE_EVENT_MAX_BYTES = 96 * 1024;
|
|
244
|
-
var SESSION_EVENT_BROWSER_PENDING_MAX_BYTES = 1024 * 1024;
|
|
245
|
-
var SESSION_EVENT_BROWSER_PENDING_MAX_COUNT = 256;
|
|
246
|
-
var EMPTY_EVENT_WINDOW = {
|
|
247
|
-
events: EMPTY_EVENTS,
|
|
248
|
-
bytes: 2,
|
|
249
|
-
truncated: false
|
|
250
|
-
};
|
|
251
|
-
function useSessionEvents(sessionId, options = {}) {
|
|
252
|
-
const { client, workspaceId, reconcileSession } = useEmbeddedSession(options);
|
|
253
|
-
const enabled = options.enabled ?? true;
|
|
254
|
-
const pageLive = usePageLiveActivity();
|
|
255
|
-
const streamEnabled = enabled && pageLive;
|
|
256
|
-
const after = options.after ?? 0;
|
|
257
|
-
const replay = options.replay ?? "windowed";
|
|
258
|
-
const fullReplay = replay === "full" || after !== 0;
|
|
259
|
-
const streamKey = `${workspaceId}\0${sessionId ?? ""}\0${after}\0${fullReplay ? "full" : "windowed"}`;
|
|
260
|
-
const [eventWindow, setEventWindow] = useState2(EMPTY_EVENT_WINDOW);
|
|
261
|
-
const [connectionState, setConnectionState] = useState2("idle");
|
|
262
|
-
const [error, setError] = useState2(null);
|
|
263
|
-
const [hasOlder, setHasOlder] = useState2(false);
|
|
264
|
-
const [hasNewer, setHasNewer] = useState2(false);
|
|
265
|
-
const [sessionStatusProjection, setSessionStatusProjection] = useState2(
|
|
266
|
-
null
|
|
267
|
-
);
|
|
268
|
-
const [initialLoading, setInitialLoading] = useState2(true);
|
|
269
|
-
const [loadingOlder, setLoadingOlder] = useState2(false);
|
|
270
|
-
const [loadingNewer, setLoadingNewer] = useState2(false);
|
|
271
|
-
const [loadingOldest, setLoadingOldest] = useState2(false);
|
|
272
|
-
const [loadingLatest, setLoadingLatest] = useState2(false);
|
|
273
|
-
const [streamEpoch, setStreamEpoch] = useState2(0);
|
|
274
|
-
const [viewMode, setViewMode] = useState2("live");
|
|
275
|
-
const lastSequenceRef = useRef2(after);
|
|
276
|
-
const streamResumeSequenceRef = useRef2(after);
|
|
277
|
-
const oldestSequenceRef = useRef2(null);
|
|
278
|
-
const newestSequenceRef = useRef2(null);
|
|
279
|
-
const hasOlderRef = useRef2(false);
|
|
280
|
-
const hasNewerRef = useRef2(false);
|
|
281
|
-
const loadingOlderRef = useRef2(false);
|
|
282
|
-
const loadingNewerRef = useRef2(false);
|
|
283
|
-
const loadingOldestRef = useRef2(false);
|
|
284
|
-
const loadingLatestRef = useRef2(false);
|
|
285
|
-
const viewModeRef = useRef2("live");
|
|
286
|
-
const initialWindowLoadedRef = useRef2(false);
|
|
287
|
-
const reconcileAfterPageResumeRef = useRef2(false);
|
|
288
|
-
const streamAbortRef = useRef2(null);
|
|
289
|
-
const streamKeyRef = useRef2(null);
|
|
290
|
-
const generationRef = useRef2(0);
|
|
291
|
-
const eventWindowRef = useRef2(EMPTY_EVENT_WINDOW);
|
|
292
|
-
const sessionStatusRef = useRef2({
|
|
293
|
-
sequence: after,
|
|
294
|
-
status: null
|
|
295
|
-
});
|
|
296
|
-
const [stateStreamKey, setStateStreamKey] = useState2(streamKey);
|
|
297
|
-
useEffect2(() => {
|
|
298
|
-
if (streamKeyRef.current !== streamKey) {
|
|
299
|
-
streamKeyRef.current = streamKey;
|
|
300
|
-
setStateStreamKey(streamKey);
|
|
301
|
-
eventWindowRef.current = EMPTY_EVENT_WINDOW;
|
|
302
|
-
setEventWindow(EMPTY_EVENT_WINDOW);
|
|
303
|
-
setError(null);
|
|
304
|
-
setHasOlder(false);
|
|
305
|
-
setHasNewer(false);
|
|
306
|
-
sessionStatusRef.current = { sequence: after, status: null };
|
|
307
|
-
setSessionStatusProjection(null);
|
|
308
|
-
setLoadingOlder(false);
|
|
309
|
-
setLoadingNewer(false);
|
|
310
|
-
setLoadingOldest(false);
|
|
311
|
-
setLoadingLatest(false);
|
|
312
|
-
setInitialLoading(true);
|
|
313
|
-
lastSequenceRef.current = after;
|
|
314
|
-
streamResumeSequenceRef.current = after;
|
|
315
|
-
oldestSequenceRef.current = null;
|
|
316
|
-
newestSequenceRef.current = null;
|
|
317
|
-
hasOlderRef.current = false;
|
|
318
|
-
hasNewerRef.current = false;
|
|
319
|
-
loadingOlderRef.current = false;
|
|
320
|
-
loadingNewerRef.current = false;
|
|
321
|
-
loadingOldestRef.current = false;
|
|
322
|
-
loadingLatestRef.current = false;
|
|
323
|
-
viewModeRef.current = "live";
|
|
324
|
-
setViewMode("live");
|
|
325
|
-
initialWindowLoadedRef.current = false;
|
|
326
|
-
reconcileAfterPageResumeRef.current = false;
|
|
327
|
-
}
|
|
328
|
-
const generation = generationRef.current + 1;
|
|
329
|
-
generationRef.current = generation;
|
|
330
|
-
if (loadingOlderRef.current) {
|
|
331
|
-
loadingOlderRef.current = false;
|
|
332
|
-
setLoadingOlder(false);
|
|
333
|
-
}
|
|
334
|
-
if (!sessionId || !streamEnabled) {
|
|
335
|
-
if (sessionId && enabled && !pageLive && !fullReplay && viewModeRef.current === "live" && initialWindowLoadedRef.current) {
|
|
336
|
-
reconcileAfterPageResumeRef.current = true;
|
|
337
|
-
}
|
|
338
|
-
setConnectionState("idle");
|
|
339
|
-
return;
|
|
340
|
-
}
|
|
341
|
-
if (viewMode === "history") {
|
|
342
|
-
reconcileAfterPageResumeRef.current = false;
|
|
343
|
-
setConnectionState("idle");
|
|
344
|
-
setInitialLoading(false);
|
|
345
|
-
return;
|
|
346
|
-
}
|
|
347
|
-
const reconcileForegroundResume = reconcileAfterPageResumeRef.current && !fullReplay;
|
|
348
|
-
reconcileAfterPageResumeRef.current = false;
|
|
349
|
-
const controller = new AbortController();
|
|
350
|
-
const isCurrent = () => generationRef.current === generation && !controller.signal.aborted;
|
|
351
|
-
streamAbortRef.current = controller;
|
|
352
|
-
let pending = [];
|
|
353
|
-
let pendingBytes = 2;
|
|
354
|
-
let flushTimer = null;
|
|
355
|
-
const flush = () => {
|
|
356
|
-
if (flushTimer !== null) {
|
|
357
|
-
clearTimeout(flushTimer);
|
|
358
|
-
flushTimer = null;
|
|
359
|
-
}
|
|
360
|
-
if (!isCurrent()) {
|
|
361
|
-
pending = [];
|
|
362
|
-
pendingBytes = 2;
|
|
363
|
-
return;
|
|
364
|
-
}
|
|
365
|
-
if (pending.length === 0) {
|
|
366
|
-
return;
|
|
367
|
-
}
|
|
368
|
-
const batch = pending;
|
|
369
|
-
pending = [];
|
|
370
|
-
pendingBytes = 2;
|
|
371
|
-
const lastInBatch = batch[batch.length - 1];
|
|
372
|
-
if (lastInBatch) {
|
|
373
|
-
const batchResumeSequence = Math.max(...batch.map(eventResumeSequence));
|
|
374
|
-
lastSequenceRef.current = Math.max(lastSequenceRef.current, batchResumeSequence);
|
|
375
|
-
streamResumeSequenceRef.current = Math.max(
|
|
376
|
-
streamResumeSequenceRef.current,
|
|
377
|
-
batchResumeSequence
|
|
378
|
-
);
|
|
379
|
-
}
|
|
380
|
-
const status = observeSessionStatus(batch, sessionStatusRef);
|
|
381
|
-
if (status !== void 0) {
|
|
382
|
-
setSessionStatusProjection(status);
|
|
383
|
-
}
|
|
384
|
-
const current = eventWindowRef.current;
|
|
385
|
-
const next = boundBrowserSessionEventWindow([...current.events, ...batch]);
|
|
386
|
-
const retained = {
|
|
387
|
-
...next,
|
|
388
|
-
truncated: current.truncated || next.truncated
|
|
389
|
-
};
|
|
390
|
-
eventWindowRef.current = retained;
|
|
391
|
-
setEventWindow(retained);
|
|
392
|
-
oldestSequenceRef.current = retained.events[0]?.sequence ?? null;
|
|
393
|
-
newestSequenceRef.current = maxResumeSequenceOrNull(retained.events);
|
|
394
|
-
if (retained.truncated) {
|
|
395
|
-
hasOlderRef.current = true;
|
|
396
|
-
setHasOlder(true);
|
|
397
|
-
}
|
|
398
|
-
};
|
|
399
|
-
const scheduleFlush = () => {
|
|
400
|
-
if (!isCurrent()) return;
|
|
401
|
-
flushTimer ??= setTimeout(flush, 16);
|
|
402
|
-
};
|
|
403
|
-
void (async () => {
|
|
404
|
-
try {
|
|
405
|
-
if (reconcileForegroundResume) {
|
|
406
|
-
setConnectionState("connecting");
|
|
407
|
-
const cursor = streamResumeSequenceRef.current;
|
|
408
|
-
let plan = { kind: "resume" };
|
|
409
|
-
try {
|
|
410
|
-
plan = await planForegroundCatchup(client, workspaceId, sessionId, cursor, {
|
|
411
|
-
signal: controller.signal
|
|
412
|
-
});
|
|
413
|
-
} catch {
|
|
414
|
-
if (controller.signal.aborted) return;
|
|
415
|
-
plan = { kind: "resume" };
|
|
416
|
-
}
|
|
417
|
-
if (!isCurrent()) return;
|
|
418
|
-
if (plan.kind === "append") {
|
|
419
|
-
const current = eventWindowRef.current;
|
|
420
|
-
assertAppendOrder(current.events, plan.events);
|
|
421
|
-
const status = observeSessionStatus(plan.events, sessionStatusRef);
|
|
422
|
-
const next = boundBrowserSessionEventWindow([...current.events, ...plan.events]);
|
|
423
|
-
const retained = {
|
|
424
|
-
...next,
|
|
425
|
-
truncated: current.truncated || next.truncated
|
|
426
|
-
};
|
|
427
|
-
eventWindowRef.current = retained;
|
|
428
|
-
oldestSequenceRef.current = retained.events[0]?.sequence ?? null;
|
|
429
|
-
newestSequenceRef.current = maxResumeSequenceOrNull(retained.events);
|
|
430
|
-
lastSequenceRef.current = Math.max(lastSequenceRef.current, plan.resumeSequence);
|
|
431
|
-
streamResumeSequenceRef.current = Math.max(
|
|
432
|
-
streamResumeSequenceRef.current,
|
|
433
|
-
plan.resumeSequence
|
|
434
|
-
);
|
|
435
|
-
if (status !== void 0) {
|
|
436
|
-
setSessionStatusProjection(status);
|
|
437
|
-
}
|
|
438
|
-
setEventWindow(retained);
|
|
439
|
-
if (retained.truncated) {
|
|
440
|
-
hasOlderRef.current = true;
|
|
441
|
-
setHasOlder(true);
|
|
442
|
-
}
|
|
443
|
-
} else if (plan.kind === "reload") {
|
|
444
|
-
eventWindowRef.current = EMPTY_EVENT_WINDOW;
|
|
445
|
-
oldestSequenceRef.current = null;
|
|
446
|
-
newestSequenceRef.current = null;
|
|
447
|
-
hasOlderRef.current = false;
|
|
448
|
-
hasNewerRef.current = false;
|
|
449
|
-
initialWindowLoadedRef.current = false;
|
|
450
|
-
streamResumeSequenceRef.current = after;
|
|
451
|
-
setEventWindow(EMPTY_EVENT_WINDOW);
|
|
452
|
-
setHasOlder(false);
|
|
453
|
-
setHasNewer(false);
|
|
454
|
-
setInitialLoading(true);
|
|
455
|
-
setError(null);
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
if (!fullReplay && !initialWindowLoadedRef.current) {
|
|
459
|
-
setConnectionState("connecting");
|
|
460
|
-
const window = await loadEventWindow(client, workspaceId, sessionId, {
|
|
461
|
-
before: Number.MAX_SAFE_INTEGER,
|
|
462
|
-
pageSize: INITIAL_TAIL_PAGE_SIZE,
|
|
463
|
-
targetGroups: Number.POSITIVE_INFINITY,
|
|
464
|
-
maxFetches: INITIAL_FETCH_CAP,
|
|
465
|
-
signal: controller.signal
|
|
466
|
-
});
|
|
467
|
-
if (!isCurrent()) {
|
|
468
|
-
return;
|
|
469
|
-
}
|
|
470
|
-
const status = observeSessionStatus(window.events, sessionStatusRef);
|
|
471
|
-
const retained = boundBrowserSessionEventWindow(window.events);
|
|
472
|
-
eventWindowRef.current = retained;
|
|
473
|
-
oldestSequenceRef.current = retained.events[0]?.sequence ?? window.oldestSequence;
|
|
474
|
-
newestSequenceRef.current = maxResumeSequenceOrNull(retained.events) ?? window.newestSequence;
|
|
475
|
-
hasOlderRef.current = window.hasOlder || retained.truncated;
|
|
476
|
-
hasNewerRef.current = false;
|
|
477
|
-
lastSequenceRef.current = window.newestSequence;
|
|
478
|
-
streamResumeSequenceRef.current = window.newestSequence;
|
|
479
|
-
initialWindowLoadedRef.current = true;
|
|
480
|
-
if (status !== void 0) {
|
|
481
|
-
setSessionStatusProjection(status);
|
|
482
|
-
}
|
|
483
|
-
setEventWindow(retained);
|
|
484
|
-
setHasOlder(window.hasOlder || retained.truncated);
|
|
485
|
-
setHasNewer(false);
|
|
486
|
-
setInitialLoading(false);
|
|
487
|
-
}
|
|
488
|
-
if (fullReplay) {
|
|
489
|
-
setInitialLoading(false);
|
|
490
|
-
}
|
|
491
|
-
const stream = client.streamEvents(workspaceId, sessionId, {
|
|
492
|
-
after: streamResumeSequenceRef.current,
|
|
493
|
-
signal: controller.signal,
|
|
494
|
-
onOpen: () => {
|
|
495
|
-
void Promise.resolve().then(() => reconcileSession(sessionId)).catch((cause) => {
|
|
496
|
-
if (isCurrent()) {
|
|
497
|
-
setError(cause instanceof Error ? cause : new Error(String(cause)));
|
|
498
|
-
}
|
|
499
|
-
});
|
|
500
|
-
},
|
|
501
|
-
onStateChange: (state) => {
|
|
502
|
-
if (isCurrent()) {
|
|
503
|
-
setConnectionState(state);
|
|
504
|
-
}
|
|
505
|
-
}
|
|
506
|
-
});
|
|
507
|
-
for await (const event of stream) {
|
|
508
|
-
if (!isCurrent()) break;
|
|
509
|
-
const boundedEvent = boundBrowserLegacyEvent(event);
|
|
510
|
-
const boundedEventBytes = browserJsonBytes(boundedEvent);
|
|
511
|
-
const separatorBytes = pending.length === 0 ? 0 : 1;
|
|
512
|
-
if (pending.length > 0 && (pending.length >= SESSION_EVENT_BROWSER_PENDING_MAX_COUNT || pendingBytes + separatorBytes + boundedEventBytes > SESSION_EVENT_BROWSER_PENDING_MAX_BYTES)) {
|
|
513
|
-
flush();
|
|
514
|
-
}
|
|
515
|
-
pending.push(boundedEvent);
|
|
516
|
-
pendingBytes += (pending.length === 1 ? 0 : 1) + boundedEventBytes;
|
|
517
|
-
if (pending.length >= SESSION_EVENT_BROWSER_PENDING_MAX_COUNT || pendingBytes >= SESSION_EVENT_BROWSER_PENDING_MAX_BYTES) {
|
|
518
|
-
flush();
|
|
519
|
-
} else {
|
|
520
|
-
scheduleFlush();
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
if (isCurrent()) {
|
|
524
|
-
flush();
|
|
525
|
-
setConnectionState("ended");
|
|
526
|
-
}
|
|
527
|
-
} catch (cause) {
|
|
528
|
-
if (isCurrent()) {
|
|
529
|
-
flush();
|
|
530
|
-
setError(cause instanceof Error ? cause : new Error(String(cause)));
|
|
531
|
-
setConnectionState("error");
|
|
532
|
-
setInitialLoading(false);
|
|
533
|
-
}
|
|
534
|
-
}
|
|
535
|
-
})();
|
|
536
|
-
return () => {
|
|
537
|
-
controller.abort();
|
|
538
|
-
if (generationRef.current === generation) {
|
|
539
|
-
generationRef.current = generation + 1;
|
|
540
|
-
}
|
|
541
|
-
if (streamAbortRef.current === controller) {
|
|
542
|
-
streamAbortRef.current = null;
|
|
543
|
-
}
|
|
544
|
-
if (flushTimer !== null) {
|
|
545
|
-
clearTimeout(flushTimer);
|
|
546
|
-
}
|
|
547
|
-
};
|
|
548
|
-
}, [
|
|
549
|
-
client,
|
|
550
|
-
workspaceId,
|
|
551
|
-
sessionId,
|
|
552
|
-
after,
|
|
553
|
-
enabled,
|
|
554
|
-
pageLive,
|
|
555
|
-
streamEnabled,
|
|
556
|
-
fullReplay,
|
|
557
|
-
streamKey,
|
|
558
|
-
streamEpoch,
|
|
559
|
-
viewMode,
|
|
560
|
-
reconcileSession
|
|
561
|
-
]);
|
|
562
|
-
const navigationBusy = () => loadingOlderRef.current || loadingNewerRef.current || loadingOldestRef.current || loadingLatestRef.current;
|
|
563
|
-
const loadOlder = useCallback2(
|
|
564
|
-
() => createOlderHistoryLoadReceipt(async (markCommitted) => {
|
|
565
|
-
if (!sessionId || navigationBusy() || !hasOlderRef.current) {
|
|
566
|
-
return false;
|
|
567
|
-
}
|
|
568
|
-
const before = oldestSequenceRef.current;
|
|
569
|
-
if (before === null) {
|
|
570
|
-
oldestSequenceRef.current = null;
|
|
571
|
-
hasOlderRef.current = false;
|
|
572
|
-
setHasOlder(false);
|
|
573
|
-
return false;
|
|
574
|
-
}
|
|
575
|
-
const generation = generationRef.current;
|
|
576
|
-
loadingOlderRef.current = true;
|
|
577
|
-
setLoadingOlder(true);
|
|
578
|
-
let published = false;
|
|
579
|
-
try {
|
|
580
|
-
const window = await loadEventWindow(client, workspaceId, sessionId, {
|
|
581
|
-
before,
|
|
582
|
-
pageSize: OLDER_PAGE_SIZE,
|
|
583
|
-
targetGroups: OLDER_GROUP_TARGET,
|
|
584
|
-
maxFetches: OLDER_FETCH_CAP
|
|
585
|
-
});
|
|
586
|
-
if (generationRef.current !== generation) {
|
|
587
|
-
return false;
|
|
588
|
-
}
|
|
589
|
-
if (window.events.length === 0) {
|
|
590
|
-
oldestSequenceRef.current = null;
|
|
591
|
-
hasOlderRef.current = false;
|
|
592
|
-
setHasOlder(false);
|
|
593
|
-
return false;
|
|
594
|
-
}
|
|
595
|
-
const current = eventWindowRef.current;
|
|
596
|
-
assertPrependOrder(current.events, window.events);
|
|
597
|
-
streamAbortRef.current?.abort();
|
|
598
|
-
const status = observeSessionStatus(window.events, sessionStatusRef);
|
|
599
|
-
const next = boundBrowserSessionEventWindow([...window.events, ...current.events], {
|
|
600
|
-
direction: "oldest"
|
|
601
|
-
});
|
|
602
|
-
const retained = {
|
|
603
|
-
...next,
|
|
604
|
-
truncated: current.truncated || next.truncated
|
|
605
|
-
};
|
|
606
|
-
const retainedOldest = retained.events[0]?.sequence ?? null;
|
|
607
|
-
if (retainedOldest === null || retainedOldest >= before) {
|
|
608
|
-
throw new Error("@opengeni/react: loadOlder made no durable sequence progress");
|
|
609
|
-
}
|
|
610
|
-
const retainedNewest = maxResumeSequenceOrNull(retained.events);
|
|
611
|
-
const previousNewest = maxResumeSequenceOrNull(current.events);
|
|
612
|
-
eventWindowRef.current = retained;
|
|
613
|
-
markCommitted();
|
|
614
|
-
oldestSequenceRef.current = retainedOldest;
|
|
615
|
-
newestSequenceRef.current = retainedNewest;
|
|
616
|
-
streamResumeSequenceRef.current = maxResumeSequence(retained.events);
|
|
617
|
-
const olderStillAvailable = window.hasOlder;
|
|
618
|
-
hasOlderRef.current = olderStillAvailable;
|
|
619
|
-
const evictedLiveTail = previousNewest !== null && retainedNewest !== null && retainedNewest < previousNewest;
|
|
620
|
-
let newer = null;
|
|
621
|
-
let enterHistory = false;
|
|
622
|
-
let reconnectLive = false;
|
|
623
|
-
if (viewModeRef.current === "history" || evictedLiveTail) {
|
|
624
|
-
const highWater = lastSequenceRef.current;
|
|
625
|
-
newer = retainedNewest !== null && (retainedNewest < highWater || retained.truncated);
|
|
626
|
-
hasNewerRef.current = newer;
|
|
627
|
-
if (evictedLiveTail) {
|
|
628
|
-
viewModeRef.current = "history";
|
|
629
|
-
enterHistory = true;
|
|
630
|
-
}
|
|
631
|
-
} else {
|
|
632
|
-
reconnectLive = true;
|
|
633
|
-
}
|
|
634
|
-
loadingOlderRef.current = false;
|
|
635
|
-
startTransition(() => {
|
|
636
|
-
if (status !== void 0) {
|
|
637
|
-
setSessionStatusProjection(status);
|
|
638
|
-
}
|
|
639
|
-
setEventWindow(retained);
|
|
640
|
-
setHasOlder(olderStillAvailable);
|
|
641
|
-
if (newer !== null) {
|
|
642
|
-
setHasNewer(newer);
|
|
643
|
-
}
|
|
644
|
-
if (enterHistory) {
|
|
645
|
-
setViewMode("history");
|
|
646
|
-
} else if (reconnectLive) {
|
|
647
|
-
setStreamEpoch((epoch) => epoch + 1);
|
|
648
|
-
}
|
|
649
|
-
setLoadingOlder(false);
|
|
650
|
-
});
|
|
651
|
-
published = true;
|
|
652
|
-
return olderStillAvailable;
|
|
653
|
-
} finally {
|
|
654
|
-
if (!published) {
|
|
655
|
-
loadingOlderRef.current = false;
|
|
656
|
-
setLoadingOlder(false);
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
}),
|
|
660
|
-
[client, workspaceId, sessionId]
|
|
661
|
-
);
|
|
662
|
-
const loadOldest = useCallback2(async () => {
|
|
663
|
-
if (!sessionId || navigationBusy() || !hasOlderRef.current) {
|
|
664
|
-
return false;
|
|
665
|
-
}
|
|
666
|
-
const generation = generationRef.current;
|
|
667
|
-
loadingOldestRef.current = true;
|
|
668
|
-
setLoadingOldest(true);
|
|
669
|
-
let published = false;
|
|
670
|
-
try {
|
|
671
|
-
const window = await loadForwardEventWindow(client, workspaceId, sessionId, {
|
|
672
|
-
after: 0,
|
|
673
|
-
pageSize: OLDEST_PAGE_SIZE,
|
|
674
|
-
targetGroups: OLDEST_GROUP_TARGET,
|
|
675
|
-
maxFetches: OLDEST_FETCH_CAP
|
|
676
|
-
});
|
|
677
|
-
if (generationRef.current !== generation) {
|
|
678
|
-
return false;
|
|
679
|
-
}
|
|
680
|
-
if (window.events.length === 0) {
|
|
681
|
-
hasOlderRef.current = false;
|
|
682
|
-
setHasOlder(false);
|
|
683
|
-
return false;
|
|
684
|
-
}
|
|
685
|
-
streamAbortRef.current?.abort();
|
|
686
|
-
const status = observeSessionStatus(window.events, sessionStatusRef);
|
|
687
|
-
const retained = boundBrowserSessionEventWindow(window.events, { direction: "oldest" });
|
|
688
|
-
const retainedOldest = retained.events[0]?.sequence ?? null;
|
|
689
|
-
const retainedNewest = maxResumeSequenceOrNull(retained.events);
|
|
690
|
-
eventWindowRef.current = retained;
|
|
691
|
-
oldestSequenceRef.current = retainedOldest;
|
|
692
|
-
newestSequenceRef.current = retainedNewest;
|
|
693
|
-
hasOlderRef.current = false;
|
|
694
|
-
const highWater = Math.max(lastSequenceRef.current, retainedNewest ?? 0);
|
|
695
|
-
lastSequenceRef.current = highWater;
|
|
696
|
-
const newer = window.hasNewer || retained.truncated || retainedNewest !== null && retainedNewest < highWater;
|
|
697
|
-
hasNewerRef.current = newer;
|
|
698
|
-
initialWindowLoadedRef.current = true;
|
|
699
|
-
viewModeRef.current = "history";
|
|
700
|
-
loadingOldestRef.current = false;
|
|
701
|
-
startTransition(() => {
|
|
702
|
-
if (status !== void 0) {
|
|
703
|
-
setSessionStatusProjection(status);
|
|
704
|
-
}
|
|
705
|
-
setEventWindow(retained);
|
|
706
|
-
setHasOlder(false);
|
|
707
|
-
setHasNewer(newer);
|
|
708
|
-
setViewMode("history");
|
|
709
|
-
setLoadingOldest(false);
|
|
710
|
-
});
|
|
711
|
-
published = true;
|
|
712
|
-
return newer;
|
|
713
|
-
} finally {
|
|
714
|
-
if (!published) {
|
|
715
|
-
loadingOldestRef.current = false;
|
|
716
|
-
setLoadingOldest(false);
|
|
717
|
-
}
|
|
718
|
-
}
|
|
719
|
-
}, [client, workspaceId, sessionId]);
|
|
720
|
-
const loadNewer = useCallback2(async () => {
|
|
721
|
-
if (!sessionId || navigationBusy() || !hasNewerRef.current) {
|
|
722
|
-
return false;
|
|
723
|
-
}
|
|
724
|
-
const afterSequence = newestSequenceRef.current;
|
|
725
|
-
if (afterSequence === null) {
|
|
726
|
-
hasNewerRef.current = false;
|
|
727
|
-
setHasNewer(false);
|
|
728
|
-
return false;
|
|
729
|
-
}
|
|
730
|
-
const generation = generationRef.current;
|
|
731
|
-
loadingNewerRef.current = true;
|
|
732
|
-
setLoadingNewer(true);
|
|
733
|
-
let published = false;
|
|
734
|
-
try {
|
|
735
|
-
const window = await loadForwardEventWindow(client, workspaceId, sessionId, {
|
|
736
|
-
after: afterSequence,
|
|
737
|
-
pageSize: NEWER_PAGE_SIZE,
|
|
738
|
-
targetGroups: NEWER_GROUP_TARGET,
|
|
739
|
-
maxFetches: NEWER_FETCH_CAP
|
|
740
|
-
});
|
|
741
|
-
if (generationRef.current !== generation) {
|
|
742
|
-
return false;
|
|
743
|
-
}
|
|
744
|
-
if (window.events.length === 0) {
|
|
745
|
-
hasNewerRef.current = false;
|
|
746
|
-
setHasNewer(false);
|
|
747
|
-
streamResumeSequenceRef.current = afterSequence;
|
|
748
|
-
viewModeRef.current = "live";
|
|
749
|
-
setViewMode("live");
|
|
750
|
-
setStreamEpoch((epoch) => epoch + 1);
|
|
751
|
-
return false;
|
|
752
|
-
}
|
|
753
|
-
const current = eventWindowRef.current;
|
|
754
|
-
assertAppendOrder(current.events, window.events);
|
|
755
|
-
const status = observeSessionStatus(window.events, sessionStatusRef);
|
|
756
|
-
const previousOldest = current.events[0]?.sequence ?? null;
|
|
757
|
-
const next = boundBrowserSessionEventWindow([...current.events, ...window.events], {
|
|
758
|
-
direction: "newest"
|
|
759
|
-
});
|
|
760
|
-
const retained = {
|
|
761
|
-
...next,
|
|
762
|
-
truncated: current.truncated || next.truncated || window.hasNewer
|
|
763
|
-
};
|
|
764
|
-
const retainedOldest = retained.events[0]?.sequence ?? null;
|
|
765
|
-
const retainedNewest = maxResumeSequenceOrNull(retained.events);
|
|
766
|
-
if (retainedNewest === null || retainedNewest <= afterSequence) {
|
|
767
|
-
throw new Error("@opengeni/react: loadNewer made no durable sequence progress");
|
|
768
|
-
}
|
|
769
|
-
eventWindowRef.current = retained;
|
|
770
|
-
oldestSequenceRef.current = retainedOldest;
|
|
771
|
-
newestSequenceRef.current = retainedNewest;
|
|
772
|
-
if (retainedOldest !== null && (previousOldest === null || retainedOldest > previousOldest || retained.truncated && retained.events[0]?.type !== "session.created" && retainedOldest > 1)) {
|
|
773
|
-
hasOlderRef.current = true;
|
|
774
|
-
}
|
|
775
|
-
const rearmedOlder = hasOlderRef.current;
|
|
776
|
-
const highWater = Math.max(lastSequenceRef.current, retainedNewest);
|
|
777
|
-
lastSequenceRef.current = highWater;
|
|
778
|
-
const newer = window.hasNewer || retainedNewest < highWater;
|
|
779
|
-
hasNewerRef.current = newer;
|
|
780
|
-
let resumeLive = false;
|
|
781
|
-
if (!newer) {
|
|
782
|
-
streamResumeSequenceRef.current = retainedNewest;
|
|
783
|
-
viewModeRef.current = "live";
|
|
784
|
-
resumeLive = true;
|
|
785
|
-
}
|
|
786
|
-
loadingNewerRef.current = false;
|
|
787
|
-
startTransition(() => {
|
|
788
|
-
if (status !== void 0) {
|
|
789
|
-
setSessionStatusProjection(status);
|
|
790
|
-
}
|
|
791
|
-
setEventWindow(retained);
|
|
792
|
-
setHasOlder(rearmedOlder);
|
|
793
|
-
setHasNewer(newer);
|
|
794
|
-
if (resumeLive) {
|
|
795
|
-
setViewMode("live");
|
|
796
|
-
setStreamEpoch((epoch) => epoch + 1);
|
|
797
|
-
}
|
|
798
|
-
setLoadingNewer(false);
|
|
799
|
-
});
|
|
800
|
-
published = true;
|
|
801
|
-
return newer;
|
|
802
|
-
} finally {
|
|
803
|
-
if (!published) {
|
|
804
|
-
loadingNewerRef.current = false;
|
|
805
|
-
setLoadingNewer(false);
|
|
806
|
-
}
|
|
807
|
-
}
|
|
808
|
-
}, [client, workspaceId, sessionId]);
|
|
809
|
-
const jumpToLatest = useCallback2(async () => {
|
|
810
|
-
if (!sessionId || navigationBusy()) {
|
|
811
|
-
return;
|
|
812
|
-
}
|
|
813
|
-
loadingLatestRef.current = true;
|
|
814
|
-
setLoadingLatest(true);
|
|
815
|
-
let published = false;
|
|
816
|
-
try {
|
|
817
|
-
streamAbortRef.current?.abort();
|
|
818
|
-
hasNewerRef.current = false;
|
|
819
|
-
hasOlderRef.current = false;
|
|
820
|
-
newestSequenceRef.current = null;
|
|
821
|
-
oldestSequenceRef.current = null;
|
|
822
|
-
eventWindowRef.current = EMPTY_EVENT_WINDOW;
|
|
823
|
-
initialWindowLoadedRef.current = false;
|
|
824
|
-
streamResumeSequenceRef.current = after;
|
|
825
|
-
viewModeRef.current = "live";
|
|
826
|
-
loadingLatestRef.current = false;
|
|
827
|
-
startTransition(() => {
|
|
828
|
-
setHasNewer(false);
|
|
829
|
-
setHasOlder(false);
|
|
830
|
-
setEventWindow(EMPTY_EVENT_WINDOW);
|
|
831
|
-
setInitialLoading(true);
|
|
832
|
-
setViewMode("live");
|
|
833
|
-
setStreamEpoch((epoch) => epoch + 1);
|
|
834
|
-
setLoadingLatest(false);
|
|
835
|
-
});
|
|
836
|
-
published = true;
|
|
837
|
-
await Promise.resolve();
|
|
838
|
-
} finally {
|
|
839
|
-
if (!published) {
|
|
840
|
-
loadingLatestRef.current = false;
|
|
841
|
-
setLoadingLatest(false);
|
|
842
|
-
}
|
|
843
|
-
}
|
|
844
|
-
}, [after, sessionId]);
|
|
845
|
-
const identityMatches = stateStreamKey === streamKey;
|
|
846
|
-
const visibleEvents = identityMatches ? eventWindow.events : EMPTY_EVENTS;
|
|
847
|
-
const timeline = useMemo2(() => buildTimeline(visibleEvents), [visibleEvents]);
|
|
848
|
-
return {
|
|
849
|
-
events: visibleEvents,
|
|
850
|
-
timeline,
|
|
851
|
-
sessionStatus: identityMatches ? sessionStatusProjection : null,
|
|
852
|
-
connectionState: identityMatches ? connectionState : "idle",
|
|
853
|
-
lastSequence: identityMatches ? lastSequenceRef.current : after,
|
|
854
|
-
windowBytes: identityMatches ? eventWindow.bytes : 2,
|
|
855
|
-
windowTruncated: identityMatches ? eventWindow.truncated : false,
|
|
856
|
-
initialLoading: fullReplay ? false : identityMatches ? initialLoading : true,
|
|
857
|
-
hasOlder: !identityMatches ? false : hasOlder,
|
|
858
|
-
loadingOlder: !identityMatches ? false : loadingOlder,
|
|
859
|
-
loadOlder,
|
|
860
|
-
hasNewer: !identityMatches ? false : hasNewer,
|
|
861
|
-
loadingNewer: !identityMatches ? false : loadingNewer,
|
|
862
|
-
loadNewer,
|
|
863
|
-
loadingOldest: !identityMatches ? false : loadingOldest,
|
|
864
|
-
loadOldest,
|
|
865
|
-
loadingLatest: !identityMatches ? false : loadingLatest,
|
|
866
|
-
jumpToLatest,
|
|
867
|
-
error: identityMatches ? error : null
|
|
868
|
-
};
|
|
869
|
-
}
|
|
870
|
-
function boundBrowserSessionEventWindow(events, options = {}) {
|
|
871
|
-
const maxBytes = Math.max(1024, options.maxBytes ?? SESSION_EVENT_BROWSER_MAX_BYTES);
|
|
872
|
-
const maxCount = Math.max(1, Math.floor(options.maxCount ?? SESSION_EVENT_BROWSER_MAX_COUNT));
|
|
873
|
-
const safe = events.map(boundBrowserLegacyEvent);
|
|
874
|
-
const selected = [];
|
|
875
|
-
let bytes = 2;
|
|
876
|
-
const direction = options.direction ?? "newest";
|
|
877
|
-
const start = direction === "newest" ? safe.length - 1 : 0;
|
|
878
|
-
const end = direction === "newest" ? Math.max(-1, safe.length - maxCount - 1) : safe.length;
|
|
879
|
-
const step = direction === "newest" ? -1 : 1;
|
|
880
|
-
for (let index = start; index !== end && selected.length < maxCount; index += step) {
|
|
881
|
-
const event = safe[index];
|
|
882
|
-
const eventBytes = browserJsonBytes(event);
|
|
883
|
-
const separator = selected.length === 0 ? 0 : 1;
|
|
884
|
-
if (bytes + separator + eventBytes > maxBytes) break;
|
|
885
|
-
selected.push(event);
|
|
886
|
-
bytes += separator + eventBytes;
|
|
887
|
-
}
|
|
888
|
-
if (direction === "newest") selected.reverse();
|
|
889
|
-
return {
|
|
890
|
-
events: selected,
|
|
891
|
-
bytes,
|
|
892
|
-
truncated: selected.length < safe.length
|
|
893
|
-
};
|
|
894
|
-
}
|
|
895
|
-
function boundBrowserLegacyEvent(event) {
|
|
896
|
-
const serialized = browserSerialize(event);
|
|
897
|
-
const originalBytes = serialized.serializable ? encoder.encode(serialized.value).byteLength : null;
|
|
898
|
-
const typeIsSafe = browserUtf8Bytes(event.type) <= BROWSER_EVENT_TYPE_MAX_BYTES && !event.type.includes("\n") && !event.type.includes("\r");
|
|
899
|
-
const clientEventId = boundBrowserOptionalText(
|
|
900
|
-
event.clientEventId,
|
|
901
|
-
BROWSER_EVENT_CLIENT_ID_MAX_BYTES
|
|
902
|
-
);
|
|
903
|
-
const duplicateReason = boundBrowserOptionalText(
|
|
904
|
-
event.duplicateReason,
|
|
905
|
-
BROWSER_EVENT_DUPLICATE_REASON_MAX_BYTES
|
|
906
|
-
);
|
|
907
|
-
if (serialized.serializable && originalBytes !== null && originalBytes <= SESSION_EVENT_BROWSER_SINGLE_EVENT_MAX_BYTES && typeIsSafe && clientEventId === event.clientEventId && duplicateReason === event.duplicateReason) {
|
|
908
|
-
return event;
|
|
909
|
-
}
|
|
910
|
-
const envelopeProjection = [
|
|
911
|
-
!typeIsSafe ? browserEnvelopeFieldProjection("type", event.type, "session.event.envelope_omitted") : null,
|
|
912
|
-
clientEventId !== event.clientEventId ? browserEnvelopeFieldProjection("clientEventId", event.clientEventId, clientEventId) : null,
|
|
913
|
-
duplicateReason !== event.duplicateReason ? browserEnvelopeFieldProjection("duplicateReason", event.duplicateReason, duplicateReason) : null
|
|
914
|
-
].filter((field) => field !== null);
|
|
915
|
-
const payloadSerialization = browserSerialize(event.payload);
|
|
916
|
-
const payloadBytes = payloadSerialization.serializable ? encoder.encode(payloadSerialization.value).byteLength : null;
|
|
917
|
-
const preview = truncateBrowserUtf8Middle(
|
|
918
|
-
payloadSerialization.value,
|
|
919
|
-
BROWSER_EVENT_PAYLOAD_PREVIEW_MAX_BYTES
|
|
920
|
-
);
|
|
921
|
-
const truncation = {
|
|
922
|
-
truncated: true,
|
|
923
|
-
surface: "browser_legacy_guard",
|
|
924
|
-
reason: serialized.serializable ? "event_envelope_bytes_exceeded" : "event_not_serializable",
|
|
925
|
-
originalBytes,
|
|
926
|
-
deliveredBytes: 0,
|
|
927
|
-
omittedBytes: originalBytes,
|
|
928
|
-
estimatedOriginalTokens: originalBytes === null ? null : Math.ceil(originalBytes / 4),
|
|
929
|
-
estimatedDeliveredTokens: 0,
|
|
930
|
-
fullEvidence: { available: false, reason: "not_retained" },
|
|
931
|
-
details: [
|
|
932
|
-
{
|
|
933
|
-
path: "$.payload",
|
|
934
|
-
kind: "payload_preview",
|
|
935
|
-
originalBytes: payloadBytes,
|
|
936
|
-
deliveredBytes: browserUtf8Bytes(preview)
|
|
937
|
-
}
|
|
938
|
-
]
|
|
939
|
-
};
|
|
940
|
-
const payload = {
|
|
941
|
-
...browserPayloadIdentity(event.payload),
|
|
942
|
-
preview,
|
|
943
|
-
...envelopeProjection.length > 0 ? {
|
|
944
|
-
originalType: boundBrowserText(event.type, BROWSER_EVENT_TYPE_MAX_BYTES),
|
|
945
|
-
envelopeProjection: {
|
|
946
|
-
truncated: true,
|
|
947
|
-
surface: "browser_legacy_guard",
|
|
948
|
-
fields: envelopeProjection
|
|
949
|
-
}
|
|
950
|
-
} : {},
|
|
951
|
-
truncation
|
|
952
|
-
};
|
|
953
|
-
const bounded = {
|
|
954
|
-
id: boundBrowserText(event.id, BROWSER_EVENT_ID_MAX_BYTES),
|
|
955
|
-
workspaceId: boundBrowserText(event.workspaceId, BROWSER_EVENT_ID_MAX_BYTES),
|
|
956
|
-
sessionId: boundBrowserText(event.sessionId, BROWSER_EVENT_ID_MAX_BYTES),
|
|
957
|
-
sequence: event.sequence,
|
|
958
|
-
type: typeIsSafe ? event.type : "session.event.envelope_omitted",
|
|
959
|
-
payload,
|
|
960
|
-
occurredAt: boundBrowserText(event.occurredAt, BROWSER_EVENT_ID_MAX_BYTES),
|
|
961
|
-
clientEventId,
|
|
962
|
-
turnId: boundBrowserOptionalText(event.turnId, BROWSER_EVENT_ID_MAX_BYTES),
|
|
963
|
-
turnGeneration: event.turnGeneration,
|
|
964
|
-
turnAttemptId: boundBrowserOptionalText(event.turnAttemptId, BROWSER_EVENT_ID_MAX_BYTES),
|
|
965
|
-
turnAssociation: event.turnAssociation,
|
|
966
|
-
duplicateOfEventId: boundBrowserOptionalText(
|
|
967
|
-
event.duplicateOfEventId,
|
|
968
|
-
BROWSER_EVENT_ID_MAX_BYTES
|
|
969
|
-
),
|
|
970
|
-
duplicateReason
|
|
971
|
-
};
|
|
972
|
-
settleBrowserEventTruncation(bounded, truncation);
|
|
973
|
-
if (browserJsonBytes(bounded) > SESSION_EVENT_BROWSER_SINGLE_EVENT_MAX_BYTES) {
|
|
974
|
-
payload.preview = truncateBrowserUtf8Middle(String(payload.preview), 4 * 1024);
|
|
975
|
-
truncation.details = truncation.details.slice(0, 1);
|
|
976
|
-
settleBrowserEventTruncation(bounded, truncation);
|
|
977
|
-
}
|
|
978
|
-
if (browserJsonBytes(bounded) > SESSION_EVENT_BROWSER_SINGLE_EVENT_MAX_BYTES) {
|
|
979
|
-
bounded.clientEventId = null;
|
|
980
|
-
bounded.duplicateReason = null;
|
|
981
|
-
payload.preview = "[legacy event omitted at the browser byte boundary]";
|
|
982
|
-
settleBrowserEventTruncation(bounded, truncation);
|
|
983
|
-
}
|
|
984
|
-
return bounded;
|
|
985
|
-
}
|
|
986
|
-
function browserJsonBytes(value) {
|
|
987
|
-
return encoder.encode(browserSerialize(value).value).byteLength;
|
|
988
|
-
}
|
|
989
|
-
function browserSerialize(value) {
|
|
990
|
-
try {
|
|
991
|
-
const serialized = JSON.stringify(value);
|
|
992
|
-
return { value: serialized === void 0 ? "null" : serialized, serializable: true };
|
|
993
|
-
} catch {
|
|
994
|
-
return {
|
|
995
|
-
value: '"[unserializable event payload omitted at browser boundary]"',
|
|
996
|
-
serializable: false
|
|
997
|
-
};
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1000
|
-
function browserPayloadIdentity(payload) {
|
|
1001
|
-
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return {};
|
|
1002
|
-
const record = payload;
|
|
1003
|
-
const identity = {};
|
|
1004
|
-
for (const field of BROWSER_EVENT_PAYLOAD_IDENTITY_FIELDS) {
|
|
1005
|
-
const value = record[field];
|
|
1006
|
-
if (typeof value === "string") {
|
|
1007
|
-
identity[field] = boundBrowserText(value, BROWSER_EVENT_ID_MAX_BYTES);
|
|
1008
|
-
} else if (typeof value === "number" || typeof value === "boolean" || value === null) {
|
|
1009
|
-
identity[field] = value;
|
|
1010
|
-
}
|
|
1011
|
-
}
|
|
1012
|
-
return identity;
|
|
1013
|
-
}
|
|
1014
|
-
function browserEnvelopeFieldProjection(field, original, delivered) {
|
|
1015
|
-
return {
|
|
1016
|
-
field,
|
|
1017
|
-
originalBytes: typeof original === "string" ? browserUtf8Bytes(original) : 0,
|
|
1018
|
-
deliveredBytes: typeof delivered === "string" ? browserUtf8Bytes(delivered) : 0
|
|
1019
|
-
};
|
|
1020
|
-
}
|
|
1021
|
-
function boundBrowserOptionalText(value, maxBytes) {
|
|
1022
|
-
return typeof value === "string" ? boundBrowserText(value, maxBytes) : value;
|
|
1023
|
-
}
|
|
1024
|
-
function boundBrowserText(value, maxBytes) {
|
|
1025
|
-
const bytes = encoder.encode(value);
|
|
1026
|
-
if (bytes.byteLength <= maxBytes) return value;
|
|
1027
|
-
const marker = "\u2026[truncated]";
|
|
1028
|
-
const prefixBudget = Math.max(0, maxBytes - browserUtf8Bytes(marker));
|
|
1029
|
-
let prefixEnd = Math.min(prefixBudget, bytes.byteLength);
|
|
1030
|
-
while (prefixEnd > 0 && prefixEnd < bytes.byteLength && isBrowserUtf8Continuation(bytes[prefixEnd])) {
|
|
1031
|
-
prefixEnd -= 1;
|
|
1032
|
-
}
|
|
1033
|
-
return `${decoder.decode(bytes.subarray(0, prefixEnd))}${marker}`;
|
|
1034
|
-
}
|
|
1035
|
-
function truncateBrowserUtf8Middle(value, maxBytes) {
|
|
1036
|
-
const bytes = encoder.encode(value);
|
|
1037
|
-
if (bytes.byteLength <= maxBytes) return value;
|
|
1038
|
-
const marker = `\u2026[${bytes.byteLength - maxBytes} bytes omitted]\u2026`;
|
|
1039
|
-
const contentBudget = Math.max(0, maxBytes - browserUtf8Bytes(marker));
|
|
1040
|
-
const leftBudget = Math.floor(contentBudget / 2);
|
|
1041
|
-
const rightBudget = contentBudget - leftBudget;
|
|
1042
|
-
let leftEnd = Math.min(leftBudget, bytes.byteLength);
|
|
1043
|
-
while (leftEnd > 0 && leftEnd < bytes.byteLength && isBrowserUtf8Continuation(bytes[leftEnd])) {
|
|
1044
|
-
leftEnd -= 1;
|
|
1045
|
-
}
|
|
1046
|
-
let rightStart = Math.max(0, bytes.byteLength - rightBudget);
|
|
1047
|
-
while (rightStart < bytes.byteLength && isBrowserUtf8Continuation(bytes[rightStart])) {
|
|
1048
|
-
rightStart += 1;
|
|
1049
|
-
}
|
|
1050
|
-
return `${decoder.decode(bytes.subarray(0, leftEnd))}${marker}${decoder.decode(bytes.subarray(rightStart))}`;
|
|
1051
|
-
}
|
|
1052
|
-
function settleBrowserEventTruncation(event, truncation) {
|
|
1053
|
-
for (let attempt = 0; attempt < 16; attempt += 1) {
|
|
1054
|
-
const deliveredBytes = browserJsonBytes(event);
|
|
1055
|
-
const omittedBytes = truncation.originalBytes === null ? null : Math.max(0, truncation.originalBytes - deliveredBytes);
|
|
1056
|
-
const estimatedDeliveredTokens = Math.ceil(deliveredBytes / 4);
|
|
1057
|
-
if (truncation.deliveredBytes === deliveredBytes && truncation.omittedBytes === omittedBytes && truncation.estimatedDeliveredTokens === estimatedDeliveredTokens) {
|
|
1058
|
-
return;
|
|
1059
|
-
}
|
|
1060
|
-
truncation.deliveredBytes = deliveredBytes;
|
|
1061
|
-
truncation.omittedBytes = omittedBytes;
|
|
1062
|
-
truncation.estimatedDeliveredTokens = estimatedDeliveredTokens;
|
|
1063
|
-
}
|
|
1064
|
-
throw new RangeError("Browser event byte accounting did not converge");
|
|
1065
|
-
}
|
|
1066
|
-
function browserUtf8Bytes(value) {
|
|
1067
|
-
return encoder.encode(value).byteLength;
|
|
1068
|
-
}
|
|
1069
|
-
function isBrowserUtf8Continuation(value) {
|
|
1070
|
-
return (value & 192) === 128;
|
|
1071
|
-
}
|
|
1072
|
-
function observeSessionStatus(events, ref) {
|
|
1073
|
-
let latest = null;
|
|
1074
|
-
for (const event of events) {
|
|
1075
|
-
const status = sessionStatusFromEvents([event]);
|
|
1076
|
-
if (status && (!latest || event.sequence >= latest.sequence)) {
|
|
1077
|
-
latest = { sequence: event.sequence, status };
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
|
-
if (!latest || latest.sequence < ref.current.sequence) return void 0;
|
|
1081
|
-
ref.current = latest;
|
|
1082
|
-
return latest.status;
|
|
1083
|
-
}
|
|
1084
|
-
async function planForegroundCatchup(client, workspaceId, sessionId, cursor, options = {}) {
|
|
1085
|
-
if (options.signal?.aborted) throw abortError();
|
|
1086
|
-
const session = await client.getSession(workspaceId, sessionId, {
|
|
1087
|
-
fresh: true,
|
|
1088
|
-
...options.signal ? { signal: options.signal } : {}
|
|
1089
|
-
});
|
|
1090
|
-
if (options.signal?.aborted) throw abortError();
|
|
1091
|
-
const durableHead = Math.max(cursor, session.lastSequence);
|
|
1092
|
-
const rawGap = durableHead - cursor;
|
|
1093
|
-
if (rawGap <= FOREGROUND_DIRECT_REPLAY_MAX_SEQUENCES) {
|
|
1094
|
-
return { kind: "resume" };
|
|
1095
|
-
}
|
|
1096
|
-
if (rawGap > FOREGROUND_COMPACT_PROBE_MAX_SEQUENCES) {
|
|
1097
|
-
return { kind: "reload" };
|
|
1098
|
-
}
|
|
1099
|
-
const page = await loadNextPage(client, workspaceId, sessionId, cursor, {
|
|
1100
|
-
pageSize: rawGap,
|
|
1101
|
-
...options.signal ? { signal: options.signal } : {}
|
|
1102
|
-
});
|
|
1103
|
-
const events = page.filter((event) => eventResumeSequence(event) > cursor).map(boundBrowserLegacyEvent);
|
|
1104
|
-
if (events.length === 0) {
|
|
1105
|
-
return { kind: "reload" };
|
|
1106
|
-
}
|
|
1107
|
-
assertAscending(events);
|
|
1108
|
-
const resumeSequence = maxResumeSequence(events);
|
|
1109
|
-
if (resumeSequence < durableHead) {
|
|
1110
|
-
return { kind: "reload" };
|
|
1111
|
-
}
|
|
1112
|
-
if (events.length > FOREGROUND_COMPACT_CATCHUP_MAX_EVENTS || groupCount(events) > FOREGROUND_COMPACT_CATCHUP_MAX_GROUPS || browserJsonBytes(events) > FOREGROUND_COMPACT_CATCHUP_MAX_BYTES) {
|
|
1113
|
-
return { kind: "reload" };
|
|
1114
|
-
}
|
|
1115
|
-
return { kind: "append", events, resumeSequence };
|
|
1116
|
-
}
|
|
1117
|
-
async function loadEventWindow(client, workspaceId, sessionId, options) {
|
|
1118
|
-
let cursor = options.before;
|
|
1119
|
-
let buffer = [];
|
|
1120
|
-
let reachedStart = false;
|
|
1121
|
-
let fetches = 0;
|
|
1122
|
-
while (fetches < options.maxFetches) {
|
|
1123
|
-
if (buffer.length > 0 && groupCount(buffer) >= options.targetGroups) {
|
|
1124
|
-
break;
|
|
1125
|
-
}
|
|
1126
|
-
const page = await loadPreviousPage(client, workspaceId, sessionId, cursor, {
|
|
1127
|
-
pageSize: options.pageSize,
|
|
1128
|
-
...options.signal ? { signal: options.signal } : {}
|
|
1129
|
-
});
|
|
1130
|
-
fetches += 1;
|
|
1131
|
-
if (page.length === 0) {
|
|
1132
|
-
reachedStart = true;
|
|
1133
|
-
break;
|
|
1134
|
-
}
|
|
1135
|
-
assertAscending(page);
|
|
1136
|
-
buffer = [...page, ...buffer];
|
|
1137
|
-
cursor = page[0].sequence;
|
|
1138
|
-
if (isLogStart(page[0])) {
|
|
1139
|
-
reachedStart = true;
|
|
1140
|
-
break;
|
|
1141
|
-
}
|
|
1142
|
-
}
|
|
1143
|
-
let snapPages = 0;
|
|
1144
|
-
while (!reachedStart && findBoundaryIndex(buffer) === -1 && snapPages < BOUNDARY_PAGE_CAP && fetches < options.maxFetches) {
|
|
1145
|
-
const page = await loadPreviousPage(client, workspaceId, sessionId, cursor, {
|
|
1146
|
-
pageSize: options.pageSize,
|
|
1147
|
-
...options.signal ? { signal: options.signal } : {}
|
|
1148
|
-
});
|
|
1149
|
-
fetches += 1;
|
|
1150
|
-
snapPages += 1;
|
|
1151
|
-
if (page.length === 0) {
|
|
1152
|
-
reachedStart = true;
|
|
1153
|
-
break;
|
|
1154
|
-
}
|
|
1155
|
-
assertAscending(page);
|
|
1156
|
-
buffer = [...page, ...buffer];
|
|
1157
|
-
cursor = page[0].sequence;
|
|
1158
|
-
if (isLogStart(page[0])) {
|
|
1159
|
-
reachedStart = true;
|
|
1160
|
-
break;
|
|
1161
|
-
}
|
|
1162
|
-
}
|
|
1163
|
-
if (!reachedStart) {
|
|
1164
|
-
const boundary = findBoundaryIndex(buffer);
|
|
1165
|
-
if (boundary > 0) {
|
|
1166
|
-
buffer = buffer.slice(boundary);
|
|
1167
|
-
}
|
|
1168
|
-
}
|
|
1169
|
-
const densityWindow = trimBackwardToGroupTarget(buffer, options.targetGroups);
|
|
1170
|
-
buffer = densityWindow.events;
|
|
1171
|
-
const oldest = buffer[0] ?? null;
|
|
1172
|
-
const newest = buffer[buffer.length - 1] ?? null;
|
|
1173
|
-
return {
|
|
1174
|
-
events: buffer,
|
|
1175
|
-
oldestSequence: oldest?.sequence ?? null,
|
|
1176
|
-
newestSequence: newest ? maxResumeSequence(buffer) : 0,
|
|
1177
|
-
hasOlder: buffer.length > 0 && (densityWindow.trimmed || !reachedStart && oldest?.type !== "session.created")
|
|
1178
|
-
};
|
|
1179
|
-
}
|
|
1180
|
-
function findBoundaryIndex(events) {
|
|
1181
|
-
for (let index = 0; index < events.length; index += 1) {
|
|
1182
|
-
if (isTurnBoundary(events[index])) {
|
|
1183
|
-
return index;
|
|
1184
|
-
}
|
|
1185
|
-
}
|
|
1186
|
-
return -1;
|
|
1187
|
-
}
|
|
1188
|
-
function isTurnBoundary(event) {
|
|
1189
|
-
return event.type === "session.created" || event.type === "user.message";
|
|
1190
|
-
}
|
|
1191
|
-
async function loadForwardEventWindow(client, workspaceId, sessionId, options) {
|
|
1192
|
-
let cursor = options.after;
|
|
1193
|
-
let buffer = [];
|
|
1194
|
-
let fetches = 0;
|
|
1195
|
-
let reachedEnd = false;
|
|
1196
|
-
while (fetches < options.maxFetches) {
|
|
1197
|
-
if (buffer.length > 0 && groupCount(buffer) >= options.targetGroups) {
|
|
1198
|
-
break;
|
|
1199
|
-
}
|
|
1200
|
-
const page = await loadNextPage(client, workspaceId, sessionId, cursor, {
|
|
1201
|
-
pageSize: options.pageSize,
|
|
1202
|
-
...options.signal ? { signal: options.signal } : {}
|
|
1203
|
-
});
|
|
1204
|
-
fetches += 1;
|
|
1205
|
-
if (page.length === 0) {
|
|
1206
|
-
reachedEnd = true;
|
|
1207
|
-
break;
|
|
1208
|
-
}
|
|
1209
|
-
assertAscending(page);
|
|
1210
|
-
buffer = [...buffer, ...page];
|
|
1211
|
-
cursor = maxResumeSequence(page);
|
|
1212
|
-
}
|
|
1213
|
-
const densityWindow = trimForwardToGroupTarget(buffer, options.targetGroups);
|
|
1214
|
-
buffer = densityWindow.events;
|
|
1215
|
-
const oldest = buffer[0] ?? null;
|
|
1216
|
-
const newest = buffer[buffer.length - 1] ?? null;
|
|
1217
|
-
return {
|
|
1218
|
-
events: buffer,
|
|
1219
|
-
oldestSequence: oldest?.sequence ?? null,
|
|
1220
|
-
newestSequence: newest ? maxResumeSequence(buffer) : 0,
|
|
1221
|
-
hasNewer: buffer.length > 0 && (densityWindow.trimmed || !reachedEnd)
|
|
1222
|
-
};
|
|
1223
|
-
}
|
|
1224
|
-
function trimBackwardToGroupTarget(events, targetGroups) {
|
|
1225
|
-
if (!Number.isFinite(targetGroups) || targetGroups <= 0 || events.length === 0) {
|
|
1226
|
-
return { events, trimmed: false };
|
|
1227
|
-
}
|
|
1228
|
-
for (let index = events.length - 1; index > 0; index -= 1) {
|
|
1229
|
-
if (!isTurnBoundary(events[index])) continue;
|
|
1230
|
-
const candidate = events.slice(index);
|
|
1231
|
-
if (groupCount(candidate) >= targetGroups) {
|
|
1232
|
-
return { events: candidate, trimmed: true };
|
|
1233
|
-
}
|
|
1234
|
-
}
|
|
1235
|
-
return { events, trimmed: false };
|
|
1236
|
-
}
|
|
1237
|
-
function trimForwardToGroupTarget(events, targetGroups) {
|
|
1238
|
-
if (!Number.isFinite(targetGroups) || targetGroups <= 0 || events.length === 0) {
|
|
1239
|
-
return { events, trimmed: false };
|
|
1240
|
-
}
|
|
1241
|
-
for (let index = 1; index < events.length; index += 1) {
|
|
1242
|
-
if (!isTurnBoundary(events[index])) continue;
|
|
1243
|
-
const candidate = events.slice(0, index);
|
|
1244
|
-
if (groupCount(candidate) >= targetGroups) {
|
|
1245
|
-
return { events: candidate, trimmed: true };
|
|
1246
|
-
}
|
|
1247
|
-
}
|
|
1248
|
-
return { events, trimmed: false };
|
|
1249
|
-
}
|
|
1250
|
-
async function loadPreviousPage(client, workspaceId, sessionId, before, options) {
|
|
1251
|
-
if (options.signal?.aborted) {
|
|
1252
|
-
throw abortError();
|
|
1253
|
-
}
|
|
1254
|
-
const requested = options.pageSize;
|
|
1255
|
-
if (requested === 0) {
|
|
1256
|
-
return Object.assign([], { requested });
|
|
1257
|
-
}
|
|
1258
|
-
const page = await client.listEvents(workspaceId, sessionId, {
|
|
1259
|
-
before,
|
|
1260
|
-
limit: requested,
|
|
1261
|
-
compact: true,
|
|
1262
|
-
// Monitoring summaries omit correlation fields once a bounded durable
|
|
1263
|
-
// payload exceeds their preview threshold. The browser timeline needs the
|
|
1264
|
-
// exact stored payload so persisted tool outputs still match their
|
|
1265
|
-
// calls and expose truthful truncation telemetry after a reload.
|
|
1266
|
-
payloadMode: "full"
|
|
1267
|
-
});
|
|
1268
|
-
if (options.signal?.aborted) {
|
|
1269
|
-
throw abortError();
|
|
1270
|
-
}
|
|
1271
|
-
return Object.assign(page, { requested });
|
|
1272
|
-
}
|
|
1273
|
-
async function loadNextPage(client, workspaceId, sessionId, after, options) {
|
|
1274
|
-
if (options.signal?.aborted) {
|
|
1275
|
-
throw abortError();
|
|
1276
|
-
}
|
|
1277
|
-
const requested = options.pageSize;
|
|
1278
|
-
if (requested === 0) {
|
|
1279
|
-
return Object.assign([], { requested });
|
|
1280
|
-
}
|
|
1281
|
-
const page = await client.listEvents(workspaceId, sessionId, {
|
|
1282
|
-
after,
|
|
1283
|
-
limit: requested,
|
|
1284
|
-
compact: true,
|
|
1285
|
-
direction: "after",
|
|
1286
|
-
payloadMode: "full"
|
|
1287
|
-
});
|
|
1288
|
-
if (options.signal?.aborted) {
|
|
1289
|
-
throw abortError();
|
|
1290
|
-
}
|
|
1291
|
-
return Object.assign(page, { requested });
|
|
1292
|
-
}
|
|
1293
|
-
function groupCount(events) {
|
|
1294
|
-
if (events.length === 0) {
|
|
1295
|
-
return 0;
|
|
1296
|
-
}
|
|
1297
|
-
return groupTimeline(buildTimeline(events)).length;
|
|
1298
|
-
}
|
|
1299
|
-
function isLogStart(event) {
|
|
1300
|
-
return event.type === "session.created" || event.sequence <= 1;
|
|
1301
|
-
}
|
|
1302
|
-
function maxResumeSequence(events) {
|
|
1303
|
-
return events.reduce((max, event) => Math.max(max, eventResumeSequence(event)), 0);
|
|
1304
|
-
}
|
|
1305
|
-
function maxResumeSequenceOrNull(events) {
|
|
1306
|
-
return events.length > 0 ? maxResumeSequence(events) : null;
|
|
1307
|
-
}
|
|
1308
|
-
function eventResumeSequence(event) {
|
|
1309
|
-
const streamedCoverage = sessionEventStreamCoveredThrough(event);
|
|
1310
|
-
if (streamedCoverage !== null) return streamedCoverage;
|
|
1311
|
-
return typeof event.coveredThrough === "number" && Number.isSafeInteger(event.coveredThrough) && event.coveredThrough >= event.sequence ? event.coveredThrough : event.sequence;
|
|
1312
|
-
}
|
|
1313
|
-
function assertAscending(events) {
|
|
1314
|
-
for (let index = 1; index < events.length; index += 1) {
|
|
1315
|
-
if (events[index - 1].sequence >= events[index].sequence) {
|
|
1316
|
-
throw new Error("@opengeni/react: session events must be ordered by ascending sequence");
|
|
1317
|
-
}
|
|
1318
|
-
}
|
|
1319
|
-
}
|
|
1320
|
-
function assertPrependOrder(existing, older) {
|
|
1321
|
-
if (!shouldAssertDevelopment() || existing.length === 0 || older.length === 0) {
|
|
1322
|
-
return;
|
|
1323
|
-
}
|
|
1324
|
-
if (older[older.length - 1].sequence >= existing[0].sequence) {
|
|
1325
|
-
throw new Error("@opengeni/react: loadOlder returned overlapping session events");
|
|
1326
|
-
}
|
|
1327
|
-
}
|
|
1328
|
-
function assertAppendOrder(existing, newer) {
|
|
1329
|
-
if (!shouldAssertDevelopment() || existing.length === 0 || newer.length === 0) {
|
|
1330
|
-
return;
|
|
1331
|
-
}
|
|
1332
|
-
if (newer[0].sequence <= existing[existing.length - 1].sequence) {
|
|
1333
|
-
throw new Error("@opengeni/react: loadNewer returned overlapping session events");
|
|
1334
|
-
}
|
|
1335
|
-
}
|
|
1336
|
-
function shouldAssertDevelopment() {
|
|
1337
|
-
const processEnv = globalThis.process?.env;
|
|
1338
|
-
return processEnv !== void 0 && processEnv.NODE_ENV !== "production";
|
|
1339
|
-
}
|
|
1340
|
-
function abortError() {
|
|
1341
|
-
const error = new Error("The operation was aborted.");
|
|
1342
|
-
error.name = "AbortError";
|
|
1343
|
-
return error;
|
|
1344
|
-
}
|
|
1345
|
-
|
|
1346
|
-
// src/hooks/use-file-attachments.ts
|
|
1347
|
-
import { useCallback as useCallback3, useEffect as useEffect3, useRef as useRef3, useState as useState3 } from "react";
|
|
1348
|
-
var isImage = (file) => file.type.startsWith("image/");
|
|
1349
|
-
var fallbackAttachmentId = 0;
|
|
1350
|
-
function createAttachmentId() {
|
|
1351
|
-
const cryptoSource = globalThis.crypto;
|
|
1352
|
-
if (typeof cryptoSource?.randomUUID === "function") return cryptoSource.randomUUID();
|
|
1353
|
-
fallbackAttachmentId += 1;
|
|
1354
|
-
return `attachment:${Date.now().toString(36)}:${fallbackAttachmentId.toString(36)}`;
|
|
1355
|
-
}
|
|
1356
|
-
function secureContextRequiredErrorCode(error) {
|
|
1357
|
-
return typeof error === "object" && error !== null && "code" in error && error.code === "secure_context_required" ? error.code : void 0;
|
|
1358
|
-
}
|
|
1359
|
-
function useFileAttachments(options = {}) {
|
|
1360
|
-
const { client, workspaceId } = useEmbeddedFileAttachments(options);
|
|
1361
|
-
const pasteFilter = options.pasteFilter ?? isImage;
|
|
1362
|
-
const [attachments, setAttachments] = useState3([]);
|
|
1363
|
-
const sources = useRef3(/* @__PURE__ */ new Map());
|
|
1364
|
-
const previewUrls = useRef3(/* @__PURE__ */ new Map());
|
|
1365
|
-
const previewRetainers = useRef3(/* @__PURE__ */ new Map());
|
|
1366
|
-
const pendingPreviewRevocations = useRef3(/* @__PURE__ */ new Set());
|
|
1367
|
-
const revokePreview = useCallback3((id) => {
|
|
1368
|
-
const previewUrl = previewUrls.current.get(id);
|
|
1369
|
-
if (!previewUrl) return;
|
|
1370
|
-
if ((previewRetainers.current.get(id) ?? 0) > 0) {
|
|
1371
|
-
pendingPreviewRevocations.current.add(id);
|
|
1372
|
-
return;
|
|
1373
|
-
}
|
|
1374
|
-
previewUrls.current.delete(id);
|
|
1375
|
-
pendingPreviewRevocations.current.delete(id);
|
|
1376
|
-
URL.revokeObjectURL(previewUrl);
|
|
1377
|
-
}, []);
|
|
1378
|
-
const revokeAllPreviews = useCallback3(() => {
|
|
1379
|
-
for (const id of previewUrls.current.keys()) revokePreview(id);
|
|
1380
|
-
}, [revokePreview]);
|
|
1381
|
-
const retainPreview = useCallback3(
|
|
1382
|
-
(id) => {
|
|
1383
|
-
if (!previewUrls.current.has(id)) return void 0;
|
|
1384
|
-
previewRetainers.current.set(id, (previewRetainers.current.get(id) ?? 0) + 1);
|
|
1385
|
-
let released = false;
|
|
1386
|
-
return () => {
|
|
1387
|
-
if (released) return;
|
|
1388
|
-
released = true;
|
|
1389
|
-
const remaining = (previewRetainers.current.get(id) ?? 1) - 1;
|
|
1390
|
-
if (remaining > 0) {
|
|
1391
|
-
previewRetainers.current.set(id, remaining);
|
|
1392
|
-
return;
|
|
1393
|
-
}
|
|
1394
|
-
previewRetainers.current.delete(id);
|
|
1395
|
-
if (pendingPreviewRevocations.current.has(id)) revokePreview(id);
|
|
1396
|
-
};
|
|
1397
|
-
},
|
|
1398
|
-
[revokePreview]
|
|
1399
|
-
);
|
|
1400
|
-
const committedAttachmentIds = useRef3(/* @__PURE__ */ new Set());
|
|
1401
|
-
useEffect3(() => {
|
|
1402
|
-
const currentIds = new Set(attachments.map((attachment) => attachment.id));
|
|
1403
|
-
for (const id of committedAttachmentIds.current) {
|
|
1404
|
-
if (!currentIds.has(id)) revokePreview(id);
|
|
1405
|
-
}
|
|
1406
|
-
committedAttachmentIds.current = currentIds;
|
|
1407
|
-
}, [attachments, revokePreview]);
|
|
1408
|
-
const scopeGeneration = useRef3(0);
|
|
1409
|
-
const previousScope = useRef3({ client, workspaceId });
|
|
1410
|
-
useEffect3(() => {
|
|
1411
|
-
const previous = previousScope.current;
|
|
1412
|
-
if (previous.client === client && previous.workspaceId === workspaceId) return;
|
|
1413
|
-
previousScope.current = { client, workspaceId };
|
|
1414
|
-
scopeGeneration.current += 1;
|
|
1415
|
-
sources.current.clear();
|
|
1416
|
-
revokeAllPreviews();
|
|
1417
|
-
setAttachments([]);
|
|
1418
|
-
}, [client, revokeAllPreviews, workspaceId]);
|
|
1419
|
-
useEffect3(
|
|
1420
|
-
() => () => {
|
|
1421
|
-
scopeGeneration.current += 1;
|
|
1422
|
-
sources.current.clear();
|
|
1423
|
-
revokeAllPreviews();
|
|
1424
|
-
},
|
|
1425
|
-
[revokeAllPreviews]
|
|
1426
|
-
);
|
|
1427
|
-
const startUpload = useCallback3(
|
|
1428
|
-
(id, file) => {
|
|
1429
|
-
const generation = scopeGeneration.current;
|
|
1430
|
-
void client.uploadFile(workspaceId, {
|
|
1431
|
-
filename: file.name || "file",
|
|
1432
|
-
contentType: file.type || "application/octet-stream",
|
|
1433
|
-
data: file
|
|
1434
|
-
}).then((asset) => {
|
|
1435
|
-
if (scopeGeneration.current !== generation) return;
|
|
1436
|
-
sources.current.delete(id);
|
|
1437
|
-
setAttachments(
|
|
1438
|
-
(current) => current.map(
|
|
1439
|
-
(attachment) => attachment.id === id ? {
|
|
1440
|
-
...attachment,
|
|
1441
|
-
status: "ready",
|
|
1442
|
-
file: asset,
|
|
1443
|
-
name: asset.filename,
|
|
1444
|
-
contentType: asset.contentType,
|
|
1445
|
-
sizeBytes: asset.sizeBytes,
|
|
1446
|
-
errorCode: void 0,
|
|
1447
|
-
error: void 0
|
|
1448
|
-
} : attachment
|
|
1449
|
-
)
|
|
1450
|
-
);
|
|
1451
|
-
}).catch((error) => {
|
|
1452
|
-
if (scopeGeneration.current !== generation) return;
|
|
1453
|
-
setAttachments(
|
|
1454
|
-
(current) => current.map(
|
|
1455
|
-
(attachment) => attachment.id === id ? {
|
|
1456
|
-
...attachment,
|
|
1457
|
-
status: "failed",
|
|
1458
|
-
errorCode: secureContextRequiredErrorCode(error),
|
|
1459
|
-
error: error instanceof Error ? error.message : String(error)
|
|
1460
|
-
} : attachment
|
|
1461
|
-
)
|
|
1462
|
-
);
|
|
1463
|
-
});
|
|
1464
|
-
},
|
|
1465
|
-
[client, workspaceId]
|
|
1466
|
-
);
|
|
1467
|
-
const addFiles = useCallback3(
|
|
1468
|
-
(files) => {
|
|
1469
|
-
for (const file of files) {
|
|
1470
|
-
const id = createAttachmentId();
|
|
1471
|
-
sources.current.set(id, file);
|
|
1472
|
-
const previewUrl = isImage(file) ? URL.createObjectURL(file) : void 0;
|
|
1473
|
-
if (previewUrl) previewUrls.current.set(id, previewUrl);
|
|
1474
|
-
setAttachments((current) => [
|
|
1475
|
-
...current,
|
|
1476
|
-
{
|
|
1477
|
-
id,
|
|
1478
|
-
name: file.name || "image",
|
|
1479
|
-
contentType: file.type || "application/octet-stream",
|
|
1480
|
-
sizeBytes: file.size,
|
|
1481
|
-
status: "uploading",
|
|
1482
|
-
...previewUrl ? { previewUrl } : {}
|
|
1483
|
-
}
|
|
1484
|
-
]);
|
|
1485
|
-
startUpload(id, file);
|
|
1486
|
-
}
|
|
1487
|
-
},
|
|
1488
|
-
[startUpload]
|
|
1489
|
-
);
|
|
1490
|
-
const retry = useCallback3(
|
|
1491
|
-
(id) => {
|
|
1492
|
-
const file = sources.current.get(id);
|
|
1493
|
-
if (!file) {
|
|
1494
|
-
return;
|
|
1495
|
-
}
|
|
1496
|
-
setAttachments(
|
|
1497
|
-
(current) => current.map(
|
|
1498
|
-
(attachment) => attachment.id === id ? { ...attachment, status: "uploading", errorCode: void 0, error: void 0 } : attachment
|
|
1499
|
-
)
|
|
1500
|
-
);
|
|
1501
|
-
startUpload(id, file);
|
|
1502
|
-
},
|
|
1503
|
-
[startUpload]
|
|
1504
|
-
);
|
|
1505
|
-
const addFromPaste = useCallback3(
|
|
1506
|
-
(event) => {
|
|
1507
|
-
const clipboardFiles = event.clipboardData?.files;
|
|
1508
|
-
if (!clipboardFiles) {
|
|
1509
|
-
return;
|
|
1510
|
-
}
|
|
1511
|
-
const files = [...clipboardFiles].filter(pasteFilter);
|
|
1512
|
-
if (files.length > 0) {
|
|
1513
|
-
addFiles(files);
|
|
1514
|
-
}
|
|
1515
|
-
},
|
|
1516
|
-
[addFiles, pasteFilter]
|
|
1517
|
-
);
|
|
1518
|
-
const restoreReadyFiles = useCallback3(
|
|
1519
|
-
(files) => {
|
|
1520
|
-
const incoming = /* @__PURE__ */ new Map();
|
|
1521
|
-
for (const file of files) {
|
|
1522
|
-
if (file.status === "ready" && file.workspaceId === workspaceId) {
|
|
1523
|
-
incoming.set(file.id, file);
|
|
1524
|
-
}
|
|
1525
|
-
}
|
|
1526
|
-
setAttachments((current) => {
|
|
1527
|
-
const unresolved = current.filter((attachment) => attachment.status !== "ready");
|
|
1528
|
-
const existingReady = new Map(
|
|
1529
|
-
current.flatMap(
|
|
1530
|
-
(attachment) => attachment.status === "ready" && attachment.file ? [[attachment.file.id, attachment]] : []
|
|
1531
|
-
)
|
|
1532
|
-
);
|
|
1533
|
-
const restored = [...incoming.values()].map((file) => {
|
|
1534
|
-
const existing = existingReady.get(file.id);
|
|
1535
|
-
return existing ? {
|
|
1536
|
-
...existing,
|
|
1537
|
-
name: file.filename,
|
|
1538
|
-
contentType: file.contentType,
|
|
1539
|
-
sizeBytes: file.sizeBytes,
|
|
1540
|
-
status: "ready",
|
|
1541
|
-
file,
|
|
1542
|
-
errorCode: void 0,
|
|
1543
|
-
error: void 0
|
|
1544
|
-
} : {
|
|
1545
|
-
id: `restored:${file.id}`,
|
|
1546
|
-
name: file.filename,
|
|
1547
|
-
contentType: file.contentType,
|
|
1548
|
-
sizeBytes: file.sizeBytes,
|
|
1549
|
-
status: "ready",
|
|
1550
|
-
file
|
|
1551
|
-
// No source File and no object URL: server metadata is the
|
|
1552
|
-
// only authority restored across page/device boundaries.
|
|
1553
|
-
};
|
|
1554
|
-
});
|
|
1555
|
-
return [...unresolved, ...restored];
|
|
1556
|
-
});
|
|
1557
|
-
},
|
|
1558
|
-
[workspaceId]
|
|
1559
|
-
);
|
|
1560
|
-
const remove = useCallback3(
|
|
1561
|
-
(id) => {
|
|
1562
|
-
sources.current.delete(id);
|
|
1563
|
-
revokePreview(id);
|
|
1564
|
-
setAttachments((current) => current.filter((attachment) => attachment.id !== id));
|
|
1565
|
-
},
|
|
1566
|
-
[revokePreview]
|
|
1567
|
-
);
|
|
1568
|
-
const removeReadyFiles = useCallback3((fileIds) => {
|
|
1569
|
-
const accepted = new Set(fileIds);
|
|
1570
|
-
if (accepted.size === 0) return;
|
|
1571
|
-
setAttachments(
|
|
1572
|
-
(current) => current.filter((attachment) => {
|
|
1573
|
-
return !(attachment.status === "ready" && attachment.file !== void 0 && accepted.has(attachment.file.id));
|
|
1574
|
-
})
|
|
1575
|
-
);
|
|
1576
|
-
}, []);
|
|
1577
|
-
const clear = useCallback3(() => {
|
|
1578
|
-
sources.current.clear();
|
|
1579
|
-
revokeAllPreviews();
|
|
1580
|
-
setAttachments([]);
|
|
1581
|
-
}, [revokeAllPreviews]);
|
|
1582
|
-
return {
|
|
1583
|
-
attachments,
|
|
1584
|
-
readyResources: attachments.flatMap(
|
|
1585
|
-
(attachment) => attachment.status === "ready" && attachment.file ? [{ kind: "file", fileId: attachment.file.id }] : []
|
|
1586
|
-
),
|
|
1587
|
-
uploading: attachments.some((attachment) => attachment.status === "uploading"),
|
|
1588
|
-
hasUnresolved: attachments.some((attachment) => attachment.status !== "ready"),
|
|
1589
|
-
addFiles,
|
|
1590
|
-
addFromPaste,
|
|
1591
|
-
restoreReadyFiles,
|
|
1592
|
-
retry,
|
|
1593
|
-
retainPreview,
|
|
1594
|
-
remove,
|
|
1595
|
-
removeReadyFiles,
|
|
1596
|
-
clear
|
|
1597
|
-
};
|
|
1598
|
-
}
|
|
1599
|
-
|
|
1600
|
-
// src/hooks/use-turn-queue.ts
|
|
1601
|
-
import { useCallback as useCallback4, useEffect as useEffect4, useLayoutEffect, useMemo as useMemo3, useRef as useRef4, useState as useState4 } from "react";
|
|
1602
|
-
function isTurnQueueEvent(event) {
|
|
1603
|
-
return event.type.startsWith("turn.") || event.type.startsWith("session.queue.") || event.type.startsWith("session.control.") || event.type.startsWith("system.update.") || event.type.startsWith("workspace.inference.");
|
|
1604
|
-
}
|
|
1605
|
-
function queueSnapshotCovers(candidate, observed) {
|
|
1606
|
-
return Boolean(
|
|
1607
|
-
candidate && candidate.version >= observed.version && candidate.effectiveControl.controlVersion >= observed.effectiveControl.controlVersion
|
|
1608
|
-
);
|
|
1609
|
-
}
|
|
1610
|
-
var EMPTY_PENDING_BY_TURN = {};
|
|
1611
|
-
function useTurnQueue(sessionId, options = {}) {
|
|
1612
|
-
const { client, workspaceId, workspaceControlEvent, registerSessionReconciler } = useEmbeddedSession(options);
|
|
1613
|
-
const enabled = (options.enabled ?? true) && Boolean(sessionId);
|
|
1614
|
-
const targetKey = `${workspaceId}\0${sessionId ?? ""}`;
|
|
1615
|
-
const pageLive = usePageLiveActivity();
|
|
1616
|
-
const [snapshot, setSnapshot] = useState4(null);
|
|
1617
|
-
const [stateTargetKey, setStateTargetKey] = useState4(targetKey);
|
|
1618
|
-
const [loading, setLoading] = useState4(enabled);
|
|
1619
|
-
const [error, setError] = useState4(null);
|
|
1620
|
-
const [mutationError, setMutationError] = useState4(null);
|
|
1621
|
-
const [pendingByTurn, setPendingByTurn] = useState4({});
|
|
1622
|
-
const [acceptedSteers, setAcceptedSteers] = useState4([]);
|
|
1623
|
-
const pendingRef = useRef4({});
|
|
1624
|
-
const readGeneration = useRef4(0);
|
|
1625
|
-
const targetKeyRef = useRef4(targetKey);
|
|
1626
|
-
const snapshotRef = useRef4(null);
|
|
1627
|
-
useLayoutEffect(() => {
|
|
1628
|
-
if (targetKeyRef.current === targetKey) return;
|
|
1629
|
-
targetKeyRef.current = targetKey;
|
|
1630
|
-
readGeneration.current += 1;
|
|
1631
|
-
snapshotRef.current = null;
|
|
1632
|
-
pendingRef.current = {};
|
|
1633
|
-
setStateTargetKey(targetKey);
|
|
1634
|
-
setSnapshot(null);
|
|
1635
|
-
setLoading(enabled);
|
|
1636
|
-
setError(null);
|
|
1637
|
-
setMutationError(null);
|
|
1638
|
-
setPendingByTurn({});
|
|
1639
|
-
setAcceptedSteers([]);
|
|
1640
|
-
}, [enabled, targetKey]);
|
|
1641
|
-
useEffect4(() => {
|
|
1642
|
-
if (acceptedSteers.length === 0 || options.events === void 0) return;
|
|
1643
|
-
const durablyStartedTurnIds = new Set(
|
|
1644
|
-
options.events.flatMap(
|
|
1645
|
-
(event) => event.type === "turn.started" && event.turnId ? [event.turnId] : []
|
|
1646
|
-
)
|
|
1647
|
-
);
|
|
1648
|
-
if (durablyStartedTurnIds.size === 0) return;
|
|
1649
|
-
setAcceptedSteers(
|
|
1650
|
-
(current) => current.filter((steer) => !durablyStartedTurnIds.has(steer.turnId))
|
|
1651
|
-
);
|
|
1652
|
-
}, [acceptedSteers.length, options.events]);
|
|
1653
|
-
const acceptSnapshot = useCallback4(
|
|
1654
|
-
(ownedTargetKey, next) => {
|
|
1655
|
-
if (targetKeyRef.current !== ownedTargetKey) return false;
|
|
1656
|
-
const current = snapshotRef.current;
|
|
1657
|
-
if (next && current && (next.version < current.version || next.effectiveControl.controlVersion < current.effectiveControl.controlVersion)) {
|
|
1658
|
-
return false;
|
|
1659
|
-
}
|
|
1660
|
-
snapshotRef.current = next;
|
|
1661
|
-
setStateTargetKey(ownedTargetKey);
|
|
1662
|
-
setSnapshot(next);
|
|
1663
|
-
return true;
|
|
1664
|
-
},
|
|
1665
|
-
[]
|
|
1666
|
-
);
|
|
1667
|
-
const load = useCallback4(
|
|
1668
|
-
async (rejectOnFailure = false) => {
|
|
1669
|
-
if (!sessionId) return;
|
|
1670
|
-
const ownedTargetKey = `${workspaceId}\0${sessionId}`;
|
|
1671
|
-
const ticket = ++readGeneration.current;
|
|
1672
|
-
try {
|
|
1673
|
-
const fetched = await client.getQueue(workspaceId, sessionId);
|
|
1674
|
-
if (targetKeyRef.current !== ownedTargetKey) return;
|
|
1675
|
-
const ownsLatestRead = ticket === readGeneration.current;
|
|
1676
|
-
if (rejectOnFailure) {
|
|
1677
|
-
const committed = acceptSnapshot(ownedTargetKey, fetched);
|
|
1678
|
-
if (!committed && !queueSnapshotCovers(snapshotRef.current, fetched)) {
|
|
1679
|
-
throw new TypeError("Queue reconciliation did not commit authoritative state");
|
|
1680
|
-
}
|
|
1681
|
-
setError(null);
|
|
1682
|
-
if (ownsLatestRead) setLoading(false);
|
|
1683
|
-
} else if (ownsLatestRead) {
|
|
1684
|
-
acceptSnapshot(ownedTargetKey, fetched);
|
|
1685
|
-
setError(null);
|
|
1686
|
-
setLoading(false);
|
|
1687
|
-
}
|
|
1688
|
-
} catch (cause) {
|
|
1689
|
-
if (targetKeyRef.current === ownedTargetKey && (ticket === readGeneration.current || rejectOnFailure)) {
|
|
1690
|
-
setError(asError(cause));
|
|
1691
|
-
if (ticket === readGeneration.current) setLoading(false);
|
|
1692
|
-
}
|
|
1693
|
-
if (rejectOnFailure) throw cause;
|
|
1694
|
-
}
|
|
1695
|
-
},
|
|
1696
|
-
[client, workspaceId, sessionId, acceptSnapshot]
|
|
1697
|
-
);
|
|
1698
|
-
useEffect4(() => {
|
|
1699
|
-
if (!enabled || !pageLive) {
|
|
1700
|
-
setLoading(false);
|
|
1701
|
-
return;
|
|
1702
|
-
}
|
|
1703
|
-
setLoading(true);
|
|
1704
|
-
const pollIntervalMs = options.pollIntervalMs;
|
|
1705
|
-
if (pollIntervalMs === void 0 || pollIntervalMs <= 0) {
|
|
1706
|
-
void load();
|
|
1707
|
-
return () => {
|
|
1708
|
-
readGeneration.current += 1;
|
|
1709
|
-
};
|
|
1710
|
-
}
|
|
1711
|
-
let cancelled = false;
|
|
1712
|
-
let timer = null;
|
|
1713
|
-
const schedule = () => {
|
|
1714
|
-
if (cancelled) return;
|
|
1715
|
-
timer = setTimeout(() => {
|
|
1716
|
-
timer = null;
|
|
1717
|
-
void load().finally(schedule);
|
|
1718
|
-
}, pollIntervalMs);
|
|
1719
|
-
};
|
|
1720
|
-
void load().finally(schedule);
|
|
1721
|
-
return () => {
|
|
1722
|
-
cancelled = true;
|
|
1723
|
-
if (timer !== null) clearTimeout(timer);
|
|
1724
|
-
readGeneration.current += 1;
|
|
1725
|
-
};
|
|
1726
|
-
}, [load, enabled, pageLive, options.pollIntervalMs]);
|
|
1727
|
-
useEffect4(() => {
|
|
1728
|
-
if (enabled && workspaceControlEvent) void load();
|
|
1729
|
-
}, [enabled, load, workspaceControlEvent]);
|
|
1730
|
-
useEffect4(() => {
|
|
1731
|
-
if (!sessionId || !enabled) return;
|
|
1732
|
-
return registerSessionReconciler(sessionId, "queue", load);
|
|
1733
|
-
}, [enabled, load, registerSessionReconciler, sessionId]);
|
|
1734
|
-
const scheduleRefresh = useDebouncedCallback(() => void load());
|
|
1735
|
-
useSessionEventTrigger(
|
|
1736
|
-
client,
|
|
1737
|
-
workspaceId,
|
|
1738
|
-
sessionId,
|
|
1739
|
-
isTurnQueueEvent,
|
|
1740
|
-
scheduleRefresh,
|
|
1741
|
-
{
|
|
1742
|
-
enabled,
|
|
1743
|
-
...options.events !== void 0 ? { events: options.events } : {}
|
|
1744
|
-
},
|
|
1745
|
-
async () => await load(true)
|
|
1746
|
-
);
|
|
1747
|
-
const mutate = useCallback4(
|
|
1748
|
-
async (turnId, kind, command) => {
|
|
1749
|
-
const ownedTargetKey = targetKey;
|
|
1750
|
-
if (!sessionId || targetKeyRef.current !== ownedTargetKey || pendingRef.current[turnId]) {
|
|
1751
|
-
return null;
|
|
1752
|
-
}
|
|
1753
|
-
const current = snapshotRef.current;
|
|
1754
|
-
const turn = current?.items.find((candidate) => candidate.id === turnId);
|
|
1755
|
-
if (!current || !turn) return null;
|
|
1756
|
-
pendingRef.current = { ...pendingRef.current, [turnId]: kind };
|
|
1757
|
-
setStateTargetKey(ownedTargetKey);
|
|
1758
|
-
setPendingByTurn(pendingRef.current);
|
|
1759
|
-
setMutationError(null);
|
|
1760
|
-
const clientEventId = operationKey();
|
|
1761
|
-
try {
|
|
1762
|
-
let result;
|
|
1763
|
-
try {
|
|
1764
|
-
result = await command(current, turn, clientEventId);
|
|
1765
|
-
} catch (cause) {
|
|
1766
|
-
if (!isOutcomeUnknownError(cause)) throw cause;
|
|
1767
|
-
result = await command(current, turn, clientEventId);
|
|
1768
|
-
}
|
|
1769
|
-
if (targetKeyRef.current !== ownedTargetKey) return null;
|
|
1770
|
-
acceptSnapshot(ownedTargetKey, result.snapshot);
|
|
1771
|
-
return result;
|
|
1772
|
-
} catch (cause) {
|
|
1773
|
-
if (targetKeyRef.current === ownedTargetKey) {
|
|
1774
|
-
setMutationError(asError(cause));
|
|
1775
|
-
await load();
|
|
1776
|
-
}
|
|
1777
|
-
return null;
|
|
1778
|
-
} finally {
|
|
1779
|
-
if (targetKeyRef.current === ownedTargetKey && turnId in pendingRef.current) {
|
|
1780
|
-
const next = { ...pendingRef.current };
|
|
1781
|
-
delete next[turnId];
|
|
1782
|
-
pendingRef.current = next;
|
|
1783
|
-
setPendingByTurn(next);
|
|
1784
|
-
}
|
|
1785
|
-
}
|
|
1786
|
-
},
|
|
1787
|
-
[acceptSnapshot, load, sessionId, targetKey]
|
|
1788
|
-
);
|
|
1789
|
-
const moveTurn = useCallback4(
|
|
1790
|
-
async (turnId, beforeTurnId) => {
|
|
1791
|
-
const result = await mutate(
|
|
1792
|
-
turnId,
|
|
1793
|
-
"move",
|
|
1794
|
-
(current, _turn, clientEventId) => client.moveQueueItem(workspaceId, sessionId, turnId, {
|
|
1795
|
-
clientEventId,
|
|
1796
|
-
expectedQueueVersion: current.version,
|
|
1797
|
-
beforeTurnId
|
|
1798
|
-
})
|
|
1799
|
-
);
|
|
1800
|
-
return result !== null;
|
|
1801
|
-
},
|
|
1802
|
-
[client, mutate, sessionId, workspaceId]
|
|
1803
|
-
);
|
|
1804
|
-
const editTurn = useCallback4(
|
|
1805
|
-
async (turnId, edit) => {
|
|
1806
|
-
const result = await mutate(
|
|
1807
|
-
turnId,
|
|
1808
|
-
"edit",
|
|
1809
|
-
(_current, turn, clientEventId) => client.editQueueItem(workspaceId, sessionId, turnId, {
|
|
1810
|
-
clientEventId,
|
|
1811
|
-
expectedTurnVersion: turn.version,
|
|
1812
|
-
expectedDraftRevision: edit.expectedDraftRevision,
|
|
1813
|
-
replaceDraft: edit.replaceDraft
|
|
1814
|
-
})
|
|
1815
|
-
);
|
|
1816
|
-
return result?.draft ?? null;
|
|
1817
|
-
},
|
|
1818
|
-
[client, mutate, sessionId, workspaceId]
|
|
1819
|
-
);
|
|
1820
|
-
const steerTurn = useCallback4(
|
|
1821
|
-
async (turnId) => {
|
|
1822
|
-
const ownedTargetKey = targetKey;
|
|
1823
|
-
const turn = snapshotRef.current?.items.find((candidate) => candidate.id === turnId);
|
|
1824
|
-
if (targetKeyRef.current !== ownedTargetKey) return false;
|
|
1825
|
-
if (!turn) {
|
|
1826
|
-
setMutationError(new Error("That queued prompt changed before Steer could be sent."));
|
|
1827
|
-
await load();
|
|
1828
|
-
return false;
|
|
1829
|
-
}
|
|
1830
|
-
const optimistic = {
|
|
1831
|
-
turnId: turn.id,
|
|
1832
|
-
triggerEventId: turn.triggerEventId,
|
|
1833
|
-
text: turn.prompt,
|
|
1834
|
-
annotations: turn.annotations ?? [],
|
|
1835
|
-
resources: turn.resources,
|
|
1836
|
-
tools: turn.tools,
|
|
1837
|
-
occurredAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1838
|
-
state: "sending"
|
|
1839
|
-
};
|
|
1840
|
-
setAcceptedSteers((current) => [
|
|
1841
|
-
...current.filter((steer) => steer.turnId !== turnId),
|
|
1842
|
-
optimistic
|
|
1843
|
-
]);
|
|
1844
|
-
const result = await mutate(
|
|
1845
|
-
turnId,
|
|
1846
|
-
"steer",
|
|
1847
|
-
(current, queuedTurn, clientEventId) => client.steerQueueItem(workspaceId, sessionId, turnId, {
|
|
1848
|
-
clientEventId,
|
|
1849
|
-
expectedTurnVersion: queuedTurn.version,
|
|
1850
|
-
controlEtag: current.effectiveControl.controlEtag
|
|
1851
|
-
})
|
|
1852
|
-
);
|
|
1853
|
-
if (targetKeyRef.current !== ownedTargetKey) return false;
|
|
1854
|
-
const advancedWithoutSteer = result === null && snapshotRef.current !== null && !snapshotRef.current.items.some((candidate) => candidate.id === turnId);
|
|
1855
|
-
if (advancedWithoutSteer) {
|
|
1856
|
-
setMutationError(null);
|
|
1857
|
-
setAcceptedSteers((current) => current.filter((steer) => steer.turnId !== turnId));
|
|
1858
|
-
return true;
|
|
1859
|
-
}
|
|
1860
|
-
const accepted = result !== null || snapshotRef.current?.items.some(
|
|
1861
|
-
(candidate) => candidate.id === turnId && candidate.metadata.delivery === "steer"
|
|
1862
|
-
) === true;
|
|
1863
|
-
setAcceptedSteers(
|
|
1864
|
-
(current) => accepted ? current.map(
|
|
1865
|
-
(steer) => steer.turnId === turnId ? { ...steer, state: "queued" } : steer
|
|
1866
|
-
) : current.filter((steer) => steer.turnId !== turnId)
|
|
1867
|
-
);
|
|
1868
|
-
return accepted;
|
|
1869
|
-
},
|
|
1870
|
-
[client, load, mutate, sessionId, targetKey, workspaceId]
|
|
1871
|
-
);
|
|
1872
|
-
const removeTurn = useCallback4(
|
|
1873
|
-
async (turnId) => {
|
|
1874
|
-
const result = await mutate(
|
|
1875
|
-
turnId,
|
|
1876
|
-
"delete",
|
|
1877
|
-
(_current, turn, clientEventId) => client.deleteQueueItem(workspaceId, sessionId, turnId, {
|
|
1878
|
-
clientEventId,
|
|
1879
|
-
expectedTurnVersion: turn.version,
|
|
1880
|
-
reason: "Deleted from the prompt queue"
|
|
1881
|
-
})
|
|
1882
|
-
);
|
|
1883
|
-
return result !== null;
|
|
1884
|
-
},
|
|
1885
|
-
[client, mutate, sessionId, workspaceId]
|
|
1886
|
-
);
|
|
1887
|
-
const identityMatches = stateTargetKey === targetKey;
|
|
1888
|
-
const visibleSnapshot = identityMatches ? snapshot : null;
|
|
1889
|
-
const visiblePendingByTurn = identityMatches ? pendingByTurn : EMPTY_PENDING_BY_TURN;
|
|
1890
|
-
const mutating = useMemo3(
|
|
1891
|
-
() => Object.keys(visiblePendingByTurn).length > 0,
|
|
1892
|
-
[visiblePendingByTurn]
|
|
1893
|
-
);
|
|
1894
|
-
const mutationFor = useCallback4(
|
|
1895
|
-
(turnId) => visiblePendingByTurn[turnId] ?? null,
|
|
1896
|
-
[visiblePendingByTurn]
|
|
1897
|
-
);
|
|
1898
|
-
return {
|
|
1899
|
-
snapshot: visibleSnapshot,
|
|
1900
|
-
queue: visibleSnapshot?.items ?? [],
|
|
1901
|
-
pendingInputs: visibleSnapshot?.pendingInputs ?? [],
|
|
1902
|
-
pendingInputAttachment: visibleSnapshot?.pendingInputAttachment ?? null,
|
|
1903
|
-
activePersonalConnections: visibleSnapshot?.activePersonalConnections ?? [],
|
|
1904
|
-
effectiveControl: visibleSnapshot?.effectiveControl ?? null,
|
|
1905
|
-
stoppingPreviousAttempt: visibleSnapshot?.stoppingPreviousAttempt ?? false,
|
|
1906
|
-
loading: identityMatches ? loading : enabled,
|
|
1907
|
-
error: identityMatches ? error : null,
|
|
1908
|
-
refresh: load,
|
|
1909
|
-
moveTurn,
|
|
1910
|
-
editTurn,
|
|
1911
|
-
steerTurn,
|
|
1912
|
-
removeTurn,
|
|
1913
|
-
pendingByTurn: visiblePendingByTurn,
|
|
1914
|
-
mutationFor,
|
|
1915
|
-
mutating,
|
|
1916
|
-
mutationError: identityMatches ? mutationError : null,
|
|
1917
|
-
clearMutationError: useCallback4(() => {
|
|
1918
|
-
if (targetKeyRef.current === targetKey) setMutationError(null);
|
|
1919
|
-
}, [targetKey]),
|
|
1920
|
-
acceptedSteers: identityMatches ? acceptedSteers : []
|
|
1921
|
-
};
|
|
1922
|
-
}
|
|
1923
|
-
function operationKey() {
|
|
1924
|
-
return globalThis.crypto.randomUUID();
|
|
1925
|
-
}
|
|
1926
|
-
function asError(cause) {
|
|
1927
|
-
return cause instanceof Error ? cause : new Error(String(cause));
|
|
1928
|
-
}
|
|
1929
|
-
function isOutcomeUnknownError(cause) {
|
|
1930
|
-
return typeof cause === "object" && cause !== null && cause.outcomeUnknown === true;
|
|
1931
|
-
}
|
|
1932
|
-
|
|
1933
|
-
// src/hooks/use-goal.ts
|
|
1934
|
-
import { OpenGeniApiError } from "@opengeni/sdk";
|
|
1935
|
-
import { useCallback as useCallback5, useEffect as useEffect5, useRef as useRef5, useState as useState5 } from "react";
|
|
1936
|
-
function isGoalEvent(event) {
|
|
1937
|
-
return event.type.startsWith("goal.");
|
|
1938
|
-
}
|
|
1939
|
-
function isGoalRefreshEvent(event) {
|
|
1940
|
-
const type = event.type;
|
|
1941
|
-
return isGoalEvent(event) || type.startsWith("turn.") || type.startsWith("session.") || type.startsWith("system.update.") || type.startsWith("workspace.inference.") || type.startsWith("user.");
|
|
1942
|
-
}
|
|
1943
|
-
function useGoal(sessionId, options = {}) {
|
|
1944
|
-
const { client, workspaceId } = useEmbeddedGoal(options);
|
|
1945
|
-
const enabled = (options.enabled ?? true) && Boolean(sessionId);
|
|
1946
|
-
const sharedEvents = options.events;
|
|
1947
|
-
const sharedFeed = sharedEvents !== void 0;
|
|
1948
|
-
const pageLive = usePageLiveActivity();
|
|
1949
|
-
const [goal, setGoal] = useState5(null);
|
|
1950
|
-
const [loading, setLoading] = useState5(enabled);
|
|
1951
|
-
const [error, setError] = useState5(null);
|
|
1952
|
-
const { run, mutating, mutationError, clearMutationError } = useMutationRunner();
|
|
1953
|
-
const generation = useRef5(0);
|
|
1954
|
-
const targetKeyRef = useRef5(null);
|
|
1955
|
-
const loadAbort = useRef5(null);
|
|
1956
|
-
const retireLoads = useCallback5(() => {
|
|
1957
|
-
generation.current += 1;
|
|
1958
|
-
loadAbort.current?.abort();
|
|
1959
|
-
loadAbort.current = null;
|
|
1960
|
-
}, []);
|
|
1961
|
-
const load = useCallback5(async () => {
|
|
1962
|
-
if (!sessionId) {
|
|
1963
|
-
return;
|
|
1964
|
-
}
|
|
1965
|
-
const ticket = ++generation.current;
|
|
1966
|
-
loadAbort.current?.abort();
|
|
1967
|
-
const controller = new AbortController();
|
|
1968
|
-
loadAbort.current = controller;
|
|
1969
|
-
try {
|
|
1970
|
-
const fetched = await client.getGoal(workspaceId, sessionId, {
|
|
1971
|
-
signal: controller.signal
|
|
1972
|
-
});
|
|
1973
|
-
if (ticket === generation.current) {
|
|
1974
|
-
setGoal(fetched);
|
|
1975
|
-
setError(null);
|
|
1976
|
-
setLoading(false);
|
|
1977
|
-
}
|
|
1978
|
-
} catch (cause) {
|
|
1979
|
-
if (ticket !== generation.current) {
|
|
1980
|
-
return;
|
|
1981
|
-
}
|
|
1982
|
-
if (cause instanceof OpenGeniApiError && cause.status === 404) {
|
|
1983
|
-
setGoal(null);
|
|
1984
|
-
setError(null);
|
|
1985
|
-
} else {
|
|
1986
|
-
setError(cause instanceof Error ? cause : new Error(String(cause)));
|
|
1987
|
-
}
|
|
1988
|
-
setLoading(false);
|
|
1989
|
-
} finally {
|
|
1990
|
-
if (loadAbort.current === controller) loadAbort.current = null;
|
|
1991
|
-
}
|
|
1992
|
-
}, [client, workspaceId, sessionId]);
|
|
1993
|
-
useEffect5(() => {
|
|
1994
|
-
const targetKey = `${workspaceId} ${sessionId ?? ""}`;
|
|
1995
|
-
if (targetKeyRef.current !== targetKey) {
|
|
1996
|
-
targetKeyRef.current = targetKey;
|
|
1997
|
-
setGoal(null);
|
|
1998
|
-
setError(null);
|
|
1999
|
-
}
|
|
2000
|
-
if (!enabled || !pageLive) {
|
|
2001
|
-
retireLoads();
|
|
2002
|
-
setLoading(false);
|
|
2003
|
-
return;
|
|
2004
|
-
}
|
|
2005
|
-
if (sharedFeed) {
|
|
2006
|
-
setLoading(false);
|
|
2007
|
-
return () => {
|
|
2008
|
-
retireLoads();
|
|
2009
|
-
};
|
|
2010
|
-
}
|
|
2011
|
-
setLoading(true);
|
|
2012
|
-
const pollIntervalMs = options.pollIntervalMs;
|
|
2013
|
-
if (pollIntervalMs === void 0 || pollIntervalMs <= 0) {
|
|
2014
|
-
void load();
|
|
2015
|
-
return () => {
|
|
2016
|
-
retireLoads();
|
|
2017
|
-
};
|
|
2018
|
-
}
|
|
2019
|
-
let cancelled = false;
|
|
2020
|
-
let timer = null;
|
|
2021
|
-
const schedule = () => {
|
|
2022
|
-
if (cancelled) return;
|
|
2023
|
-
timer = setTimeout(() => {
|
|
2024
|
-
timer = null;
|
|
2025
|
-
void load().finally(schedule);
|
|
2026
|
-
}, pollIntervalMs);
|
|
2027
|
-
};
|
|
2028
|
-
void load().finally(schedule);
|
|
2029
|
-
return () => {
|
|
2030
|
-
cancelled = true;
|
|
2031
|
-
if (timer !== null) clearTimeout(timer);
|
|
2032
|
-
retireLoads();
|
|
2033
|
-
};
|
|
2034
|
-
}, [
|
|
2035
|
-
load,
|
|
2036
|
-
enabled,
|
|
2037
|
-
pageLive,
|
|
2038
|
-
workspaceId,
|
|
2039
|
-
sessionId,
|
|
2040
|
-
options.pollIntervalMs,
|
|
2041
|
-
retireLoads,
|
|
2042
|
-
sharedFeed
|
|
2043
|
-
]);
|
|
2044
|
-
const scheduleRefresh = useDebouncedCallback(() => void load());
|
|
2045
|
-
useSessionEventTrigger(client, workspaceId, sessionId, isGoalRefreshEvent, scheduleRefresh, {
|
|
2046
|
-
enabled,
|
|
2047
|
-
...sharedEvents !== void 0 ? { events: sharedEvents } : {}
|
|
2048
|
-
});
|
|
2049
|
-
const pause = useCallback5(
|
|
2050
|
-
async (rationale) => {
|
|
2051
|
-
if (!sessionId) {
|
|
2052
|
-
return null;
|
|
2053
|
-
}
|
|
2054
|
-
const result = await run(
|
|
2055
|
-
() => client.updateGoal(workspaceId, sessionId, {
|
|
2056
|
-
status: "paused",
|
|
2057
|
-
...rationale !== void 0 ? { rationale } : {}
|
|
2058
|
-
})
|
|
2059
|
-
);
|
|
2060
|
-
if (result) {
|
|
2061
|
-
setGoal(result);
|
|
2062
|
-
}
|
|
2063
|
-
return result;
|
|
2064
|
-
},
|
|
2065
|
-
[client, workspaceId, sessionId, run]
|
|
2066
|
-
);
|
|
2067
|
-
const resume = useCallback5(async () => {
|
|
2068
|
-
if (!sessionId) {
|
|
2069
|
-
return null;
|
|
2070
|
-
}
|
|
2071
|
-
const result = await run(() => client.updateGoal(workspaceId, sessionId, { status: "active" }));
|
|
2072
|
-
if (result) {
|
|
2073
|
-
setGoal(result);
|
|
2074
|
-
}
|
|
2075
|
-
return result;
|
|
2076
|
-
}, [client, workspaceId, sessionId, run]);
|
|
2077
|
-
const clearGoal = useCallback5(async () => {
|
|
2078
|
-
if (!sessionId) {
|
|
2079
|
-
return;
|
|
2080
|
-
}
|
|
2081
|
-
const ok = await run(async () => {
|
|
2082
|
-
await client.deleteGoal(workspaceId, sessionId);
|
|
2083
|
-
return true;
|
|
2084
|
-
});
|
|
2085
|
-
if (ok) {
|
|
2086
|
-
retireLoads();
|
|
2087
|
-
setGoal(null);
|
|
2088
|
-
setError(null);
|
|
2089
|
-
}
|
|
2090
|
-
}, [client, workspaceId, sessionId, run, retireLoads]);
|
|
2091
|
-
return {
|
|
2092
|
-
goal,
|
|
2093
|
-
isActive: goal?.status === "active",
|
|
2094
|
-
isPaused: goal?.status === "paused",
|
|
2095
|
-
isCompleted: goal?.status === "completed",
|
|
2096
|
-
loading,
|
|
2097
|
-
error,
|
|
2098
|
-
refresh: load,
|
|
2099
|
-
pause,
|
|
2100
|
-
resume,
|
|
2101
|
-
clearGoal,
|
|
2102
|
-
deleteGoal: clearGoal,
|
|
2103
|
-
updating: mutating,
|
|
2104
|
-
mutationError,
|
|
2105
|
-
clearMutationError
|
|
2106
|
-
};
|
|
2107
|
-
}
|
|
2108
|
-
|
|
2109
|
-
// src/hooks/use-session-control.ts
|
|
2110
|
-
import { useCallback as useCallback6, useLayoutEffect as useLayoutEffect2, useRef as useRef6 } from "react";
|
|
2111
|
-
function useSessionControl(sessionId, options = {}) {
|
|
2112
|
-
const { client, workspaceId } = useEmbeddedSession(options);
|
|
2113
|
-
const approvalTargetKey = `${workspaceId}\0${sessionId ?? ""}`;
|
|
2114
|
-
const pendingApproval = useRef6(null);
|
|
2115
|
-
useLayoutEffect2(() => {
|
|
2116
|
-
if (pendingApproval.current?.targetKey !== approvalTargetKey) {
|
|
2117
|
-
pendingApproval.current = null;
|
|
2118
|
-
}
|
|
2119
|
-
}, [approvalTargetKey]);
|
|
2120
|
-
const {
|
|
2121
|
-
run: runControl,
|
|
2122
|
-
mutating: controlling,
|
|
2123
|
-
mutationError: controlError,
|
|
2124
|
-
clearMutationError: clearControlError
|
|
2125
|
-
} = useMutationRunner(approvalTargetKey);
|
|
2126
|
-
const {
|
|
2127
|
-
run: runApproval,
|
|
2128
|
-
mutating: responding,
|
|
2129
|
-
mutationError: approvalError,
|
|
2130
|
-
clearMutationError: clearApprovalError
|
|
2131
|
-
} = useMutationRunner(approvalTargetKey);
|
|
2132
|
-
const pause = useCallback6(
|
|
2133
|
-
async (reason) => {
|
|
2134
|
-
if (!sessionId) {
|
|
2135
|
-
return null;
|
|
2136
|
-
}
|
|
2137
|
-
return await runControl(
|
|
2138
|
-
() => client.pauseSession(workspaceId, sessionId, reason !== void 0 ? { reason } : {})
|
|
2139
|
-
);
|
|
2140
|
-
},
|
|
2141
|
-
[client, workspaceId, sessionId, runControl]
|
|
2142
|
-
);
|
|
2143
|
-
const resume = useCallback6(
|
|
2144
|
-
async (reason) => {
|
|
2145
|
-
if (!sessionId) return null;
|
|
2146
|
-
return await runControl(
|
|
2147
|
-
() => client.resumeSession(workspaceId, sessionId, reason !== void 0 ? { reason } : {})
|
|
2148
|
-
);
|
|
2149
|
-
},
|
|
2150
|
-
[client, workspaceId, sessionId, runControl]
|
|
2151
|
-
);
|
|
2152
|
-
const decide = useCallback6(
|
|
2153
|
-
async (approvalId, decision, message) => {
|
|
2154
|
-
if (!sessionId) {
|
|
2155
|
-
return null;
|
|
2156
|
-
}
|
|
2157
|
-
const decisionKey = JSON.stringify([approvalId, decision, message ?? null]);
|
|
2158
|
-
if (pendingApproval.current?.decisionKey !== decisionKey) {
|
|
2159
|
-
pendingApproval.current = {
|
|
2160
|
-
targetKey: approvalTargetKey,
|
|
2161
|
-
decisionKey,
|
|
2162
|
-
clientEventId: crypto.randomUUID()
|
|
2163
|
-
};
|
|
2164
|
-
}
|
|
2165
|
-
const clientEventId = pendingApproval.current.clientEventId;
|
|
2166
|
-
const accepted = await runApproval(
|
|
2167
|
-
() => client.sendApprovalDecision(workspaceId, sessionId, {
|
|
2168
|
-
approvalId,
|
|
2169
|
-
decision,
|
|
2170
|
-
...message !== void 0 ? { message } : {},
|
|
2171
|
-
clientEventId
|
|
2172
|
-
})
|
|
2173
|
-
);
|
|
2174
|
-
if (accepted !== null && pendingApproval.current?.targetKey === approvalTargetKey && pendingApproval.current.clientEventId === clientEventId) {
|
|
2175
|
-
pendingApproval.current = null;
|
|
2176
|
-
}
|
|
2177
|
-
return accepted;
|
|
2178
|
-
},
|
|
2179
|
-
[approvalTargetKey, client, workspaceId, sessionId, runApproval]
|
|
2180
|
-
);
|
|
2181
|
-
const approve = useCallback6(
|
|
2182
|
-
async (approvalId, message) => await decide(approvalId, "approve", message),
|
|
2183
|
-
[decide]
|
|
2184
|
-
);
|
|
2185
|
-
const reject = useCallback6(
|
|
2186
|
-
async (approvalId, message) => await decide(approvalId, "reject", message),
|
|
2187
|
-
[decide]
|
|
2188
|
-
);
|
|
2189
|
-
const error = approvalError ?? controlError;
|
|
2190
|
-
const clearError = useCallback6(() => {
|
|
2191
|
-
clearControlError();
|
|
2192
|
-
clearApprovalError();
|
|
2193
|
-
}, [clearControlError, clearApprovalError]);
|
|
2194
|
-
return {
|
|
2195
|
-
pause,
|
|
2196
|
-
resume,
|
|
2197
|
-
controlling,
|
|
2198
|
-
approve,
|
|
2199
|
-
reject,
|
|
2200
|
-
responding,
|
|
2201
|
-
error,
|
|
2202
|
-
clearError
|
|
2203
|
-
};
|
|
2204
|
-
}
|
|
2205
|
-
|
|
2206
|
-
// src/hooks/use-session-mcp-approval-policy.ts
|
|
2207
|
-
import { useCallback as useCallback7, useEffect as useEffect6, useLayoutEffect as useLayoutEffect3, useMemo as useMemo4, useRef as useRef7, useState as useState6 } from "react";
|
|
2208
|
-
function isSessionMcpApprovalPolicyEvent(event, serverId) {
|
|
2209
|
-
if (event.type !== "session.mcp.approval_policy.updated") {
|
|
2210
|
-
return false;
|
|
2211
|
-
}
|
|
2212
|
-
if (serverId === void 0) {
|
|
2213
|
-
return true;
|
|
2214
|
-
}
|
|
2215
|
-
const payload = event.payload;
|
|
2216
|
-
return payload?.serverId === serverId;
|
|
2217
|
-
}
|
|
2218
|
-
function useSessionMcpApprovalPolicy(sessionId, serverId, options = {}) {
|
|
2219
|
-
const { client, workspaceId } = useEmbeddedSessionMcpApprovalPolicy(options);
|
|
2220
|
-
const enabled = (options.enabled ?? true) && Boolean(sessionId && serverId);
|
|
2221
|
-
const [override, setOverride] = useState6(null);
|
|
2222
|
-
const targetKey = `${workspaceId}\0${sessionId ?? ""}\0${serverId ?? ""}`;
|
|
2223
|
-
const authoritativeGeneration = useRef7(0);
|
|
2224
|
-
const mutationIdentity = useMemo4(() => ({ client, targetKey }), [client, targetKey]);
|
|
2225
|
-
const { run, mutating, mutationError, clearMutationError } = useMutationRunner(mutationIdentity);
|
|
2226
|
-
useLayoutEffect3(() => {
|
|
2227
|
-
authoritativeGeneration.current += 1;
|
|
2228
|
-
setOverride(null);
|
|
2229
|
-
}, [client, targetKey]);
|
|
2230
|
-
const load = useCallback7(async () => {
|
|
2231
|
-
const generation = authoritativeGeneration.current;
|
|
2232
|
-
if (!sessionId || !serverId) {
|
|
2233
|
-
return { targetKey, generation, server: null };
|
|
2234
|
-
}
|
|
2235
|
-
const session = await client.getSession(workspaceId, sessionId);
|
|
2236
|
-
return {
|
|
2237
|
-
targetKey,
|
|
2238
|
-
generation,
|
|
2239
|
-
server: session.mcpServers.find((server2) => server2.id === serverId) ?? null
|
|
2240
|
-
};
|
|
2241
|
-
}, [client, serverId, sessionId, targetKey, workspaceId]);
|
|
2242
|
-
const {
|
|
2243
|
-
data,
|
|
2244
|
-
loading,
|
|
2245
|
-
error: loadError,
|
|
2246
|
-
refresh
|
|
2247
|
-
} = usePolledValue(load, {
|
|
2248
|
-
enabled,
|
|
2249
|
-
pollIntervalMs: options.pollIntervalMs
|
|
2250
|
-
});
|
|
2251
|
-
useEffect6(() => {
|
|
2252
|
-
if (data?.targetKey === targetKey && data.generation === authoritativeGeneration.current) {
|
|
2253
|
-
setOverride(null);
|
|
2254
|
-
}
|
|
2255
|
-
}, [data, targetKey]);
|
|
2256
|
-
const scheduleRefresh = useDebouncedCallback(() => void refresh());
|
|
2257
|
-
const matchesPolicyEvent = useCallback7(
|
|
2258
|
-
(event) => isSessionMcpApprovalPolicyEvent(event, serverId ?? void 0),
|
|
2259
|
-
[serverId]
|
|
2260
|
-
);
|
|
2261
|
-
useSessionEventTrigger(
|
|
2262
|
-
client,
|
|
2263
|
-
workspaceId,
|
|
2264
|
-
sessionId,
|
|
2265
|
-
matchesPolicyEvent,
|
|
2266
|
-
scheduleRefresh,
|
|
2267
|
-
{
|
|
2268
|
-
enabled,
|
|
2269
|
-
...options.events !== void 0 ? { events: options.events } : {}
|
|
2270
|
-
},
|
|
2271
|
-
async () => await refresh()
|
|
2272
|
-
);
|
|
2273
|
-
const update = useCallback7(
|
|
2274
|
-
async (policy) => {
|
|
2275
|
-
if (!sessionId || !serverId) {
|
|
2276
|
-
return null;
|
|
2277
|
-
}
|
|
2278
|
-
const response = await run(
|
|
2279
|
-
() => client.updateSessionMcpApprovalPolicy(workspaceId, sessionId, serverId, {
|
|
2280
|
-
requireApproval: policy
|
|
2281
|
-
})
|
|
2282
|
-
);
|
|
2283
|
-
if (response) {
|
|
2284
|
-
authoritativeGeneration.current += 1;
|
|
2285
|
-
setOverride(response.server);
|
|
2286
|
-
void refresh();
|
|
2287
|
-
}
|
|
2288
|
-
return response;
|
|
2289
|
-
},
|
|
2290
|
-
[client, refresh, run, serverId, sessionId, workspaceId]
|
|
2291
|
-
);
|
|
2292
|
-
const server = override ?? data?.server ?? null;
|
|
2293
|
-
return {
|
|
2294
|
-
server,
|
|
2295
|
-
policy: server?.requireApproval ?? null,
|
|
2296
|
-
loading,
|
|
2297
|
-
error: mutationError ?? loadError,
|
|
2298
|
-
refresh: async () => {
|
|
2299
|
-
await refresh();
|
|
2300
|
-
},
|
|
2301
|
-
update,
|
|
2302
|
-
updating: mutating,
|
|
2303
|
-
clearError: clearMutationError
|
|
2304
|
-
};
|
|
2305
|
-
}
|
|
2306
|
-
|
|
2307
|
-
// src/hooks/use-session-lineage.ts
|
|
2308
|
-
import { useCallback as useCallback8, useEffect as useEffect7, useRef as useRef8 } from "react";
|
|
2309
|
-
function isLineageRefreshEvent(event) {
|
|
2310
|
-
if (event.type === "session.status.changed" || event.type === "session.created") {
|
|
2311
|
-
return true;
|
|
2312
|
-
}
|
|
2313
|
-
if (event.type === "agent.toolCall.output") {
|
|
2314
|
-
const payload = event.payload;
|
|
2315
|
-
const name = typeof payload?.name === "string" ? payload.name : typeof payload?.toolName === "string" ? payload.toolName : "";
|
|
2316
|
-
return name === "session_create" || name.endsWith("__session_create");
|
|
2317
|
-
}
|
|
2318
|
-
return false;
|
|
2319
|
-
}
|
|
2320
|
-
function isSessionCreateToolCallCreated(event) {
|
|
2321
|
-
if (event.type !== "agent.toolCall.created") {
|
|
2322
|
-
return false;
|
|
2323
|
-
}
|
|
2324
|
-
const payload = event.payload;
|
|
2325
|
-
const name = typeof payload?.name === "string" ? payload.name : typeof payload?.toolName === "string" ? payload.toolName : "";
|
|
2326
|
-
return name === "session_create" || name.endsWith("__session_create");
|
|
2327
|
-
}
|
|
2328
|
-
function useSessionLineage(sessionId, options = {}) {
|
|
2329
|
-
const { client, workspaceId, workspaceControlEvent, registerSessionReconciler } = useEmbeddedSessionLineage(options);
|
|
2330
|
-
const enabled = (options.enabled ?? true) && Boolean(sessionId);
|
|
2331
|
-
const nextReadGeneration = useRef8(0);
|
|
2332
|
-
const beginRead = options.beginRead;
|
|
2333
|
-
const load = useCallback8(
|
|
2334
|
-
async (signal) => {
|
|
2335
|
-
if (!sessionId) {
|
|
2336
|
-
return {
|
|
2337
|
-
lineage: { ancestors: [], children: [], truncated: false },
|
|
2338
|
-
readGeneration: 0
|
|
2339
|
-
};
|
|
2340
|
-
}
|
|
2341
|
-
let readGeneration = 0;
|
|
2342
|
-
const lineage = await client.getSessionLineage(workspaceId, sessionId, {
|
|
2343
|
-
signal,
|
|
2344
|
-
onRequestStart: (sharedReadGeneration) => {
|
|
2345
|
-
readGeneration = sharedReadGeneration ?? beginRead?.() ?? ++nextReadGeneration.current;
|
|
2346
|
-
}
|
|
2347
|
-
});
|
|
2348
|
-
return {
|
|
2349
|
-
lineage,
|
|
2350
|
-
readGeneration
|
|
2351
|
-
};
|
|
2352
|
-
},
|
|
2353
|
-
[beginRead, client, workspaceId, sessionId]
|
|
2354
|
-
);
|
|
2355
|
-
const state = usePolledValue(load, { pollIntervalMs: options.pollIntervalMs, enabled });
|
|
2356
|
-
const refresh = state.refresh;
|
|
2357
|
-
useEffect7(() => {
|
|
2358
|
-
if (enabled && workspaceControlEvent) void refresh();
|
|
2359
|
-
}, [enabled, refresh, workspaceControlEvent]);
|
|
2360
|
-
useEffect7(() => {
|
|
2361
|
-
if (!sessionId || !enabled) return;
|
|
2362
|
-
return registerSessionReconciler(sessionId, "lineage", refresh);
|
|
2363
|
-
}, [enabled, refresh, registerSessionReconciler, sessionId]);
|
|
2364
|
-
const refreshSoon = useDebouncedCallback(() => void refresh(), 150);
|
|
2365
|
-
const delayedChildRefreshRef = useRef8(null);
|
|
2366
|
-
useEffect7(() => {
|
|
2367
|
-
return () => {
|
|
2368
|
-
if (delayedChildRefreshRef.current !== null) {
|
|
2369
|
-
clearTimeout(delayedChildRefreshRef.current);
|
|
2370
|
-
delayedChildRefreshRef.current = null;
|
|
2371
|
-
}
|
|
2372
|
-
};
|
|
2373
|
-
}, [sessionId, workspaceId]);
|
|
2374
|
-
const refreshAfterChildCreate = useCallback8(() => {
|
|
2375
|
-
void refresh();
|
|
2376
|
-
if (delayedChildRefreshRef.current !== null) {
|
|
2377
|
-
clearTimeout(delayedChildRefreshRef.current);
|
|
2378
|
-
}
|
|
2379
|
-
delayedChildRefreshRef.current = setTimeout(() => {
|
|
2380
|
-
delayedChildRefreshRef.current = null;
|
|
2381
|
-
void refresh();
|
|
2382
|
-
}, 2500);
|
|
2383
|
-
}, [refresh]);
|
|
2384
|
-
useSessionEventTrigger(
|
|
2385
|
-
client,
|
|
2386
|
-
workspaceId,
|
|
2387
|
-
sessionId,
|
|
2388
|
-
isLineageRefreshEvent,
|
|
2389
|
-
refreshSoon,
|
|
2390
|
-
// SHARED-FEED ONLY: without a caller-provided events log the trigger would
|
|
2391
|
-
// open its OWN streamEvents tail — a second live SSE connection next to the
|
|
2392
|
-
// session route's useSessionEvents. A caller with no feed opts into polling
|
|
2393
|
-
// (pollIntervalMs), never a duplicate stream.
|
|
2394
|
-
{ events: options.events, enabled: enabled && options.events !== void 0 }
|
|
2395
|
-
);
|
|
2396
|
-
useSessionEventTrigger(
|
|
2397
|
-
client,
|
|
2398
|
-
workspaceId,
|
|
2399
|
-
sessionId,
|
|
2400
|
-
isSessionCreateToolCallCreated,
|
|
2401
|
-
refreshAfterChildCreate,
|
|
2402
|
-
{ events: options.events, enabled: enabled && options.events !== void 0 }
|
|
2403
|
-
);
|
|
2404
|
-
return {
|
|
2405
|
-
lineage: state.data?.lineage ?? null,
|
|
2406
|
-
loading: state.loading,
|
|
2407
|
-
error: state.error,
|
|
2408
|
-
readGeneration: state.data?.readGeneration ?? 0,
|
|
2409
|
-
refresh
|
|
2410
|
-
};
|
|
2411
|
-
}
|
|
2412
|
-
|
|
2413
|
-
// src/approvals.ts
|
|
2414
|
-
function approvalsFromRequiresAction(payload) {
|
|
2415
|
-
const approvals = payload && typeof payload === "object" ? payload.approvals : void 0;
|
|
2416
|
-
if (!Array.isArray(approvals)) {
|
|
2417
|
-
return [];
|
|
2418
|
-
}
|
|
2419
|
-
return approvals.map((approval, index) => {
|
|
2420
|
-
const raw = approval && typeof approval === "object" ? approval : {};
|
|
2421
|
-
const rawItem = raw.rawItem && typeof raw.rawItem === "object" ? raw.rawItem : {};
|
|
2422
|
-
return {
|
|
2423
|
-
// Mirror the runtime's canonical interruption identity before retaining
|
|
2424
|
-
// the legacy top-level callId fallback for already-normalized payloads.
|
|
2425
|
-
id: String(rawItem.callId ?? rawItem.id ?? raw.id ?? raw.callId ?? raw.name ?? index),
|
|
2426
|
-
name: String(raw.name ?? raw.toolName ?? rawItem.name ?? "approval"),
|
|
2427
|
-
arguments: raw.arguments ?? rawItem.arguments,
|
|
2428
|
-
raw: approval
|
|
2429
|
-
};
|
|
2430
|
-
});
|
|
2431
|
-
}
|
|
2432
|
-
function projectPendingApprovals(events) {
|
|
2433
|
-
let pending = [];
|
|
2434
|
-
let owningTurnId = null;
|
|
2435
|
-
for (const event of events) {
|
|
2436
|
-
switch (event.type) {
|
|
2437
|
-
case "session.requiresAction": {
|
|
2438
|
-
pending = approvalsFromRequiresAction(event.payload);
|
|
2439
|
-
owningTurnId = event.turnId ?? null;
|
|
2440
|
-
break;
|
|
2441
|
-
}
|
|
2442
|
-
case "user.approvalDecision": {
|
|
2443
|
-
const payload = event.payload && typeof event.payload === "object" ? event.payload : {};
|
|
2444
|
-
if (typeof payload.approvalId === "string") {
|
|
2445
|
-
pending = pending.filter((approval) => approval.id !== payload.approvalId);
|
|
2446
|
-
}
|
|
2447
|
-
break;
|
|
2448
|
-
}
|
|
2449
|
-
case "turn.completed":
|
|
2450
|
-
case "turn.failed":
|
|
2451
|
-
case "turn.cancelled": {
|
|
2452
|
-
if (owningTurnId === null || event.turnId == null || event.turnId === owningTurnId) {
|
|
2453
|
-
pending = [];
|
|
2454
|
-
owningTurnId = null;
|
|
2455
|
-
}
|
|
2456
|
-
break;
|
|
2457
|
-
}
|
|
2458
|
-
default:
|
|
2459
|
-
break;
|
|
2460
|
-
}
|
|
2461
|
-
}
|
|
2462
|
-
return pending;
|
|
2463
|
-
}
|
|
2464
|
-
|
|
2465
|
-
// src/hooks/use-human-input.ts
|
|
2466
|
-
import { useCallback as useCallback9, useEffect as useEffect8, useRef as useRef9, useState as useState7 } from "react";
|
|
2467
|
-
function isHumanInputEvent(event) {
|
|
2468
|
-
return event.type === "session.humanInput.requested" || event.type === "user.humanInputResponse" || event.type === "turn.completed" || event.type === "turn.failed" || event.type === "turn.cancelled";
|
|
2469
|
-
}
|
|
2470
|
-
function useHumanInputRequests(sessionId, options = {}) {
|
|
2471
|
-
const { client, workspaceId, registerSessionReconciler } = useEmbeddedHumanInputSession(options);
|
|
2472
|
-
const enabled = (options.enabled ?? true) && Boolean(sessionId);
|
|
2473
|
-
const load = useCallback9(
|
|
2474
|
-
async () => sessionId ? await client.listHumanInputRequests(workspaceId, sessionId, { status: "pending" }) : [],
|
|
2475
|
-
[client, sessionId, workspaceId]
|
|
2476
|
-
);
|
|
2477
|
-
const state = usePolledValue(load, {
|
|
2478
|
-
enabled,
|
|
2479
|
-
pollIntervalMs: options.pollIntervalMs
|
|
2480
|
-
});
|
|
2481
|
-
const loadedRequests = state.data;
|
|
2482
|
-
const refreshRequests = state.refresh;
|
|
2483
|
-
const mutation = useMutationRunner(`${workspaceId}\0${sessionId ?? ""}`);
|
|
2484
|
-
const [respondingRequestId, setRespondingRequestId] = useState7(null);
|
|
2485
|
-
const [deadlineVersion, setDeadlineVersion] = useState7(0);
|
|
2486
|
-
const respondingRequestRef = useRef9(null);
|
|
2487
|
-
useEffect8(() => {
|
|
2488
|
-
respondingRequestRef.current = null;
|
|
2489
|
-
setRespondingRequestId(null);
|
|
2490
|
-
}, [sessionId, workspaceId]);
|
|
2491
|
-
useEffect8(() => {
|
|
2492
|
-
if (!sessionId || !enabled) return;
|
|
2493
|
-
return registerSessionReconciler(sessionId, "human-input", state.refresh);
|
|
2494
|
-
}, [enabled, registerSessionReconciler, sessionId, state.refresh]);
|
|
2495
|
-
const scheduleRefresh = useDebouncedCallback(() => void state.refresh());
|
|
2496
|
-
useSessionEventTrigger(client, workspaceId, sessionId, isHumanInputEvent, scheduleRefresh, {
|
|
2497
|
-
enabled,
|
|
2498
|
-
...options.events !== void 0 ? { events: options.events } : {}
|
|
2499
|
-
});
|
|
2500
|
-
useEffect8(() => {
|
|
2501
|
-
const now2 = Date.now();
|
|
2502
|
-
const nextDeadline = (loadedRequests ?? []).reduce((next, request) => {
|
|
2503
|
-
if (request.status !== "pending" || !request.expiresAt) return next;
|
|
2504
|
-
const deadline = Date.parse(request.expiresAt);
|
|
2505
|
-
if (!Number.isFinite(deadline) || deadline <= now2) return next;
|
|
2506
|
-
return next === null ? deadline : Math.min(next, deadline);
|
|
2507
|
-
}, null);
|
|
2508
|
-
if (nextDeadline === null) return;
|
|
2509
|
-
const timer = globalThis.setTimeout(
|
|
2510
|
-
() => {
|
|
2511
|
-
setDeadlineVersion((current) => current + 1);
|
|
2512
|
-
void refreshRequests();
|
|
2513
|
-
},
|
|
2514
|
-
Math.min(2147483647, Math.max(0, nextDeadline - now2 + 1))
|
|
2515
|
-
);
|
|
2516
|
-
return () => globalThis.clearTimeout(timer);
|
|
2517
|
-
}, [loadedRequests, refreshRequests]);
|
|
2518
|
-
const respond = useCallback9(
|
|
2519
|
-
async (requestId, response) => {
|
|
2520
|
-
if (!sessionId || respondingRequestRef.current !== null) return null;
|
|
2521
|
-
respondingRequestRef.current = requestId;
|
|
2522
|
-
setRespondingRequestId(requestId);
|
|
2523
|
-
try {
|
|
2524
|
-
const event = await mutation.run(
|
|
2525
|
-
() => client.submitHumanInputResponse(workspaceId, sessionId, requestId, response, {
|
|
2526
|
-
clientEventId: globalThis.crypto.randomUUID()
|
|
2527
|
-
})
|
|
2528
|
-
);
|
|
2529
|
-
if (event) await state.refresh();
|
|
2530
|
-
return event;
|
|
2531
|
-
} finally {
|
|
2532
|
-
if (respondingRequestRef.current === requestId) {
|
|
2533
|
-
respondingRequestRef.current = null;
|
|
2534
|
-
setRespondingRequestId(null);
|
|
2535
|
-
}
|
|
2536
|
-
}
|
|
2537
|
-
},
|
|
2538
|
-
[client, mutation, sessionId, state, workspaceId]
|
|
2539
|
-
);
|
|
2540
|
-
const now = Date.now();
|
|
2541
|
-
void deadlineVersion;
|
|
2542
|
-
return {
|
|
2543
|
-
requests: [...state.data ?? []].filter((request) => isActionableHumanInputRequest(request, now)).sort(
|
|
2544
|
-
(left, right) => left.createdAt.localeCompare(right.createdAt) || left.id.localeCompare(right.id)
|
|
2545
|
-
),
|
|
2546
|
-
loading: state.loading,
|
|
2547
|
-
error: state.error,
|
|
2548
|
-
refresh: state.refresh,
|
|
2549
|
-
respond,
|
|
2550
|
-
respondingRequestId,
|
|
2551
|
-
mutationError: mutation.mutationError,
|
|
2552
|
-
clearMutationError: mutation.clearMutationError
|
|
2553
|
-
};
|
|
2554
|
-
}
|
|
2555
|
-
|
|
2556
|
-
export {
|
|
2557
|
-
createEmbeddedSessionClient,
|
|
2558
|
-
isTitleEvent,
|
|
2559
|
-
useSession,
|
|
2560
|
-
SESSION_EVENT_BROWSER_MAX_BYTES,
|
|
2561
|
-
SESSION_EVENT_BROWSER_MAX_COUNT,
|
|
2562
|
-
SESSION_EVENT_BROWSER_SINGLE_EVENT_MAX_BYTES,
|
|
2563
|
-
SESSION_EVENT_BROWSER_PENDING_MAX_BYTES,
|
|
2564
|
-
SESSION_EVENT_BROWSER_PENDING_MAX_COUNT,
|
|
2565
|
-
useSessionEvents,
|
|
2566
|
-
boundBrowserSessionEventWindow,
|
|
2567
|
-
useFileAttachments,
|
|
2568
|
-
isTurnQueueEvent,
|
|
2569
|
-
useTurnQueue,
|
|
2570
|
-
isGoalEvent,
|
|
2571
|
-
useGoal,
|
|
2572
|
-
useSessionControl,
|
|
2573
|
-
isSessionMcpApprovalPolicyEvent,
|
|
2574
|
-
useSessionMcpApprovalPolicy,
|
|
2575
|
-
isLineageRefreshEvent,
|
|
2576
|
-
useSessionLineage,
|
|
2577
|
-
approvalsFromRequiresAction,
|
|
2578
|
-
projectPendingApprovals,
|
|
2579
|
-
isHumanInputEvent,
|
|
2580
|
-
useHumanInputRequests
|
|
2581
|
-
};
|
|
2582
|
-
//# sourceMappingURL=chunk-IB27C53Y.js.map
|