@fungi.computer/parakeet 0.0.0-stage → 0.1.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.
Files changed (115) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +186 -2
  3. package/dist/account-commands.d.ts +102 -0
  4. package/dist/account-commands.js +239 -0
  5. package/dist/agent-head.d.ts +4 -0
  6. package/dist/agent-head.js +2 -0
  7. package/dist/client-contributions.d.ts +20 -0
  8. package/dist/client-contributions.js +41 -0
  9. package/dist/commands/advertised.d.ts +23 -0
  10. package/dist/commands/advertised.js +72 -0
  11. package/dist/commands/builtins.d.ts +14 -0
  12. package/dist/commands/builtins.js +91 -0
  13. package/dist/commands/hints.d.ts +36 -0
  14. package/dist/commands/hints.js +64 -0
  15. package/dist/commands/interactions.d.ts +55 -0
  16. package/dist/commands/interactions.js +101 -0
  17. package/dist/commands/run.d.ts +12 -0
  18. package/dist/commands/run.js +64 -0
  19. package/dist/commands/slash.d.ts +126 -0
  20. package/dist/commands/slash.js +190 -0
  21. package/dist/commands/use-slash-commands.d.ts +57 -0
  22. package/dist/commands/use-slash-commands.js +207 -0
  23. package/dist/components/AgentHead.d.ts +22 -0
  24. package/dist/components/AgentHead.js +20 -0
  25. package/dist/components/ChatComposer.d.ts +43 -0
  26. package/dist/components/ChatComposer.js +209 -0
  27. package/dist/components/ChatMessage.d.ts +8 -0
  28. package/dist/components/ChatMessage.js +16 -0
  29. package/dist/components/ClientRows.d.ts +11 -0
  30. package/dist/components/ClientRows.js +46 -0
  31. package/dist/components/CodeBlock.d.ts +11 -0
  32. package/dist/components/CodeBlock.js +38 -0
  33. package/dist/components/InteractionPanel.d.ts +15 -0
  34. package/dist/components/InteractionPanel.js +42 -0
  35. package/dist/components/MarkdownRenderer.d.ts +6 -0
  36. package/dist/components/MarkdownRenderer.js +48 -0
  37. package/dist/components/RunFailureNotice.d.ts +7 -0
  38. package/dist/components/RunFailureNotice.js +13 -0
  39. package/dist/components/ShiitakeAvatar.d.ts +21 -0
  40. package/dist/components/ShiitakeAvatar.js +22 -0
  41. package/dist/components/SlashMenu.d.ts +8 -0
  42. package/dist/components/SlashMenu.js +50 -0
  43. package/dist/components/ThinkingBlock.d.ts +3 -0
  44. package/dist/components/ThinkingBlock.js +18 -0
  45. package/dist/components/TimelineItem.d.ts +15 -0
  46. package/dist/components/TimelineItem.js +35 -0
  47. package/dist/components/ToolGroup.d.ts +12 -0
  48. package/dist/components/ToolGroup.js +90 -0
  49. package/dist/components/WaitingNotice.d.ts +7 -0
  50. package/dist/components/WaitingNotice.js +8 -0
  51. package/dist/components/avatar-presentation.d.ts +16 -0
  52. package/dist/components/avatar-presentation.js +9 -0
  53. package/dist/components/sprite-service.d.ts +2 -0
  54. package/dist/components/sprite-service.js +9 -0
  55. package/dist/conversation.d.ts +165 -0
  56. package/dist/conversation.js +762 -0
  57. package/dist/core/assistant-visibility.d.ts +3 -0
  58. package/dist/core/assistant-visibility.js +4 -0
  59. package/dist/core/author-labels.d.ts +16 -0
  60. package/dist/core/author-labels.js +16 -0
  61. package/dist/core/build-timeline.d.ts +36 -0
  62. package/dist/core/build-timeline.js +328 -0
  63. package/dist/core/client-actions.d.ts +35 -0
  64. package/dist/core/client-actions.js +128 -0
  65. package/dist/core/client-contributions.d.ts +24 -0
  66. package/dist/core/client-contributions.js +115 -0
  67. package/dist/core/client-timeline.d.ts +33 -0
  68. package/dist/core/client-timeline.js +72 -0
  69. package/dist/core/format-context.d.ts +11 -0
  70. package/dist/core/format-context.js +20 -0
  71. package/dist/core/format-tokens.d.ts +2 -0
  72. package/dist/core/format-tokens.js +6 -0
  73. package/dist/core/model-identity.d.ts +3 -0
  74. package/dist/core/model-identity.js +7 -0
  75. package/dist/core/session-projection.d.ts +11 -0
  76. package/dist/core/session-projection.js +191 -0
  77. package/dist/core/session-query.d.ts +118 -0
  78. package/dist/core/session-query.js +187 -0
  79. package/dist/core/session-view-state.d.ts +66 -0
  80. package/dist/core/session-view-state.js +114 -0
  81. package/dist/core/tool-contract.d.ts +15 -0
  82. package/dist/core/tool-contract.js +3 -0
  83. package/dist/core/tool-groups.d.ts +36 -0
  84. package/dist/core/tool-groups.js +151 -0
  85. package/dist/core/tool-view.d.ts +182 -0
  86. package/dist/core/tool-view.js +618 -0
  87. package/dist/headless.d.ts +28 -0
  88. package/dist/headless.js +24 -0
  89. package/dist/index.d.ts +9 -0
  90. package/dist/index.js +4 -0
  91. package/dist/lib/failure.d.ts +2 -0
  92. package/dist/lib/failure.js +9 -0
  93. package/dist/markdown.d.ts +3 -0
  94. package/dist/markdown.js +6 -0
  95. package/dist/model-commands.d.ts +44 -0
  96. package/dist/model-commands.js +232 -0
  97. package/dist/parakeet.d.ts +77 -0
  98. package/dist/parakeet.js +276 -0
  99. package/dist/provider-choices.d.ts +8 -0
  100. package/dist/provider-choices.js +46 -0
  101. package/dist/reload-command.d.ts +43 -0
  102. package/dist/reload-command.js +72 -0
  103. package/dist/session-commands.d.ts +53 -0
  104. package/dist/session-commands.js +259 -0
  105. package/dist/session.d.ts +24 -0
  106. package/dist/session.js +1 -0
  107. package/dist/store/types.d.ts +163 -0
  108. package/dist/store/types.js +1 -0
  109. package/dist/styles.css +19129 -0
  110. package/docs/api/README.md +15 -0
  111. package/docs/api/commands/slash.md +321 -0
  112. package/docs/api/headless.md +2510 -0
  113. package/docs/api/index.md +1249 -0
  114. package/package.json +79 -3
  115. package/src/styles.css +272 -0
@@ -0,0 +1,762 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { noAuthor } from "./core/author-labels.js";
3
+ import { nanoid } from "@fungi.computer/cairn";
4
+ import {} from "@fungi.computer/shiitake/client";
5
+ import { createClientContributions, } from "./core/client-contributions.js";
6
+ import { createParakeetInteractions, } from "./commands/interactions.js";
7
+ import { CancelledError, useQueryClient, } from "@tanstack/react-query";
8
+ import { atom, createStore, } from "jotai/vanilla";
9
+ import { atomWithInfiniteQuery, atomWithMutation, atomWithQuery, } from "jotai-tanstack-query";
10
+ import { createContext, useCallback, useContext, useEffect, useMemo, useSyncExternalStore, } from "react";
11
+ import { buildDurableTimeline, buildTimelineTail, } from "./core/build-timeline.js";
12
+ import { mergeCompactionNotices, mergeCompactions, mergeMessages, } from "./core/session-projection.js";
13
+ import { sessionHistoryQueryOptions, sessionQueryOptions, } from "./core/session-query.js";
14
+ import { sameSessionViewState, sessionActivityStatus, sessionViewState, } from "./core/session-view-state.js";
15
+ import { isCancelledOutcome } from "./commands/run.js";
16
+ import { reportFailure } from "./lib/failure.js";
17
+ const noMessages = Object.freeze([]);
18
+ const noCompactions = Object.freeze([]);
19
+ const noCompactionNotices = Object.freeze([]);
20
+ const noClients = Object.freeze([]);
21
+ const noContributions = Object.freeze([]);
22
+ const noSlots = Object.freeze({});
23
+ const noPending = Object.freeze({
24
+ total: 0,
25
+ hasMore: false,
26
+ prompts: Object.freeze([]),
27
+ });
28
+ const connecting = Object.freeze({ status: "connecting" });
29
+ const live = Object.freeze({ status: "live" });
30
+ const MAX_UNRESOLVED_LOCAL_ADMISSIONS = 32;
31
+ function errorMessage(cause) {
32
+ return cause instanceof Error && cause.message.trim().length > 0
33
+ ? cause.message
34
+ : "Prompt admission failed";
35
+ }
36
+ function observationCause(cause) {
37
+ const message = cause instanceof Error && cause.message.trim().length > 0
38
+ ? cause.message.trim()
39
+ : "Session history is unavailable";
40
+ return message.slice(0, 500);
41
+ }
42
+ /**
43
+ * Show Shiitake's Session status as-is; local admission state only refines an
44
+ * owner-reported `idle`.
45
+ */
46
+ function composerStatus(snapshot, admissions, failure) {
47
+ if (snapshot === undefined)
48
+ return "connecting";
49
+ const view = sessionViewState(snapshot);
50
+ const status = sessionActivityStatus(view);
51
+ if (status !== "idle")
52
+ return status;
53
+ if (failure !== undefined || view.kind === "error")
54
+ return "error";
55
+ if (admissions.some(({ status: state }) => state === "accepted"))
56
+ return "queued";
57
+ if (admissions.some(({ status: state }) => state === "admitting"))
58
+ return "sending";
59
+ return status;
60
+ }
61
+ function newestDurableCommandId(messages) {
62
+ if (messages === undefined)
63
+ return undefined;
64
+ let newest;
65
+ for (const message of messages) {
66
+ if (message.value.role !== "user" || message.commandId === null)
67
+ continue;
68
+ if (newest === undefined || message.sequence > newest.sequence)
69
+ newest = { commandId: message.commandId, sequence: message.sequence };
70
+ }
71
+ return newest?.commandId;
72
+ }
73
+ function newestDurablePrompt(messages, commandId) {
74
+ const message = messages.find((candidate) => candidate.commandId === commandId && candidate.value.role === "user");
75
+ if (message?.value.role !== "user")
76
+ return undefined;
77
+ const content = message.value.content;
78
+ const text = Array.isArray(content)
79
+ ? content
80
+ .flatMap((part) => (part.type === "text" ? [part.text] : []))
81
+ .join("\n")
82
+ : content;
83
+ return text.trim() === "" ? undefined : text;
84
+ }
85
+ function createConversationAtoms(session, queryClient, authorOf) {
86
+ const queryAtom = atomWithQuery(() => sessionQueryOptions(session), () => queryClient);
87
+ const snapshotAtom = atom((get) => get(queryAtom).data?.snapshot);
88
+ const pinnedHistoryAtom = atom(undefined);
89
+ const historyBeforeAtom = atom((get) => (get(pinnedHistoryAtom) ?? get(snapshotAtom))?.oldestCursor ?? null);
90
+ const historyAtom = atomWithInfiniteQuery((get) => sessionHistoryQueryOptions(session, get(historyBeforeAtom)), () => queryClient);
91
+ const hasMoreHistoryAtom = atom((get) => {
92
+ const history = get(historyAtom);
93
+ return history.data === undefined
94
+ ? get(historyBeforeAtom) !== null
95
+ : history.hasNextPage;
96
+ });
97
+ const loadingOlderHistoryAtom = atom((get) => get(historyAtom).isFetching);
98
+ const hasNewerHistoryAtom = atom((get) => {
99
+ const history = get(historyAtom);
100
+ return history.hasPreviousPage || history.data?.newerOverflow === true;
101
+ });
102
+ const loadOlderHistoryAtom = atom(null, (get) => {
103
+ if (!get(hasMoreHistoryAtom) || get(loadingOlderHistoryAtom))
104
+ return;
105
+ void get(historyAtom)
106
+ .fetchNextPage({ cancelRefetch: false, throwOnError: true })
107
+ .catch((cause) => {
108
+ if (!(cause instanceof CancelledError))
109
+ reportFailure("read older Session history", cause);
110
+ });
111
+ });
112
+ const jumpToLatestAtom = atom(null, (_get, set) => {
113
+ set(pinnedHistoryAtom, undefined);
114
+ void queryClient.resetQueries({
115
+ queryKey: sessionHistoryQueryOptions(session).queryKey,
116
+ exact: true,
117
+ });
118
+ });
119
+ const loadNewerHistoryAtom = atom(null, (get, set) => {
120
+ const history = get(historyAtom);
121
+ if (history.isFetching)
122
+ return;
123
+ if (!history.hasPreviousPage) {
124
+ if (get(hasNewerHistoryAtom))
125
+ set(jumpToLatestAtom);
126
+ return;
127
+ }
128
+ void history
129
+ .fetchPreviousPage({ cancelRefetch: false, throwOnError: true })
130
+ .catch((cause) => {
131
+ if (!(cause instanceof CancelledError))
132
+ reportFailure("read newer Session history", cause);
133
+ });
134
+ });
135
+ const pinVisibleHistoryAtom = atom(null, (get, set) => {
136
+ if (get(pinnedHistoryAtom) === undefined)
137
+ set(pinnedHistoryAtom, get(snapshotAtom));
138
+ });
139
+ const messagesAtom = atom((get) => {
140
+ const history = get(historyAtom);
141
+ const live = get(snapshotAtom)?.messages ?? noMessages;
142
+ const pages = history.data?.pages.flatMap((page) => page.messages) ?? noMessages;
143
+ const pinned = get(pinnedHistoryAtom)?.messages ?? noMessages;
144
+ if (pages.length === 0)
145
+ return pinned.length === 0 ? live : mergeMessages(pinned, live);
146
+ return get(hasNewerHistoryAtom)
147
+ ? pages
148
+ : mergeMessages(mergeMessages(pages, pinned), live);
149
+ });
150
+ const compactionsAtom = atom((get) => {
151
+ const history = get(historyAtom);
152
+ const live = get(snapshotAtom)?.compactions ?? noCompactions;
153
+ const pages = history.data?.pages.flatMap((page) => page.compactions ?? noCompactions) ?? noCompactions;
154
+ const pinned = get(pinnedHistoryAtom)?.compactions ?? noCompactions;
155
+ if (pages.length === 0)
156
+ return pinned.length === 0 ? live : mergeCompactions(pinned, live);
157
+ return get(hasNewerHistoryAtom)
158
+ ? pages
159
+ : mergeCompactions(mergeCompactions(pages, pinned), live);
160
+ });
161
+ const compactionNoticesAtom = atom((get) => {
162
+ const history = get(historyAtom);
163
+ const live = get(snapshotAtom)?.compactionNotices ?? noCompactionNotices;
164
+ const pages = history.data?.pages.flatMap((page) => page.compactionNotices ?? noCompactionNotices) ?? noCompactionNotices;
165
+ const pinned = get(pinnedHistoryAtom)?.compactionNotices ?? noCompactionNotices;
166
+ if (pages.length === 0)
167
+ return pinned.length === 0 ? live : mergeCompactionNotices(pinned, live);
168
+ return get(hasNewerHistoryAtom)
169
+ ? pages
170
+ : mergeCompactionNotices(mergeCompactionNotices(pages, pinned), live);
171
+ });
172
+ const newestCommandAtom = atom((get) => get(queryAtom).data?.latestPrompt?.[0] ??
173
+ newestDurableCommandId(get(messagesAtom)));
174
+ const pendingAtom = atom((get) => get(snapshotAtom)?.pending ?? noPending);
175
+ const extensionsAtom = atom((get) => get(snapshotAtom)?.extensions ?? noSlots);
176
+ let liveToolActivity;
177
+ const toolActivityAtom = atom((get) => {
178
+ const query = get(queryAtom);
179
+ const activity = get(snapshotAtom)?.activity;
180
+ if (query.isError ||
181
+ query.data?.observation !== "live" ||
182
+ activity?.queue.state !== "running" ||
183
+ activity.activeTool === null) {
184
+ liveToolActivity = undefined;
185
+ return undefined;
186
+ }
187
+ if (liveToolActivity?.queue.state === "running" &&
188
+ liveToolActivity.queue.commandId === activity.queue.commandId &&
189
+ liveToolActivity.activeTool === activity.activeTool)
190
+ return liveToolActivity;
191
+ liveToolActivity = activity;
192
+ return liveToolActivity;
193
+ });
194
+ /** Each accepted local prompt's request identity, by its owner command. */
195
+ const promptRequestIdsAtom = atom(new Map());
196
+ const requestIdOfAtom = atom((get) => {
197
+ const requestIds = get(promptRequestIdsAtom);
198
+ return (commandId) => requestIds.get(commandId);
199
+ });
200
+ const durableAtom = atom((get) => buildDurableTimeline(get(messagesAtom), get(compactionsAtom), get(toolActivityAtom), authorOf, get(requestIdOfAtom), get(compactionNoticesAtom)));
201
+ const draftAtom = atom("");
202
+ const draftRevisionAtom = atom(0);
203
+ const rawAdmissionsAtom = atom([]);
204
+ const latestSubmissionIdAtom = atom(undefined);
205
+ const observedCommandIdsAtom = atom((get) => new Set([
206
+ ...get(durableAtom).commandIds,
207
+ ...get(pendingAtom).prompts.map(({ commandId }) => commandId),
208
+ ...(get(snapshotAtom)?.terminals.map(({ commandId }) => commandId) ??
209
+ []),
210
+ ]));
211
+ const admissionsAtom = atom((get) => {
212
+ const knownCommandIds = get(observedCommandIdsAtom);
213
+ return get(rawAdmissionsAtom).filter((admission) => admission.status === "failed" ||
214
+ admission.commandId === undefined ||
215
+ !knownCommandIds.has(admission.commandId));
216
+ });
217
+ /** Retire a successful local admission only after owner identity is observed. */
218
+ const reconcileAdmissionsAtom = atom(null, (get, set) => {
219
+ const knownCommandIds = get(observedCommandIdsAtom);
220
+ set(rawAdmissionsAtom, (current) => {
221
+ const unresolved = current.filter(({ status, commandId }) => status !== "accepted" ||
222
+ commandId === undefined ||
223
+ !knownCommandIds.has(commandId));
224
+ return unresolved.length === current.length ? current : unresolved;
225
+ });
226
+ });
227
+ const admissionErrorAtom = atom(undefined);
228
+ const setDraftAtom = atom(null, (get, set, value) => {
229
+ if (get(draftAtom) !== value)
230
+ set(draftRevisionAtom, (revision) => revision + 1);
231
+ set(draftAtom, value);
232
+ set(admissionErrorAtom, undefined);
233
+ });
234
+ /** The one owner of "clear the draft after submit" for prompts and commands. */
235
+ const clearSubmittedDraftAtom = atom(null, (get, set, submitted) => {
236
+ if (get(draftRevisionAtom) !== submitted.revision)
237
+ return;
238
+ set(draftAtom, (current) => (current === submitted.draft ? "" : current));
239
+ });
240
+ const abortAtom = atomWithMutation(() => ({
241
+ mutationFn: (commandId) => session.command({ type: "cancel", commandId }),
242
+ }), () => queryClient);
243
+ const tailAtom = atom((get) => {
244
+ if (get(hasNewerHistoryAtom))
245
+ return [];
246
+ const snapshot = get(snapshotAtom);
247
+ const pending = get(pendingAtom);
248
+ return buildTimelineTail(get(durableAtom), snapshot?.progress ?? null, pending.prompts, get(admissionsAtom), authorOf, get(requestIdOfAtom));
249
+ });
250
+ const timelineAtom = atom((get) => {
251
+ const durable = get(durableAtom);
252
+ const base = {
253
+ pendingHasMore: get(pendingAtom).hasMore,
254
+ queued: get(pendingAtom),
255
+ commandAnchors: durable.commandAnchors,
256
+ hasMoreHistory: get(hasMoreHistoryAtom),
257
+ hasNewerHistory: get(hasNewerHistoryAtom),
258
+ loadingOlderHistory: get(loadingOlderHistoryAtom),
259
+ settled: durable.items,
260
+ tail: get(tailAtom),
261
+ };
262
+ return durable.model === undefined || durable.usage === undefined
263
+ ? base
264
+ : { ...base, model: durable.model, usage: durable.usage };
265
+ });
266
+ const failureAtom = atom((get) => {
267
+ const admissions = get(admissionsAtom);
268
+ const snapshot = get(snapshotAtom);
269
+ const pending = get(pendingAtom);
270
+ const latestSubmission = get(rawAdmissionsAtom).find(({ requestId }) => requestId === get(latestSubmissionIdAtom));
271
+ const promptPending = pending.prompts.length > 0 ||
272
+ admissions.some(({ status }) => status !== "failed");
273
+ if (latestSubmission?.status === "failed" &&
274
+ snapshot !== undefined &&
275
+ sessionActivityStatus(sessionViewState(snapshot)) === "idle" &&
276
+ !promptPending)
277
+ return "admission";
278
+ if (latestSubmission?.status === "admitting" ||
279
+ (latestSubmission?.status === "accepted" &&
280
+ !get(durableAtom).commandIds.has(latestSubmission.commandId ?? "") &&
281
+ !pending.prompts.some(({ commandId }) => commandId === latestSubmission.commandId) &&
282
+ !snapshot?.terminals.some(({ commandId }) => commandId === latestSubmission.commandId)))
283
+ return undefined;
284
+ if (promptPending)
285
+ return undefined;
286
+ const newestCommandId = get(newestCommandAtom);
287
+ const outcome = snapshot?.terminals.find(({ commandId }) => commandId === newestCommandId)?.outcome;
288
+ return outcome === "completed" ? undefined : outcome;
289
+ });
290
+ const observationAtom = atom((get) => {
291
+ const query = get(queryAtom);
292
+ if (query.isError)
293
+ return {
294
+ status: "unavailable",
295
+ reason: query.error instanceof Error && query.error.message.trim().length > 0
296
+ ? query.error.message
297
+ : "Session history is unavailable",
298
+ };
299
+ return query.data?.observation === "live" ? live : connecting;
300
+ });
301
+ const statusAtom = atom((get) => {
302
+ if (get(observationAtom).status === "unavailable")
303
+ return "disconnected";
304
+ if (get(observationAtom).status === "connecting")
305
+ return "connecting";
306
+ return composerStatus(get(snapshotAtom), get(admissionsAtom), get(failureAtom));
307
+ });
308
+ const contextAtom = atom((get) => get(snapshotAtom)?.context);
309
+ const commandIdAtom = atom((get) => {
310
+ const queue = get(snapshotAtom)?.activity.queue;
311
+ return queue?.state === "idle" ? undefined : queue?.commandId;
312
+ });
313
+ let ownerView;
314
+ const viewStateAtom = atom((get) => {
315
+ const observation = get(observationAtom);
316
+ if (observation.status === "unavailable")
317
+ return { kind: "disconnected", cause: observation.reason };
318
+ if (observation.status === "connecting")
319
+ return { kind: "connecting" };
320
+ if (get(failureAtom) === "admission") {
321
+ const failed = [...get(rawAdmissionsAtom)]
322
+ .reverse()
323
+ .find((admission) => admission.status === "failed");
324
+ return {
325
+ kind: "error",
326
+ cause: (failed?.error ?? "Prompt admission failed").slice(0, 200),
327
+ };
328
+ }
329
+ const snapshot = get(snapshotAtom);
330
+ if (snapshot === undefined)
331
+ return { kind: "connecting" };
332
+ const next = sessionViewState(snapshot);
333
+ // Keep the readout stable while its meaning is unchanged.
334
+ if (ownerView !== undefined && sameSessionViewState(ownerView, next))
335
+ return ownerView;
336
+ ownerView = next;
337
+ return next;
338
+ });
339
+ const dispatchAdmissionAtom = atom(null, (get, set, admission) => {
340
+ void session
341
+ .command({
342
+ type: "prompt",
343
+ text: admission.content,
344
+ requestId: admission.requestId,
345
+ delivery: admission.delivery,
346
+ })
347
+ .then((accepted) => {
348
+ if (accepted.status === "recorded") {
349
+ set(rawAdmissionsAtom, (current) => current.filter((candidate) => candidate.requestId !== admission.requestId));
350
+ set(clearSubmittedDraftAtom, {
351
+ draft: admission.submittedDraft,
352
+ revision: admission.submittedDraftRevision,
353
+ });
354
+ return;
355
+ }
356
+ set(promptRequestIdsAtom, (current) => new Map(current).set(accepted.commandId, admission.requestId));
357
+ set(rawAdmissionsAtom, (current) => current.map((candidate) => candidate.requestId === admission.requestId
358
+ ? {
359
+ requestId: candidate.requestId,
360
+ content: candidate.content,
361
+ submittedDraft: candidate.submittedDraft,
362
+ submittedDraftRevision: candidate.submittedDraftRevision,
363
+ delivery: candidate.delivery,
364
+ commandId: accepted.commandId,
365
+ status: "accepted",
366
+ }
367
+ : 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
+ set(reconcileAdmissionsAtom);
375
+ if (!hasNewerMatchingAdmission)
376
+ set(clearSubmittedDraftAtom, {
377
+ draft: admission.submittedDraft,
378
+ revision: admission.submittedDraftRevision,
379
+ });
380
+ })
381
+ .catch((cause) => {
382
+ set(rawAdmissionsAtom, (current) => current.map((candidate) => candidate.requestId === admission.requestId
383
+ ? {
384
+ ...candidate,
385
+ status: "failed",
386
+ error: errorMessage(cause),
387
+ }
388
+ : candidate));
389
+ });
390
+ });
391
+ const sendAtom = atom(null, (get, set, delivery = "steer") => {
392
+ const draft = get(draftAtom);
393
+ const content = draft.trim();
394
+ if (content === "")
395
+ return;
396
+ const admissions = get(admissionsAtom);
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))
402
+ return;
403
+ const unresolved = admissions.filter(({ status }) => status !== "failed");
404
+ if (unresolved.length >= MAX_UNRESOLVED_LOCAL_ADMISSIONS) {
405
+ set(admissionErrorAtom, "queue_full");
406
+ return;
407
+ }
408
+ const admission = {
409
+ content,
410
+ delivery,
411
+ requestId: nanoid(),
412
+ submittedDraft: draft,
413
+ submittedDraftRevision: draftRevision,
414
+ status: "admitting",
415
+ };
416
+ set(rawAdmissionsAtom, [...admissions, admission]);
417
+ set(latestSubmissionIdAtom, admission.requestId);
418
+ set(admissionErrorAtom, undefined);
419
+ set(dispatchAdmissionAtom, admission);
420
+ });
421
+ const submitCommandAtom = atom(null, async (get, set, run) => {
422
+ const submitted = {
423
+ draft: get(draftAtom),
424
+ revision: get(draftRevisionAtom),
425
+ };
426
+ const outcome = await run(submitted.draft, () => {
427
+ set(clearSubmittedDraftAtom, submitted);
428
+ });
429
+ if (outcome?.status === "handled" && !isCancelledOutcome(outcome))
430
+ set(clearSubmittedDraftAtom, submitted);
431
+ return outcome;
432
+ });
433
+ const retryAtom = atom(null, (get, set) => {
434
+ const admissions = get(admissionsAtom);
435
+ if (get(failureAtom) === "interrupted" || get(failureAtom) === "failed") {
436
+ const snapshot = get(snapshotAtom);
437
+ const commandId = get(newestCommandAtom);
438
+ if (snapshot === undefined || commandId === undefined)
439
+ return;
440
+ const content = newestDurablePrompt(snapshot.messages, commandId) ??
441
+ get(queryAtom).data?.latestPrompt?.[1];
442
+ if (content === undefined)
443
+ return;
444
+ if (admissions.filter(({ status }) => status !== "failed").length >=
445
+ MAX_UNRESOLVED_LOCAL_ADMISSIONS) {
446
+ set(admissionErrorAtom, "queue_full");
447
+ return;
448
+ }
449
+ const retry = {
450
+ content,
451
+ delivery: "followUp",
452
+ requestId: nanoid(),
453
+ submittedDraft: content,
454
+ submittedDraftRevision: get(draftRevisionAtom),
455
+ status: "admitting",
456
+ };
457
+ set(rawAdmissionsAtom, [...get(rawAdmissionsAtom), retry]);
458
+ set(latestSubmissionIdAtom, retry.requestId);
459
+ set(admissionErrorAtom, undefined);
460
+ set(dispatchAdmissionAtom, retry);
461
+ return;
462
+ }
463
+ const admission = admissions
464
+ .filter((candidate) => candidate.status === "failed")
465
+ .at(-1);
466
+ if (admission === undefined)
467
+ return;
468
+ if (admissions.filter(({ status }) => status !== "failed").length >=
469
+ MAX_UNRESOLVED_LOCAL_ADMISSIONS) {
470
+ set(admissionErrorAtom, "queue_full");
471
+ return;
472
+ }
473
+ const retry = {
474
+ content: admission.content,
475
+ delivery: admission.delivery,
476
+ requestId: admission.requestId,
477
+ submittedDraft: admission.submittedDraft,
478
+ submittedDraftRevision: admission.submittedDraftRevision,
479
+ status: "admitting",
480
+ };
481
+ set(rawAdmissionsAtom, (current) => current.map((candidate) => candidate.requestId === admission.requestId ? retry : candidate));
482
+ set(latestSubmissionIdAtom, retry.requestId);
483
+ set(admissionErrorAtom, undefined);
484
+ set(dispatchAdmissionAtom, retry);
485
+ });
486
+ const dismissFailedAtom = atom(null, (get, set, requestId) => {
487
+ const admissions = get(rawAdmissionsAtom);
488
+ const remaining = admissions.filter((admission) => admission.requestId !== requestId || admission.status !== "failed");
489
+ if (remaining.length === admissions.length)
490
+ return;
491
+ set(rawAdmissionsAtom, remaining);
492
+ if (get(latestSubmissionIdAtom) === requestId)
493
+ set(latestSubmissionIdAtom, remaining.at(-1)?.requestId);
494
+ });
495
+ let cancellation;
496
+ const cancelAtom = atom(null, (get) => {
497
+ if (cancellation !== undefined)
498
+ return cancellation;
499
+ const queue = get(snapshotAtom)?.activity.queue;
500
+ if (queue?.state !== "running")
501
+ return Promise.resolve();
502
+ const operation = get(abortAtom)
503
+ .mutateAsync(queue.commandId)
504
+ .then(() => undefined);
505
+ const tracked = operation.finally(() => {
506
+ if (cancellation === tracked)
507
+ cancellation = undefined;
508
+ });
509
+ cancellation = tracked;
510
+ return tracked;
511
+ });
512
+ let reconnecting;
513
+ const reconnectAtom = atom(null, (get) => {
514
+ const observation = get(observationAtom);
515
+ if (observation.status !== "unavailable")
516
+ return Promise.resolve({
517
+ kind: "nothing_to_reconnect",
518
+ state: observation.status === "live" ? "connected" : "connecting",
519
+ });
520
+ if (reconnecting !== undefined)
521
+ return reconnecting;
522
+ const query = queryClient.getQueryCache().find({
523
+ queryKey: sessionQueryOptions(session).queryKey,
524
+ });
525
+ if (query === undefined)
526
+ return Promise.resolve({
527
+ kind: "failed",
528
+ cause: "Session observation is unavailable",
529
+ });
530
+ const priorData = query.state.dataUpdateCount;
531
+ const priorError = query.state.errorUpdateCount;
532
+ const retry = new Promise((resolve) => {
533
+ let unsubscribe = () => undefined;
534
+ let settled = false;
535
+ const finish = (outcome) => {
536
+ if (settled)
537
+ return;
538
+ settled = true;
539
+ unsubscribe();
540
+ clearTimeout(timeout);
541
+ resolve(outcome);
542
+ };
543
+ const inspect = () => {
544
+ if (query.state.dataUpdateCount > priorData &&
545
+ queryClient.getQueryData(sessionQueryOptions(session).queryKey)
546
+ ?.observation === "live") {
547
+ finish({ kind: "reconnected" });
548
+ return;
549
+ }
550
+ if (query.state.errorUpdateCount > priorError)
551
+ finish({
552
+ kind: "failed",
553
+ cause: observationCause(query.state.error),
554
+ });
555
+ };
556
+ unsubscribe = queryClient.getQueryCache().subscribe((event) => {
557
+ if (event.query === query)
558
+ inspect();
559
+ });
560
+ const timeout = setTimeout(() => {
561
+ finish({
562
+ kind: "failed",
563
+ cause: "Session history did not reopen within 10 seconds",
564
+ });
565
+ }, 10_000);
566
+ // A live stream does not settle its refetch promise until the watch closes.
567
+ // The first new snapshot above is the reconnect acknowledgment.
568
+ void get(queryAtom)
569
+ .refetch({ cancelRefetch: true })
570
+ .catch((cause) => {
571
+ finish({ kind: "failed", cause: observationCause(cause) });
572
+ });
573
+ inspect();
574
+ });
575
+ reconnecting = retry.finally(() => {
576
+ reconnecting = undefined;
577
+ });
578
+ return reconnecting;
579
+ });
580
+ return {
581
+ abortAtom,
582
+ commandIdAtom,
583
+ contextAtom,
584
+ admissionErrorAtom,
585
+ admissionsAtom,
586
+ cancelAtom,
587
+ draftAtom,
588
+ dismissFailedAtom,
589
+ extensionsAtom,
590
+ failureAtom,
591
+ jumpToLatestAtom,
592
+ loadOlderHistoryAtom,
593
+ loadNewerHistoryAtom,
594
+ observationAtom,
595
+ observedCommandIdsAtom,
596
+ pinVisibleHistoryAtom,
597
+ queryAtom,
598
+ reconcileAdmissionsAtom,
599
+ reconnectAtom,
600
+ retryAtom,
601
+ setDraftAtom,
602
+ sendAtom,
603
+ snapshotAtom,
604
+ statusAtom,
605
+ viewStateAtom,
606
+ submitCommandAtom,
607
+ timelineAtom,
608
+ };
609
+ }
610
+ const ConversationContext = createContext(null);
611
+ function useConversationScope() {
612
+ const scope = useContext(ConversationContext);
613
+ if (scope === null)
614
+ throw new Error("Parakeet hooks require a ParakeetProvider");
615
+ return scope;
616
+ }
617
+ function useStoreAtom(atom) {
618
+ const { store } = useConversationScope();
619
+ const subscribe = useCallback((listener) => store.sub(atom, listener), [atom, store]);
620
+ const snapshot = useCallback(() => store.get(atom), [atom, store]);
621
+ return useSyncExternalStore(subscribe, snapshot, snapshot);
622
+ }
623
+ function useStoreSetter(atom) {
624
+ const { store } = useConversationScope();
625
+ return useCallback((...args) => store.set(atom, ...args), [atom, store]);
626
+ }
627
+ /** Own one renderer-neutral conversation scope over the existing query cache. */
628
+ export function ParakeetProvider({ children, session, clients = noClients, interactionStore, authorOf = noAuthor, }) {
629
+ const queryClient = useQueryClient();
630
+ const scope = useMemo(() => ({
631
+ atoms: createConversationAtoms(session, queryClient, authorOf),
632
+ session,
633
+ store: createStore(),
634
+ contributions: atom(noContributions),
635
+ interactions: interactionStore ?? createParakeetInteractions(),
636
+ }), [queryClient, session, interactionStore, authorOf]);
637
+ useEffect(() => {
638
+ // The live page is bounded: after a later reset an observed command may no
639
+ // longer be present. Retire its local row once, not just while it is visible.
640
+ const reconcile = () => {
641
+ scope.store.set(scope.atoms.reconcileAdmissionsAtom);
642
+ };
643
+ const unobserve = scope.store.sub(scope.atoms.observedCommandIdsAtom, reconcile);
644
+ reconcile();
645
+ return () => {
646
+ unobserve();
647
+ };
648
+ }, [scope]);
649
+ useEffect(() => {
650
+ const owner = createClientContributions(clients, (entryId, error) => {
651
+ reportFailure(`client contribution ${entryId}`, error);
652
+ scope.interactions.report({ tone: "error", message: error.message });
653
+ });
654
+ const publish = () => {
655
+ scope.store.set(scope.contributions, owner.snapshot());
656
+ };
657
+ const unsubscribe = owner.subscribe(publish);
658
+ const update = () => {
659
+ owner.update(scope.store.get(scope.atoms.extensionsAtom));
660
+ };
661
+ const unobserve = scope.store.sub(scope.atoms.extensionsAtom, update);
662
+ update();
663
+ return () => {
664
+ unobserve();
665
+ owner.dispose();
666
+ unsubscribe();
667
+ };
668
+ }, [clients, scope]);
669
+ return (_jsx(ConversationContext.Provider, { value: scope, children: children }));
670
+ }
671
+ /** Read stable history and replaceable live work. */
672
+ export function useTimeline() {
673
+ const { atoms } = useConversationScope();
674
+ const base = useStoreAtom(atoms.timelineAtom);
675
+ const loadOlderHistory = useStoreSetter(atoms.loadOlderHistoryAtom);
676
+ const loadNewerHistory = useStoreSetter(atoms.loadNewerHistoryAtom);
677
+ const jumpToLatest = useStoreSetter(atoms.jumpToLatestAtom);
678
+ const pinVisibleHistory = useStoreSetter(atoms.pinVisibleHistoryAtom);
679
+ return useMemo(() => ({
680
+ ...base,
681
+ jumpToLatest,
682
+ loadNewerHistory,
683
+ loadOlderHistory,
684
+ pinVisibleHistory,
685
+ }), [base, jumpToLatest, loadNewerHistory, loadOlderHistory, pinVisibleHistory]);
686
+ }
687
+ /** Read and mutate the current conversation draft and prompt. */
688
+ export function useComposer() {
689
+ const { atoms } = useConversationScope();
690
+ const draft = useStoreAtom(atoms.draftAtom);
691
+ const failure = useStoreAtom(atoms.failureAtom);
692
+ const admissionError = useStoreAtom(atoms.admissionErrorAtom);
693
+ const setDraft = useStoreSetter(atoms.setDraftAtom);
694
+ const send = useStoreSetter(atoms.sendAtom);
695
+ const submitCommand = useStoreSetter(atoms.submitCommandAtom);
696
+ const retry = useStoreSetter(atoms.retryAtom);
697
+ const retryLatest = useCallback(() => {
698
+ retry();
699
+ }, [retry]);
700
+ const dismissFailed = useStoreSetter(atoms.dismissFailedAtom);
701
+ const cancel = useStoreSetter(atoms.cancelAtom);
702
+ return useMemo(() => ({
703
+ admissionError,
704
+ cancel,
705
+ draft,
706
+ dismissFailed,
707
+ failure,
708
+ retry: retryLatest,
709
+ send,
710
+ setDraft,
711
+ submitCommand,
712
+ }), [
713
+ admissionError,
714
+ cancel,
715
+ draft,
716
+ dismissFailed,
717
+ failure,
718
+ retryLatest,
719
+ send,
720
+ setDraft,
721
+ submitCommand,
722
+ ]);
723
+ }
724
+ /** Dismiss a failed prompt without subscribing the timeline to composer edits. */
725
+ export function useDismissFailedPrompt() {
726
+ const { atoms } = useConversationScope();
727
+ return useStoreSetter(atoms.dismissFailedAtom);
728
+ }
729
+ /** The Session this conversation observes. */
730
+ export function useParakeetSession() {
731
+ return useConversationScope().session;
732
+ }
733
+ /** Read the Session's extension slots, including advertised command catalogs. */
734
+ export function useExtensionSlots() {
735
+ return useStoreAtom(useConversationScope().atoms.extensionsAtom);
736
+ }
737
+ /** Read Session activity and operate its observation. */
738
+ export function useStatus() {
739
+ const { atoms } = useConversationScope();
740
+ const commandId = useStoreAtom(atoms.commandIdAtom);
741
+ const status = useStoreAtom(atoms.statusAtom);
742
+ const viewState = useStoreAtom(atoms.viewStateAtom);
743
+ const observation = useStoreAtom(atoms.observationAtom);
744
+ const reconnect = useStoreSetter(atoms.reconnectAtom);
745
+ const context = useStoreAtom(atoms.contextAtom);
746
+ return useMemo(() => ({
747
+ commandId,
748
+ context,
749
+ observation,
750
+ reconnect,
751
+ status,
752
+ viewState,
753
+ }), [commandId, context, observation, reconnect, status, viewState]);
754
+ }
755
+ /** Presentation admitted once from the selected Session's already observed slots. */
756
+ export function useClientContributions() {
757
+ return useStoreAtom(useConversationScope().contributions);
758
+ }
759
+ /** The selected Session's ordinary command interaction owner. */
760
+ export function useParakeetInteractions() {
761
+ return useConversationScope().interactions;
762
+ }