@fungi.computer/parakeet 0.0.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +45 -6
- package/dist/account-commands.js +3 -11
- package/dist/commands/advertised.d.ts +3 -1
- package/dist/commands/advertised.js +89 -4
- package/dist/commands/builtins.js +1 -1
- package/dist/commands/interactions.d.ts +22 -7
- package/dist/commands/interactions.js +41 -17
- package/dist/commands/run.d.ts +3 -2
- package/dist/commands/run.js +5 -2
- package/dist/commands/slash.d.ts +2 -0
- package/dist/commands/slash.js +13 -7
- package/dist/commands/use-slash-commands.js +4 -4
- package/dist/components/AgentHead.d.ts +9 -12
- package/dist/components/AgentHead.js +12 -44
- package/dist/components/ChatComposer.d.ts +3 -1
- package/dist/components/ChatComposer.js +79 -15
- package/dist/components/ChatMessage.d.ts +4 -0
- package/dist/components/ChatMessage.js +6 -2
- package/dist/components/ClientRows.js +2 -2
- package/dist/components/InteractionPanel.d.ts +2 -4
- package/dist/components/InteractionPanel.js +2 -2
- package/dist/components/ShiitakeAvatar.d.ts +7 -5
- package/dist/components/ShiitakeAvatar.js +6 -4
- package/dist/components/TimelineItem.d.ts +2 -0
- package/dist/components/TimelineItem.js +31 -7
- package/dist/components/avatar-presentation.d.ts +7 -14
- package/dist/components/avatar-presentation.js +6 -13
- package/dist/conversation-opening.d.ts +47 -0
- package/dist/conversation-opening.js +173 -0
- package/dist/conversation.d.ts +50 -5
- package/dist/conversation.js +587 -87
- package/dist/core/author-labels.d.ts +2 -0
- package/dist/core/build-timeline.d.ts +4 -2
- package/dist/core/build-timeline.js +46 -7
- package/dist/core/client-actions.js +6 -3
- package/dist/core/client-timeline.d.ts +9 -2
- package/dist/core/client-timeline.js +15 -4
- package/dist/core/draft-storage.d.ts +63 -0
- package/dist/core/draft-storage.js +50 -0
- package/dist/core/model-identity.d.ts +2 -0
- package/dist/core/model-identity.js +6 -0
- package/dist/core/queued-recall.d.ts +35 -0
- package/dist/core/queued-recall.js +138 -0
- package/dist/core/session-projection.js +17 -0
- package/dist/core/session-query.d.ts +54 -11
- package/dist/core/session-query.js +38 -12
- package/dist/core/tool-groups.d.ts +1 -1
- package/dist/core/tool-groups.js +3 -1
- package/dist/core/tool-view.d.ts +1 -1
- package/dist/core/tool-view.js +3 -2
- package/dist/core/use-timeline-item-key.d.ts +8 -0
- package/dist/core/use-timeline-item-key.js +29 -0
- package/dist/headless.d.ts +6 -4
- package/dist/headless.js +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/model-commands.js +65 -40
- package/dist/parakeet.js +17 -18
- package/dist/provider-choices.js +10 -7
- package/dist/session-commands.d.ts +0 -9
- package/dist/session-commands.js +39 -68
- package/dist/session.d.ts +1 -1
- package/dist/store/types.d.ts +10 -4
- package/dist/styles.css +105 -117
- package/docs/api/commands/slash.md +24 -21
- package/docs/api/headless.md +375 -232
- package/docs/api/index.md +168 -60
- package/package.json +16 -10
- package/src/styles.css +4 -9
- package/dist/components/still-frame-cache.d.ts +0 -16
- package/dist/components/still-frame-cache.js +0 -63
package/dist/conversation.js
CHANGED
|
@@ -7,13 +7,15 @@ import { createParakeetInteractions, } from "./commands/interactions.js";
|
|
|
7
7
|
import { CancelledError, useQueryClient, } from "@tanstack/react-query";
|
|
8
8
|
import { atom, createStore, } from "jotai/vanilla";
|
|
9
9
|
import { atomWithInfiniteQuery, atomWithMutation, atomWithQuery, } from "jotai-tanstack-query";
|
|
10
|
-
import { createContext, useCallback, useContext, useEffect, useMemo, useSyncExternalStore, } from "react";
|
|
10
|
+
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useSyncExternalStore, } from "react";
|
|
11
11
|
import { buildDurableTimeline, buildTimelineTail, } from "./core/build-timeline.js";
|
|
12
12
|
import { mergeCompactionNotices, mergeCompactions, mergeMessages, } from "./core/session-projection.js";
|
|
13
13
|
import { sessionHistoryQueryOptions, sessionQueryOptions, } from "./core/session-query.js";
|
|
14
14
|
import { sameSessionViewState, sessionActivityStatus, sessionViewState, } from "./core/session-view-state.js";
|
|
15
15
|
import { isCancelledOutcome } from "./commands/run.js";
|
|
16
16
|
import { reportFailure } from "./lib/failure.js";
|
|
17
|
+
import { createQueuedRecall, } from "./core/queued-recall.js";
|
|
18
|
+
import { draftJournal, } from "./core/draft-storage.js";
|
|
17
19
|
const noMessages = Object.freeze([]);
|
|
18
20
|
const noCompactions = Object.freeze([]);
|
|
19
21
|
const noCompactionNotices = Object.freeze([]);
|
|
@@ -33,6 +35,18 @@ function errorMessage(cause) {
|
|
|
33
35
|
? cause.message
|
|
34
36
|
: "Prompt admission failed";
|
|
35
37
|
}
|
|
38
|
+
/** A lost withdrawal response must not block a later reconnect from retrying it. */
|
|
39
|
+
function boundWithdrawal(operation) {
|
|
40
|
+
let timer;
|
|
41
|
+
const deadline = new Promise((_resolve, reject) => {
|
|
42
|
+
timer = setTimeout(() => {
|
|
43
|
+
reject(new Error("Request recovery timed out"));
|
|
44
|
+
}, 10_000);
|
|
45
|
+
});
|
|
46
|
+
return Promise.race([operation, deadline]).finally(() => {
|
|
47
|
+
clearTimeout(timer);
|
|
48
|
+
});
|
|
49
|
+
}
|
|
36
50
|
function observationCause(cause) {
|
|
37
51
|
const message = cause instanceof Error && cause.message.trim().length > 0
|
|
38
52
|
? cause.message.trim()
|
|
@@ -70,19 +84,40 @@ function newestDurableCommandId(messages) {
|
|
|
70
84
|
}
|
|
71
85
|
return newest?.commandId;
|
|
72
86
|
}
|
|
73
|
-
function
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
.join("\n")
|
|
82
|
-
: content;
|
|
83
|
-
return text.trim() === "" ? undefined : text;
|
|
87
|
+
function sameImageCargo(left, right) {
|
|
88
|
+
const images = left ?? [];
|
|
89
|
+
const originals = right ?? [];
|
|
90
|
+
return (images.length === originals.length &&
|
|
91
|
+
images.every((image, index) => {
|
|
92
|
+
const original = originals[index];
|
|
93
|
+
return (image.data === original?.data && image.mimeType === original.mimeType);
|
|
94
|
+
}));
|
|
84
95
|
}
|
|
85
|
-
function
|
|
96
|
+
function acquireDraftStorage(supplied) {
|
|
97
|
+
if (supplied !== undefined)
|
|
98
|
+
return { status: "available", storage: supplied };
|
|
99
|
+
try {
|
|
100
|
+
const browser = globalThis;
|
|
101
|
+
const storage = browser.localStorage;
|
|
102
|
+
return storage === undefined
|
|
103
|
+
? { status: "unavailable" }
|
|
104
|
+
: { status: "available", storage };
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
// An opaque-origin sandbox denies even reading the browser's getter.
|
|
108
|
+
return { status: "unavailable" };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function createConversationAtoms(session, queryClient, authorOf, interactions, draftStorage, draftScope, contributions, shown) {
|
|
112
|
+
const journal = draftJournal(draftScope, draftStorage.status === "available" ? draftStorage.storage : undefined);
|
|
113
|
+
const saved = journal.read();
|
|
114
|
+
let mounted = false;
|
|
115
|
+
const timers = new Map();
|
|
116
|
+
const recoveryReasons = new Map(saved.pending.map(({ requestId }) => [
|
|
117
|
+
requestId,
|
|
118
|
+
"Closed before confirmation",
|
|
119
|
+
]));
|
|
120
|
+
const recovering = new Set();
|
|
86
121
|
const queryAtom = atomWithQuery(() => sessionQueryOptions(session), () => queryClient);
|
|
87
122
|
const snapshotAtom = atom((get) => get(queryAtom).data?.snapshot);
|
|
88
123
|
const pinnedHistoryAtom = atom(undefined);
|
|
@@ -169,9 +204,31 @@ function createConversationAtoms(session, queryClient, authorOf) {
|
|
|
169
204
|
? pages
|
|
170
205
|
: mergeCompactionNotices(mergeCompactionNotices(pages, pinned), live);
|
|
171
206
|
});
|
|
172
|
-
const newestCommandAtom = atom((get) => get(queryAtom).data?.latestPrompt?.
|
|
207
|
+
const newestCommandAtom = atom((get) => get(queryAtom).data?.latestPrompt?.commandId ??
|
|
173
208
|
newestDurableCommandId(get(messagesAtom)));
|
|
174
209
|
const pendingAtom = atom((get) => get(snapshotAtom)?.pending ?? noPending);
|
|
210
|
+
// Presentation only: owner snapshots and admission reconciliation stay intact.
|
|
211
|
+
const hiddenQueuedAtom = atom(new Set());
|
|
212
|
+
const cancellingQueuedAtom = atom(undefined);
|
|
213
|
+
const visiblePendingAtom = atom((get) => {
|
|
214
|
+
const pending = get(pendingAtom);
|
|
215
|
+
const hidden = get(hiddenQueuedAtom);
|
|
216
|
+
const prompts = pending.prompts.filter(({ commandId }) => !hidden.has(commandId));
|
|
217
|
+
return prompts.length === pending.prompts.length
|
|
218
|
+
? pending
|
|
219
|
+
: {
|
|
220
|
+
...pending,
|
|
221
|
+
prompts,
|
|
222
|
+
total: pending.total - (pending.prompts.length - prompts.length),
|
|
223
|
+
};
|
|
224
|
+
});
|
|
225
|
+
const reconcileQueuedCancellationAtom = atom(null, (get, set) => {
|
|
226
|
+
const hidden = get(hiddenQueuedAtom);
|
|
227
|
+
const pending = new Set(get(pendingAtom).prompts.map(({ commandId }) => commandId));
|
|
228
|
+
const retained = new Set([...hidden].filter((id) => id === get(cancellingQueuedAtom) || pending.has(id)));
|
|
229
|
+
if (retained.size !== hidden.size)
|
|
230
|
+
set(hiddenQueuedAtom, retained);
|
|
231
|
+
});
|
|
175
232
|
const extensionsAtom = atom((get) => get(snapshotAtom)?.extensions ?? noSlots);
|
|
176
233
|
let liveToolActivity;
|
|
177
234
|
const toolActivityAtom = atom((get) => {
|
|
@@ -197,11 +254,128 @@ function createConversationAtoms(session, queryClient, authorOf) {
|
|
|
197
254
|
const requestIds = get(promptRequestIdsAtom);
|
|
198
255
|
return (commandId) => requestIds.get(commandId);
|
|
199
256
|
});
|
|
200
|
-
const durableAtom = atom((get) =>
|
|
201
|
-
|
|
257
|
+
const durableAtom = atom((get) => {
|
|
258
|
+
const messages = get(messagesAtom);
|
|
259
|
+
const compactions = get(compactionsAtom);
|
|
260
|
+
const notices = get(compactionNoticesAtom);
|
|
261
|
+
const sequences = [
|
|
262
|
+
...messages.map((message) => message.sequence),
|
|
263
|
+
...compactions.map((marker) => marker.sequence),
|
|
264
|
+
...notices.map((notice) => notice.sequence),
|
|
265
|
+
];
|
|
266
|
+
const oldest = Math.min(...sequences);
|
|
267
|
+
const newest = Math.max(...sequences);
|
|
268
|
+
const positions = get(contributions).flatMap(({ value }) => value.timeline.cells.flatMap(({ afterSequence }) => afterSequence === undefined ||
|
|
269
|
+
(get(hasMoreHistoryAtom) && afterSequence < oldest) ||
|
|
270
|
+
(get(hasNewerHistoryAtom) && afterSequence > newest)
|
|
271
|
+
? []
|
|
272
|
+
: [afterSequence]));
|
|
273
|
+
return buildDurableTimeline(messages, compactions, get(toolActivityAtom), authorOf, get(requestIdOfAtom), notices, positions, shown);
|
|
274
|
+
});
|
|
275
|
+
const draftAtom = atom(saved.draft);
|
|
276
|
+
const draftImagesAtom = atom(saved.images);
|
|
277
|
+
const draftDeliveryAtom = atom(saved.delivery);
|
|
278
|
+
const recallReservationAtom = atom(saved.recall);
|
|
279
|
+
const recallStateAtom = atom({ kind: "idle" });
|
|
280
|
+
let recallOwner;
|
|
202
281
|
const draftRevisionAtom = atom(0);
|
|
203
|
-
const rawAdmissionsAtom = atom(
|
|
282
|
+
const rawAdmissionsAtom = atom(saved.pending.map((pending) => ({
|
|
283
|
+
...pending,
|
|
284
|
+
status: "admitting",
|
|
285
|
+
})));
|
|
286
|
+
const persistAtom = atom(null, (get, _set, state) => {
|
|
287
|
+
// Unavailable storage is session-only, not a pretend durable journal.
|
|
288
|
+
// A supplied adapter's write failures still refuse durable operations.
|
|
289
|
+
if (draftStorage.status === "unavailable")
|
|
290
|
+
return;
|
|
291
|
+
journal.write({
|
|
292
|
+
draft: state?.draft ?? get(draftAtom),
|
|
293
|
+
images: state?.draftPayload === undefined
|
|
294
|
+
? get(draftImagesAtom)
|
|
295
|
+
: state.draftPayload.images,
|
|
296
|
+
delivery: state?.draftPayload === undefined
|
|
297
|
+
? get(draftDeliveryAtom)
|
|
298
|
+
: state.draftPayload.delivery,
|
|
299
|
+
recall: state?.recall === null
|
|
300
|
+
? undefined
|
|
301
|
+
: (state?.recall ?? get(recallReservationAtom)),
|
|
302
|
+
pending: (state?.admissions ?? get(rawAdmissionsAtom))
|
|
303
|
+
.filter(({ status }) => status === "admitting")
|
|
304
|
+
.map(({ requestId, content, submittedDraft, delivery, images, restoreDraft, }) => ({
|
|
305
|
+
requestId,
|
|
306
|
+
content,
|
|
307
|
+
submittedDraft,
|
|
308
|
+
delivery,
|
|
309
|
+
images,
|
|
310
|
+
restoreDraft,
|
|
311
|
+
})),
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
const recallAtom = atom(null, (get, set) => {
|
|
315
|
+
if (get(cancellingQueuedAtom) !== undefined)
|
|
316
|
+
return Promise.resolve("ignored");
|
|
317
|
+
recallOwner ??= createQueuedRecall({
|
|
318
|
+
session: {
|
|
319
|
+
latestPending: () => session.latestPending(),
|
|
320
|
+
cancel: (input) => session.command({ type: "cancel", ...input }),
|
|
321
|
+
},
|
|
322
|
+
recovery: get(recallReservationAtom),
|
|
323
|
+
emptyDraftRevision: () => get(draftAtom) === "" && (get(draftImagesAtom)?.length ?? 0) === 0
|
|
324
|
+
? get(draftRevisionAtom)
|
|
325
|
+
: undefined,
|
|
326
|
+
reserve: (prompt) => {
|
|
327
|
+
set(persistAtom, { recall: prompt });
|
|
328
|
+
set(recallReservationAtom, prompt);
|
|
329
|
+
},
|
|
330
|
+
forget: () => {
|
|
331
|
+
set(persistAtom, { recall: null });
|
|
332
|
+
set(recallReservationAtom, undefined);
|
|
333
|
+
},
|
|
334
|
+
restore: (prompt, revision) => {
|
|
335
|
+
if (get(draftRevisionAtom) !== revision ||
|
|
336
|
+
get(draftAtom) !== "" ||
|
|
337
|
+
(get(draftImagesAtom)?.length ?? 0) !== 0)
|
|
338
|
+
return false;
|
|
339
|
+
set(persistAtom, {
|
|
340
|
+
draft: prompt.text,
|
|
341
|
+
draftPayload: { images: prompt.images, delivery: prompt.delivery },
|
|
342
|
+
recall: null,
|
|
343
|
+
});
|
|
344
|
+
set(recallReservationAtom, undefined);
|
|
345
|
+
set(draftImagesAtom, prompt.images);
|
|
346
|
+
set(draftDeliveryAtom, prompt.delivery);
|
|
347
|
+
set(draftAtom, prompt.text);
|
|
348
|
+
set(draftRevisionAtom, (current) => current + 1);
|
|
349
|
+
return true;
|
|
350
|
+
},
|
|
351
|
+
onState: (state) => {
|
|
352
|
+
set(recallStateAtom, state);
|
|
353
|
+
if (state.kind === "too_late" || state.kind === "uncertain")
|
|
354
|
+
interactions.toast({ variant: "warning", message: state.message });
|
|
355
|
+
},
|
|
356
|
+
});
|
|
357
|
+
return recallOwner.recall();
|
|
358
|
+
});
|
|
204
359
|
const latestSubmissionIdAtom = atom(undefined);
|
|
360
|
+
const checkpointDraftAtom = atom(null, (_get, set) => {
|
|
361
|
+
if (!mounted)
|
|
362
|
+
return {
|
|
363
|
+
status: "failed",
|
|
364
|
+
reason: "Conversation is no longer mounted",
|
|
365
|
+
};
|
|
366
|
+
if (draftStorage.status === "unavailable")
|
|
367
|
+
return {
|
|
368
|
+
status: "failed",
|
|
369
|
+
reason: "Durable draft storage is unavailable",
|
|
370
|
+
};
|
|
371
|
+
try {
|
|
372
|
+
set(persistAtom);
|
|
373
|
+
return { status: "saved" };
|
|
374
|
+
}
|
|
375
|
+
catch (cause) {
|
|
376
|
+
return { status: "failed", reason: errorMessage(cause) };
|
|
377
|
+
}
|
|
378
|
+
});
|
|
205
379
|
const observedCommandIdsAtom = atom((get) => new Set([
|
|
206
380
|
...get(durableAtom).commandIds,
|
|
207
381
|
...get(pendingAtom).prompts.map(({ commandId }) => commandId),
|
|
@@ -217,41 +391,127 @@ function createConversationAtoms(session, queryClient, authorOf) {
|
|
|
217
391
|
/** Retire a successful local admission only after owner identity is observed. */
|
|
218
392
|
const reconcileAdmissionsAtom = atom(null, (get, set) => {
|
|
219
393
|
const knownCommandIds = get(observedCommandIdsAtom);
|
|
394
|
+
const observedRequests = new Map([
|
|
395
|
+
// Recorded replies have no command: their accepted receipt names the
|
|
396
|
+
// durable entry, not model admission or cancellation.
|
|
397
|
+
...get(messagesAtom).flatMap(({ id, requestId, commandId }) => requestId === undefined ? [] : [[requestId, commandId ?? id]]),
|
|
398
|
+
...get(pendingAtom).prompts.map(({ requestId, commandId }) => [requestId, commandId]),
|
|
399
|
+
]);
|
|
400
|
+
for (const candidate of get(rawAdmissionsAtom)) {
|
|
401
|
+
const commandId = observedRequests.get(candidate.requestId);
|
|
402
|
+
if (commandId !== undefined) {
|
|
403
|
+
set(promptRequestIdsAtom, (current) => new Map(current).set(commandId, candidate.requestId));
|
|
404
|
+
}
|
|
405
|
+
}
|
|
220
406
|
set(rawAdmissionsAtom, (current) => {
|
|
221
|
-
const unresolved = current.filter(({ status, commandId }) =>
|
|
222
|
-
|
|
223
|
-
|
|
407
|
+
const unresolved = current.filter(({ requestId, status, commandId }) => !observedRequests.has(requestId) &&
|
|
408
|
+
(status !== "accepted" ||
|
|
409
|
+
commandId === undefined ||
|
|
410
|
+
!knownCommandIds.has(commandId)));
|
|
411
|
+
if (unresolved.length !== current.length) {
|
|
412
|
+
try {
|
|
413
|
+
set(persistAtom, { admissions: unresolved });
|
|
414
|
+
}
|
|
415
|
+
catch (cause) {
|
|
416
|
+
reportFailure("save reconciled draft", cause);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
224
419
|
return unresolved.length === current.length ? current : unresolved;
|
|
225
420
|
});
|
|
226
421
|
});
|
|
227
422
|
const admissionErrorAtom = atom(undefined);
|
|
228
423
|
const setDraftAtom = atom(null, (get, set, value) => {
|
|
424
|
+
try {
|
|
425
|
+
set(persistAtom, { draft: value });
|
|
426
|
+
}
|
|
427
|
+
catch (cause) {
|
|
428
|
+
reportFailure("save current draft", cause);
|
|
429
|
+
interactions.toast({
|
|
430
|
+
variant: "error",
|
|
431
|
+
message: `Draft not saved: ${errorMessage(cause)}`,
|
|
432
|
+
});
|
|
433
|
+
}
|
|
229
434
|
if (get(draftAtom) !== value)
|
|
230
435
|
set(draftRevisionAtom, (revision) => revision + 1);
|
|
231
436
|
set(draftAtom, value);
|
|
232
437
|
set(admissionErrorAtom, undefined);
|
|
233
438
|
});
|
|
234
|
-
/**
|
|
439
|
+
/** Consume only the exact draft a command submitted. */
|
|
235
440
|
const clearSubmittedDraftAtom = atom(null, (get, set, submitted) => {
|
|
236
|
-
if (get(draftRevisionAtom) !== submitted.revision
|
|
441
|
+
if (get(draftRevisionAtom) !== submitted.revision ||
|
|
442
|
+
get(draftAtom) !== submitted.draft)
|
|
237
443
|
return;
|
|
238
|
-
set(
|
|
444
|
+
set(persistAtom, { draft: "" });
|
|
445
|
+
set(draftAtom, "");
|
|
239
446
|
});
|
|
240
447
|
const abortAtom = atomWithMutation(() => ({
|
|
241
448
|
mutationFn: (commandId) => session.command({ type: "cancel", commandId }),
|
|
242
449
|
}), () => queryClient);
|
|
450
|
+
// One unresolved queued gesture per mounted conversation, like Recall.
|
|
451
|
+
// Accepted masks last only while that identity remains in the bounded owner
|
|
452
|
+
// pending page; stale pre-ack snapshots cannot resurrect the removed row.
|
|
453
|
+
let queuedCancellation;
|
|
454
|
+
const cancelQueuedAtom = atom(null, (get, set, commandId) => {
|
|
455
|
+
if (!mounted || get(recallStateAtom).kind === "recalling")
|
|
456
|
+
return Promise.resolve();
|
|
457
|
+
if (queuedCancellation !== undefined)
|
|
458
|
+
return queuedCancellation;
|
|
459
|
+
if (get(hiddenQueuedAtom).has(commandId) ||
|
|
460
|
+
!get(pendingAtom).prompts.some((prompt) => prompt.commandId === commandId))
|
|
461
|
+
return Promise.resolve();
|
|
462
|
+
set(hiddenQueuedAtom, new Set(get(hiddenQueuedAtom)).add(commandId));
|
|
463
|
+
set(cancellingQueuedAtom, commandId);
|
|
464
|
+
const reveal = () => {
|
|
465
|
+
const next = new Set(get(hiddenQueuedAtom));
|
|
466
|
+
next.delete(commandId);
|
|
467
|
+
set(hiddenQueuedAtom, next);
|
|
468
|
+
};
|
|
469
|
+
const operation = Promise.resolve()
|
|
470
|
+
.then(() => session.command({ type: "cancel", commandId, onlyIfPending: true }))
|
|
471
|
+
.then((result) => {
|
|
472
|
+
if (!mounted)
|
|
473
|
+
return;
|
|
474
|
+
if (result.status === "too_late") {
|
|
475
|
+
reveal();
|
|
476
|
+
interactions.toast({
|
|
477
|
+
variant: "warning",
|
|
478
|
+
message: "Couldn't remove that message: it is no longer pending.",
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
}, () => {
|
|
482
|
+
if (!mounted)
|
|
483
|
+
return;
|
|
484
|
+
reveal();
|
|
485
|
+
interactions.toast({
|
|
486
|
+
variant: "warning",
|
|
487
|
+
message: "Cancellation unconfirmed. That message may still run. Try again or reconnect.",
|
|
488
|
+
});
|
|
489
|
+
})
|
|
490
|
+
.finally(() => {
|
|
491
|
+
queuedCancellation = undefined;
|
|
492
|
+
if (!mounted)
|
|
493
|
+
return;
|
|
494
|
+
set(cancellingQueuedAtom, undefined);
|
|
495
|
+
set(reconcileQueuedCancellationAtom);
|
|
496
|
+
});
|
|
497
|
+
queuedCancellation = operation;
|
|
498
|
+
return operation;
|
|
499
|
+
});
|
|
243
500
|
const tailAtom = atom((get) => {
|
|
244
501
|
if (get(hasNewerHistoryAtom))
|
|
245
502
|
return [];
|
|
246
503
|
const snapshot = get(snapshotAtom);
|
|
247
|
-
const pending = get(
|
|
248
|
-
return buildTimelineTail(get(durableAtom), snapshot?.progress ?? null, pending.prompts, get(admissionsAtom), authorOf
|
|
504
|
+
const pending = get(visiblePendingAtom);
|
|
505
|
+
return buildTimelineTail(get(durableAtom), snapshot?.progress ?? null, pending.prompts, get(admissionsAtom), authorOf);
|
|
249
506
|
});
|
|
250
507
|
const timelineAtom = atom((get) => {
|
|
251
508
|
const durable = get(durableAtom);
|
|
252
509
|
const base = {
|
|
253
510
|
pendingHasMore: get(pendingAtom).hasMore,
|
|
254
|
-
queued: get(
|
|
511
|
+
queued: get(visiblePendingAtom),
|
|
512
|
+
cancellingQueued: get(cancellingQueuedAtom) !== undefined ||
|
|
513
|
+
get(recallStateAtom).kind === "recalling",
|
|
514
|
+
positionAnchors: durable.positionAnchors,
|
|
255
515
|
commandAnchors: durable.commandAnchors,
|
|
256
516
|
hasMoreHistory: get(hasMoreHistoryAtom),
|
|
257
517
|
hasNewerHistory: get(hasNewerHistoryAtom),
|
|
@@ -336,21 +596,43 @@ function createConversationAtoms(session, queryClient, authorOf) {
|
|
|
336
596
|
ownerView = next;
|
|
337
597
|
return next;
|
|
338
598
|
});
|
|
339
|
-
const dispatchAdmissionAtom = atom(null, (get, set, admission) => {
|
|
340
|
-
|
|
341
|
-
|
|
599
|
+
const dispatchAdmissionAtom = atom(null, (get, set, { admission, consumeDraft, }) => {
|
|
600
|
+
try {
|
|
601
|
+
set(persistAtom, consumeDraft
|
|
602
|
+
? { draft: "", draftPayload: {} }
|
|
603
|
+
: { draft: get(draftAtom) });
|
|
604
|
+
}
|
|
605
|
+
catch (cause) {
|
|
606
|
+
reportFailure("save pending draft before send", cause);
|
|
607
|
+
set(rawAdmissionsAtom, (current) => current.filter(({ requestId }) => requestId !== admission.requestId));
|
|
608
|
+
interactions.toast({
|
|
609
|
+
variant: "error",
|
|
610
|
+
message: `Not sent: ${errorMessage(cause)}`,
|
|
611
|
+
});
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
if (consumeDraft) {
|
|
615
|
+
set(draftImagesAtom, undefined);
|
|
616
|
+
set(draftDeliveryAtom, undefined);
|
|
617
|
+
set(setDraftAtom, "");
|
|
618
|
+
}
|
|
619
|
+
const prompt = {
|
|
342
620
|
type: "prompt",
|
|
343
621
|
text: admission.content,
|
|
344
622
|
requestId: admission.requestId,
|
|
345
623
|
delivery: admission.delivery,
|
|
346
|
-
}
|
|
624
|
+
};
|
|
625
|
+
void session
|
|
626
|
+
.command(admission.images === undefined
|
|
627
|
+
? prompt
|
|
628
|
+
: { ...prompt, images: admission.images })
|
|
347
629
|
.then((accepted) => {
|
|
630
|
+
if (!mounted ||
|
|
631
|
+
!get(rawAdmissionsAtom).some(({ requestId, status }) => requestId === admission.requestId && status === "admitting"))
|
|
632
|
+
return;
|
|
348
633
|
if (accepted.status === "recorded") {
|
|
349
634
|
set(rawAdmissionsAtom, (current) => current.filter((candidate) => candidate.requestId !== admission.requestId));
|
|
350
|
-
set(
|
|
351
|
-
draft: admission.submittedDraft,
|
|
352
|
-
revision: admission.submittedDraftRevision,
|
|
353
|
-
});
|
|
635
|
+
set(persistAtom);
|
|
354
636
|
return;
|
|
355
637
|
}
|
|
356
638
|
set(promptRequestIdsAtom, (current) => new Map(current).set(accepted.commandId, admission.requestId));
|
|
@@ -358,48 +640,139 @@ function createConversationAtoms(session, queryClient, authorOf) {
|
|
|
358
640
|
? {
|
|
359
641
|
requestId: candidate.requestId,
|
|
360
642
|
content: candidate.content,
|
|
643
|
+
images: candidate.images,
|
|
361
644
|
submittedDraft: candidate.submittedDraft,
|
|
362
|
-
|
|
645
|
+
restoreDraft: candidate.restoreDraft,
|
|
363
646
|
delivery: candidate.delivery,
|
|
364
647
|
commandId: accepted.commandId,
|
|
365
648
|
status: "accepted",
|
|
366
649
|
}
|
|
367
650
|
: candidate));
|
|
368
|
-
const admissions = get(rawAdmissionsAtom);
|
|
369
|
-
const admissionIndex = admissions.findIndex((candidate) => candidate.requestId === admission.requestId);
|
|
370
|
-
const hasNewerMatchingAdmission = admissions
|
|
371
|
-
.slice(admissionIndex + 1)
|
|
372
|
-
.some((candidate) => candidate.status === "admitting" &&
|
|
373
|
-
candidate.submittedDraft === admission.submittedDraft);
|
|
374
651
|
set(reconcileAdmissionsAtom);
|
|
375
|
-
|
|
376
|
-
set(clearSubmittedDraftAtom, {
|
|
377
|
-
draft: admission.submittedDraft,
|
|
378
|
-
revision: admission.submittedDraftRevision,
|
|
379
|
-
});
|
|
652
|
+
set(persistAtom);
|
|
380
653
|
})
|
|
381
654
|
.catch((cause) => {
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
error: errorMessage(cause),
|
|
387
|
-
}
|
|
388
|
-
: candidate));
|
|
655
|
+
if (!mounted)
|
|
656
|
+
return;
|
|
657
|
+
recoveryReasons.set(admission.requestId, errorMessage(cause));
|
|
658
|
+
set(recoverAdmissionsAtom);
|
|
389
659
|
});
|
|
390
660
|
});
|
|
661
|
+
const recoverAdmissionsAtom = atom(null, (get, set) => {
|
|
662
|
+
if (!mounted)
|
|
663
|
+
return;
|
|
664
|
+
for (const admission of get(rawAdmissionsAtom)) {
|
|
665
|
+
const reason = recoveryReasons.get(admission.requestId);
|
|
666
|
+
if (admission.status !== "admitting" ||
|
|
667
|
+
reason === undefined ||
|
|
668
|
+
recovering.has(admission.requestId))
|
|
669
|
+
continue;
|
|
670
|
+
recovering.add(admission.requestId);
|
|
671
|
+
void boundWithdrawal(session.withdraw(admission.requestId))
|
|
672
|
+
.then((resolution) => {
|
|
673
|
+
if (!mounted ||
|
|
674
|
+
!get(rawAdmissionsAtom).some(({ requestId, status }) => requestId === admission.requestId && status === "admitting"))
|
|
675
|
+
return;
|
|
676
|
+
if (resolution.status === "accepted") {
|
|
677
|
+
set(promptRequestIdsAtom, (current) => new Map(current).set(resolution.commandId, admission.requestId));
|
|
678
|
+
set(rawAdmissionsAtom, (current) => current.map((candidate) => candidate.requestId === admission.requestId
|
|
679
|
+
? {
|
|
680
|
+
...candidate,
|
|
681
|
+
status: "accepted",
|
|
682
|
+
commandId: resolution.commandId,
|
|
683
|
+
}
|
|
684
|
+
: candidate));
|
|
685
|
+
set(persistAtom);
|
|
686
|
+
set(reconcileAdmissionsAtom);
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
const currentDraft = get(draftAtom);
|
|
690
|
+
const restored = !admission.restoreDraft
|
|
691
|
+
? currentDraft
|
|
692
|
+
: currentDraft === ""
|
|
693
|
+
? admission.submittedDraft
|
|
694
|
+
: `${currentDraft}\n\n${admission.submittedDraft}`;
|
|
695
|
+
const failed = get(rawAdmissionsAtom).map((candidate) => candidate.requestId === admission.requestId
|
|
696
|
+
? { ...candidate, status: "failed", error: reason }
|
|
697
|
+
: candidate);
|
|
698
|
+
// Commit restoration and removal of the pending record in ONE replacement.
|
|
699
|
+
const restoredImages = admission.restoreDraft
|
|
700
|
+
? [...(get(draftImagesAtom) ?? []), ...(admission.images ?? [])]
|
|
701
|
+
: get(draftImagesAtom);
|
|
702
|
+
set(persistAtom, {
|
|
703
|
+
draft: restored,
|
|
704
|
+
draftPayload: {
|
|
705
|
+
images: restoredImages,
|
|
706
|
+
delivery: get(draftDeliveryAtom),
|
|
707
|
+
},
|
|
708
|
+
admissions: failed,
|
|
709
|
+
});
|
|
710
|
+
set(draftImagesAtom, restoredImages);
|
|
711
|
+
set(rawAdmissionsAtom, failed);
|
|
712
|
+
set(setDraftAtom, restored);
|
|
713
|
+
interactions.toast({
|
|
714
|
+
variant: "error",
|
|
715
|
+
message: `Not sent: ${reason}`,
|
|
716
|
+
});
|
|
717
|
+
})
|
|
718
|
+
.catch((cause) => {
|
|
719
|
+
if (mounted)
|
|
720
|
+
interactions.toast({
|
|
721
|
+
variant: "error",
|
|
722
|
+
message: `Send outcome unconfirmed; draft saved: ${errorMessage(cause)}`,
|
|
723
|
+
});
|
|
724
|
+
})
|
|
725
|
+
.finally(() => {
|
|
726
|
+
recovering.delete(admission.requestId);
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
});
|
|
730
|
+
const maintainRecoveryAtom = atom(null, (get, set) => {
|
|
731
|
+
const observation = get(observationAtom).status;
|
|
732
|
+
const admissions = get(rawAdmissionsAtom);
|
|
733
|
+
for (const [id, timer] of timers) {
|
|
734
|
+
if (!admissions.some(({ requestId, status }) => requestId === id && status === "admitting")) {
|
|
735
|
+
clearTimeout(timer);
|
|
736
|
+
timers.delete(id);
|
|
737
|
+
recoveryReasons.delete(id);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
for (const admission of admissions) {
|
|
741
|
+
if (admission.status !== "admitting")
|
|
742
|
+
continue;
|
|
743
|
+
if (observation === "unavailable")
|
|
744
|
+
recoveryReasons.set(admission.requestId, "Disconnected");
|
|
745
|
+
if (!timers.has(admission.requestId)) {
|
|
746
|
+
timers.set(admission.requestId, setTimeout(() => {
|
|
747
|
+
recoveryReasons.set(admission.requestId, "Send confirmation timed out");
|
|
748
|
+
set(recoverAdmissionsAtom);
|
|
749
|
+
}, 30_000));
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
if (observation === "live" || observation === "unavailable")
|
|
753
|
+
set(recoverAdmissionsAtom);
|
|
754
|
+
});
|
|
755
|
+
const mountRecoveryAtom = atom(null, (_get, set) => {
|
|
756
|
+
mounted = true;
|
|
757
|
+
set(recallStateAtom, { kind: "idle" });
|
|
758
|
+
set(maintainRecoveryAtom);
|
|
759
|
+
return () => {
|
|
760
|
+
mounted = false;
|
|
761
|
+
recallOwner?.dispose();
|
|
762
|
+
recallOwner = undefined;
|
|
763
|
+
for (const timer of timers.values())
|
|
764
|
+
clearTimeout(timer);
|
|
765
|
+
timers.clear();
|
|
766
|
+
};
|
|
767
|
+
});
|
|
391
768
|
const sendAtom = atom(null, (get, set, delivery = "steer") => {
|
|
392
769
|
const draft = get(draftAtom);
|
|
393
|
-
const content = draft
|
|
394
|
-
if (
|
|
770
|
+
const content = draft;
|
|
771
|
+
if (get(recallStateAtom).kind === "recalling")
|
|
395
772
|
return;
|
|
396
|
-
|
|
397
|
-
const draftRevision = get(draftRevisionAtom);
|
|
398
|
-
const rawAdmissions = get(rawAdmissionsAtom);
|
|
399
|
-
if (rawAdmissions.some((admission) => admission.status === "admitting" &&
|
|
400
|
-
admission.submittedDraft === draft &&
|
|
401
|
-
admission.submittedDraftRevision === draftRevision))
|
|
773
|
+
if (content.trim() === "" && (get(draftImagesAtom)?.length ?? 0) === 0)
|
|
402
774
|
return;
|
|
775
|
+
const admissions = get(admissionsAtom);
|
|
403
776
|
const unresolved = admissions.filter(({ status }) => status !== "failed");
|
|
404
777
|
if (unresolved.length >= MAX_UNRESOLVED_LOCAL_ADMISSIONS) {
|
|
405
778
|
set(admissionErrorAtom, "queue_full");
|
|
@@ -407,23 +780,26 @@ function createConversationAtoms(session, queryClient, authorOf) {
|
|
|
407
780
|
}
|
|
408
781
|
const admission = {
|
|
409
782
|
content,
|
|
410
|
-
delivery,
|
|
783
|
+
delivery: get(draftDeliveryAtom) ?? delivery,
|
|
784
|
+
images: get(draftImagesAtom),
|
|
411
785
|
requestId: nanoid(),
|
|
412
786
|
submittedDraft: draft,
|
|
413
|
-
|
|
787
|
+
restoreDraft: true,
|
|
414
788
|
status: "admitting",
|
|
415
789
|
};
|
|
416
790
|
set(rawAdmissionsAtom, [...admissions, admission]);
|
|
417
791
|
set(latestSubmissionIdAtom, admission.requestId);
|
|
418
792
|
set(admissionErrorAtom, undefined);
|
|
419
|
-
set(dispatchAdmissionAtom, admission);
|
|
793
|
+
set(dispatchAdmissionAtom, { admission, consumeDraft: true });
|
|
420
794
|
});
|
|
421
795
|
const submitCommandAtom = atom(null, async (get, set, run) => {
|
|
422
796
|
const submitted = {
|
|
423
797
|
draft: get(draftAtom),
|
|
424
798
|
revision: get(draftRevisionAtom),
|
|
425
799
|
};
|
|
426
|
-
const outcome = await run(submitted.draft)
|
|
800
|
+
const outcome = await run(submitted.draft, () => {
|
|
801
|
+
set(clearSubmittedDraftAtom, submitted);
|
|
802
|
+
});
|
|
427
803
|
if (outcome?.status === "handled" && !isCancelledOutcome(outcome))
|
|
428
804
|
set(clearSubmittedDraftAtom, submitted);
|
|
429
805
|
return outcome;
|
|
@@ -433,12 +809,15 @@ function createConversationAtoms(session, queryClient, authorOf) {
|
|
|
433
809
|
if (get(failureAtom) === "interrupted" || get(failureAtom) === "failed") {
|
|
434
810
|
const snapshot = get(snapshotAtom);
|
|
435
811
|
const commandId = get(newestCommandAtom);
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
if (content === undefined)
|
|
812
|
+
const prompt = get(queryAtom).data?.latestPrompt;
|
|
813
|
+
if (snapshot === undefined ||
|
|
814
|
+
prompt === undefined ||
|
|
815
|
+
prompt.commandId !== commandId)
|
|
441
816
|
return;
|
|
817
|
+
const content = prompt.text;
|
|
818
|
+
// Equal text alone does not make a newer image draft the failed prompt.
|
|
819
|
+
const consumeDraft = get(draftAtom) === content &&
|
|
820
|
+
sameImageCargo(get(draftImagesAtom), prompt.images);
|
|
442
821
|
if (admissions.filter(({ status }) => status !== "failed").length >=
|
|
443
822
|
MAX_UNRESOLVED_LOCAL_ADMISSIONS) {
|
|
444
823
|
set(admissionErrorAtom, "queue_full");
|
|
@@ -446,16 +825,20 @@ function createConversationAtoms(session, queryClient, authorOf) {
|
|
|
446
825
|
}
|
|
447
826
|
const retry = {
|
|
448
827
|
content,
|
|
828
|
+
images: prompt.images,
|
|
449
829
|
delivery: "followUp",
|
|
450
830
|
requestId: nanoid(),
|
|
451
831
|
submittedDraft: content,
|
|
452
|
-
|
|
832
|
+
restoreDraft: consumeDraft,
|
|
453
833
|
status: "admitting",
|
|
454
834
|
};
|
|
455
835
|
set(rawAdmissionsAtom, [...get(rawAdmissionsAtom), retry]);
|
|
456
836
|
set(latestSubmissionIdAtom, retry.requestId);
|
|
457
837
|
set(admissionErrorAtom, undefined);
|
|
458
|
-
set(dispatchAdmissionAtom,
|
|
838
|
+
set(dispatchAdmissionAtom, {
|
|
839
|
+
admission: retry,
|
|
840
|
+
consumeDraft,
|
|
841
|
+
});
|
|
459
842
|
return;
|
|
460
843
|
}
|
|
461
844
|
const admission = admissions
|
|
@@ -468,18 +851,24 @@ function createConversationAtoms(session, queryClient, authorOf) {
|
|
|
468
851
|
set(admissionErrorAtom, "queue_full");
|
|
469
852
|
return;
|
|
470
853
|
}
|
|
854
|
+
const consumeDraft = get(draftAtom) === admission.submittedDraft &&
|
|
855
|
+
sameImageCargo(get(draftImagesAtom), admission.images);
|
|
471
856
|
const retry = {
|
|
472
857
|
content: admission.content,
|
|
473
858
|
delivery: admission.delivery,
|
|
474
|
-
|
|
859
|
+
images: admission.images,
|
|
860
|
+
requestId: nanoid(),
|
|
475
861
|
submittedDraft: admission.submittedDraft,
|
|
476
|
-
|
|
862
|
+
restoreDraft: consumeDraft,
|
|
477
863
|
status: "admitting",
|
|
478
864
|
};
|
|
479
865
|
set(rawAdmissionsAtom, (current) => current.map((candidate) => candidate.requestId === admission.requestId ? retry : candidate));
|
|
480
866
|
set(latestSubmissionIdAtom, retry.requestId);
|
|
481
867
|
set(admissionErrorAtom, undefined);
|
|
482
|
-
set(dispatchAdmissionAtom,
|
|
868
|
+
set(dispatchAdmissionAtom, {
|
|
869
|
+
admission: retry,
|
|
870
|
+
consumeDraft,
|
|
871
|
+
});
|
|
483
872
|
});
|
|
484
873
|
const dismissFailedAtom = atom(null, (get, set, requestId) => {
|
|
485
874
|
const admissions = get(rawAdmissionsAtom);
|
|
@@ -556,9 +945,16 @@ function createConversationAtoms(session, queryClient, authorOf) {
|
|
|
556
945
|
inspect();
|
|
557
946
|
});
|
|
558
947
|
const timeout = setTimeout(() => {
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
948
|
+
// Streamed "opening" updates also replace TanStack's revert state.
|
|
949
|
+
// Cancel first, then record this real timeout on the existing query
|
|
950
|
+
// without replacing its last coherent snapshot.
|
|
951
|
+
void query.cancel({ revert: true }).then(() => {
|
|
952
|
+
const cause = "Session history did not reopen within 10 seconds";
|
|
953
|
+
query.setState({ status: "error", error: new Error(cause) });
|
|
954
|
+
finish({
|
|
955
|
+
kind: "failed",
|
|
956
|
+
cause,
|
|
957
|
+
});
|
|
562
958
|
});
|
|
563
959
|
}, 10_000);
|
|
564
960
|
// A live stream does not settle its refetch promise until the watch closes.
|
|
@@ -576,12 +972,18 @@ function createConversationAtoms(session, queryClient, authorOf) {
|
|
|
576
972
|
return reconnecting;
|
|
577
973
|
});
|
|
578
974
|
return {
|
|
975
|
+
cancelQueuedAtom,
|
|
976
|
+
reconcileQueuedCancellationAtom,
|
|
579
977
|
abortAtom,
|
|
978
|
+
draftImagesAtom,
|
|
979
|
+
recallStateAtom,
|
|
980
|
+
recallAtom,
|
|
580
981
|
commandIdAtom,
|
|
581
982
|
contextAtom,
|
|
582
983
|
admissionErrorAtom,
|
|
583
984
|
admissionsAtom,
|
|
584
985
|
cancelAtom,
|
|
986
|
+
checkpointDraftAtom,
|
|
585
987
|
draftAtom,
|
|
586
988
|
dismissFailedAtom,
|
|
587
989
|
extensionsAtom,
|
|
@@ -591,6 +993,9 @@ function createConversationAtoms(session, queryClient, authorOf) {
|
|
|
591
993
|
loadNewerHistoryAtom,
|
|
592
994
|
observationAtom,
|
|
593
995
|
observedCommandIdsAtom,
|
|
996
|
+
rawAdmissionsAtom,
|
|
997
|
+
maintainRecoveryAtom,
|
|
998
|
+
mountRecoveryAtom,
|
|
594
999
|
pinVisibleHistoryAtom,
|
|
595
1000
|
queryAtom,
|
|
596
1001
|
reconcileAdmissionsAtom,
|
|
@@ -623,15 +1028,71 @@ function useStoreSetter(atom) {
|
|
|
623
1028
|
return useCallback((...args) => store.set(atom, ...args), [atom, store]);
|
|
624
1029
|
}
|
|
625
1030
|
/** Own one renderer-neutral conversation scope over the existing query cache. */
|
|
626
|
-
export function ParakeetProvider({ children, session, clients = noClients, interactionStore, authorOf = noAuthor, }) {
|
|
1031
|
+
export function ParakeetProvider({ children, session, clients = noClients, interactionStore, authorOf = noAuthor, onToast, draftStorage, draftIdentity, }) {
|
|
627
1032
|
const queryClient = useQueryClient();
|
|
628
|
-
const
|
|
629
|
-
|
|
1033
|
+
const acquiredDraftStorage = useMemo(() => acquireDraftStorage(draftStorage), [draftStorage]);
|
|
1034
|
+
const draftScope = draftIdentity === undefined
|
|
1035
|
+
? session.address
|
|
1036
|
+
: JSON.stringify([draftIdentity.agentId, draftIdentity.sessionId]);
|
|
1037
|
+
const footerPromptOwners = useMemo(() => new Set(clients
|
|
1038
|
+
.filter((entry) => entry.promptPresentation === "footer")
|
|
1039
|
+
.map((entry) => entry.id)), [clients]);
|
|
1040
|
+
const scope = useMemo(() => {
|
|
1041
|
+
const interactions = interactionStore ?? createParakeetInteractions();
|
|
1042
|
+
const contributions = atom(noContributions);
|
|
1043
|
+
const shown = (item) => item.kind !== "user_message" ||
|
|
1044
|
+
item.origin === undefined ||
|
|
1045
|
+
!footerPromptOwners.has(item.origin.extensionId);
|
|
1046
|
+
return {
|
|
1047
|
+
atoms: createConversationAtoms(session, queryClient, authorOf, interactions, acquiredDraftStorage, draftScope, contributions, shown),
|
|
1048
|
+
session,
|
|
1049
|
+
store: createStore(),
|
|
1050
|
+
contributions,
|
|
1051
|
+
interactions,
|
|
1052
|
+
shown,
|
|
1053
|
+
};
|
|
1054
|
+
}, [
|
|
1055
|
+
queryClient,
|
|
630
1056
|
session,
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
1057
|
+
interactionStore,
|
|
1058
|
+
authorOf,
|
|
1059
|
+
footerPromptOwners,
|
|
1060
|
+
acquiredDraftStorage,
|
|
1061
|
+
draftScope,
|
|
1062
|
+
]);
|
|
1063
|
+
useEffect(() => {
|
|
1064
|
+
const update = () => {
|
|
1065
|
+
scope.store.set(scope.atoms.maintainRecoveryAtom);
|
|
1066
|
+
};
|
|
1067
|
+
const unobserve = scope.store.sub(scope.atoms.observationAtom, update);
|
|
1068
|
+
const unobserveAdmissions = scope.store.sub(scope.atoms.rawAdmissionsAtom, update);
|
|
1069
|
+
const dispose = scope.store.set(scope.atoms.mountRecoveryAtom);
|
|
1070
|
+
return () => {
|
|
1071
|
+
dispose();
|
|
1072
|
+
unobserve();
|
|
1073
|
+
unobserveAdmissions();
|
|
1074
|
+
};
|
|
1075
|
+
}, [scope]);
|
|
1076
|
+
useEffect(() => {
|
|
1077
|
+
if (onToast === undefined)
|
|
1078
|
+
return undefined;
|
|
1079
|
+
return scope.interactions.subscribeToasts(onToast);
|
|
1080
|
+
}, [scope, onToast]);
|
|
1081
|
+
useEffect(() => {
|
|
1082
|
+
if (acquiredDraftStorage.status === "unavailable")
|
|
1083
|
+
scope.interactions.report({
|
|
1084
|
+
tone: "info",
|
|
1085
|
+
message: "Drafts are session-only because browser storage is unavailable. They will be lost when this conversation closes.",
|
|
1086
|
+
});
|
|
1087
|
+
}, [scope, acquiredDraftStorage]);
|
|
1088
|
+
useEffect(() => {
|
|
1089
|
+
const reconcile = () => {
|
|
1090
|
+
scope.store.set(scope.atoms.reconcileQueuedCancellationAtom);
|
|
1091
|
+
};
|
|
1092
|
+
const unsubscribe = scope.store.sub(scope.atoms.snapshotAtom, reconcile);
|
|
1093
|
+
reconcile();
|
|
1094
|
+
return unsubscribe;
|
|
1095
|
+
}, [scope]);
|
|
635
1096
|
useEffect(() => {
|
|
636
1097
|
// The live page is bounded: after a later reset an observed command may no
|
|
637
1098
|
// longer be present. Retire its local row once, not just while it is visible.
|
|
@@ -674,21 +1135,52 @@ export function useTimeline() {
|
|
|
674
1135
|
const loadNewerHistory = useStoreSetter(atoms.loadNewerHistoryAtom);
|
|
675
1136
|
const jumpToLatest = useStoreSetter(atoms.jumpToLatestAtom);
|
|
676
1137
|
const pinVisibleHistory = useStoreSetter(atoms.pinVisibleHistoryAtom);
|
|
1138
|
+
const cancelQueued = useStoreSetter(atoms.cancelQueuedAtom);
|
|
1139
|
+
const { shown } = useConversationScope();
|
|
1140
|
+
// View-only: stored prompts, queue counts and fork stay as loaded.
|
|
1141
|
+
const settled = useMemo(() => base.settled.filter(shown), [base.settled, shown]);
|
|
1142
|
+
const tail = useMemo(() => base.tail.filter(shown), [base.tail, shown]);
|
|
1143
|
+
const view = useMemo(() => ({ ...base, settled, tail }), [base, settled, tail]);
|
|
1144
|
+
const hidingOnly = view.settled.length + view.tail.length === 0 &&
|
|
1145
|
+
base.settled.length + base.tail.length > 0;
|
|
1146
|
+
const needsOlder = hidingOnly && base.hasMoreHistory && !base.loadingOlderHistory;
|
|
1147
|
+
// One automatic request per loaded page: a failed fetch leaves the page
|
|
1148
|
+
// unchanged, so it is not retried until the reader scrolls up.
|
|
1149
|
+
const pageKey = `${base.settled[0]?.id ?? ""}:${String(base.settled.length)}`;
|
|
1150
|
+
const requestedPage = useRef(undefined);
|
|
1151
|
+
useEffect(() => {
|
|
1152
|
+
if (!needsOlder || requestedPage.current === pageKey)
|
|
1153
|
+
return;
|
|
1154
|
+
requestedPage.current = pageKey;
|
|
1155
|
+
loadOlderHistory();
|
|
1156
|
+
}, [needsOlder, pageKey, loadOlderHistory]);
|
|
677
1157
|
return useMemo(() => ({
|
|
678
|
-
...
|
|
1158
|
+
...view,
|
|
1159
|
+
cancelQueued,
|
|
679
1160
|
jumpToLatest,
|
|
680
1161
|
loadNewerHistory,
|
|
681
1162
|
loadOlderHistory,
|
|
682
1163
|
pinVisibleHistory,
|
|
683
|
-
}), [
|
|
1164
|
+
}), [
|
|
1165
|
+
view,
|
|
1166
|
+
cancelQueued,
|
|
1167
|
+
jumpToLatest,
|
|
1168
|
+
loadNewerHistory,
|
|
1169
|
+
loadOlderHistory,
|
|
1170
|
+
pinVisibleHistory,
|
|
1171
|
+
]);
|
|
684
1172
|
}
|
|
685
1173
|
/** Read and mutate the current conversation draft and prompt. */
|
|
686
1174
|
export function useComposer() {
|
|
687
1175
|
const { atoms } = useConversationScope();
|
|
688
1176
|
const draft = useStoreAtom(atoms.draftAtom);
|
|
1177
|
+
const images = useStoreAtom(atoms.draftImagesAtom);
|
|
1178
|
+
const recallState = useStoreAtom(atoms.recallStateAtom);
|
|
1179
|
+
const recall = useStoreSetter(atoms.recallAtom);
|
|
689
1180
|
const failure = useStoreAtom(atoms.failureAtom);
|
|
690
1181
|
const admissionError = useStoreAtom(atoms.admissionErrorAtom);
|
|
691
1182
|
const setDraft = useStoreSetter(atoms.setDraftAtom);
|
|
1183
|
+
const checkpointDraft = useStoreSetter(atoms.checkpointDraftAtom);
|
|
692
1184
|
const send = useStoreSetter(atoms.sendAtom);
|
|
693
1185
|
const submitCommand = useStoreSetter(atoms.submitCommandAtom);
|
|
694
1186
|
const retry = useStoreSetter(atoms.retryAtom);
|
|
@@ -700,7 +1192,11 @@ export function useComposer() {
|
|
|
700
1192
|
return useMemo(() => ({
|
|
701
1193
|
admissionError,
|
|
702
1194
|
cancel,
|
|
1195
|
+
checkpointDraft,
|
|
703
1196
|
draft,
|
|
1197
|
+
images,
|
|
1198
|
+
recall,
|
|
1199
|
+
recallState,
|
|
704
1200
|
dismissFailed,
|
|
705
1201
|
failure,
|
|
706
1202
|
retry: retryLatest,
|
|
@@ -710,7 +1206,11 @@ export function useComposer() {
|
|
|
710
1206
|
}), [
|
|
711
1207
|
admissionError,
|
|
712
1208
|
cancel,
|
|
1209
|
+
checkpointDraft,
|
|
713
1210
|
draft,
|
|
1211
|
+
images,
|
|
1212
|
+
recall,
|
|
1213
|
+
recallState,
|
|
714
1214
|
dismissFailed,
|
|
715
1215
|
failure,
|
|
716
1216
|
retryLatest,
|