@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.
Files changed (71) hide show
  1. package/README.md +45 -6
  2. package/dist/account-commands.js +3 -11
  3. package/dist/commands/advertised.d.ts +3 -1
  4. package/dist/commands/advertised.js +89 -4
  5. package/dist/commands/builtins.js +1 -1
  6. package/dist/commands/interactions.d.ts +22 -7
  7. package/dist/commands/interactions.js +41 -17
  8. package/dist/commands/run.d.ts +3 -2
  9. package/dist/commands/run.js +5 -2
  10. package/dist/commands/slash.d.ts +2 -0
  11. package/dist/commands/slash.js +13 -7
  12. package/dist/commands/use-slash-commands.js +4 -4
  13. package/dist/components/AgentHead.d.ts +9 -12
  14. package/dist/components/AgentHead.js +12 -44
  15. package/dist/components/ChatComposer.d.ts +3 -1
  16. package/dist/components/ChatComposer.js +79 -15
  17. package/dist/components/ChatMessage.d.ts +4 -0
  18. package/dist/components/ChatMessage.js +6 -2
  19. package/dist/components/ClientRows.js +2 -2
  20. package/dist/components/InteractionPanel.d.ts +2 -4
  21. package/dist/components/InteractionPanel.js +2 -2
  22. package/dist/components/ShiitakeAvatar.d.ts +7 -5
  23. package/dist/components/ShiitakeAvatar.js +6 -4
  24. package/dist/components/TimelineItem.d.ts +2 -0
  25. package/dist/components/TimelineItem.js +31 -7
  26. package/dist/components/avatar-presentation.d.ts +7 -14
  27. package/dist/components/avatar-presentation.js +6 -13
  28. package/dist/conversation-opening.d.ts +47 -0
  29. package/dist/conversation-opening.js +173 -0
  30. package/dist/conversation.d.ts +50 -5
  31. package/dist/conversation.js +587 -87
  32. package/dist/core/author-labels.d.ts +2 -0
  33. package/dist/core/build-timeline.d.ts +4 -2
  34. package/dist/core/build-timeline.js +46 -7
  35. package/dist/core/client-actions.js +6 -3
  36. package/dist/core/client-timeline.d.ts +9 -2
  37. package/dist/core/client-timeline.js +15 -4
  38. package/dist/core/draft-storage.d.ts +63 -0
  39. package/dist/core/draft-storage.js +50 -0
  40. package/dist/core/model-identity.d.ts +2 -0
  41. package/dist/core/model-identity.js +6 -0
  42. package/dist/core/queued-recall.d.ts +35 -0
  43. package/dist/core/queued-recall.js +138 -0
  44. package/dist/core/session-projection.js +17 -0
  45. package/dist/core/session-query.d.ts +54 -11
  46. package/dist/core/session-query.js +38 -12
  47. package/dist/core/tool-groups.d.ts +1 -1
  48. package/dist/core/tool-groups.js +3 -1
  49. package/dist/core/tool-view.d.ts +1 -1
  50. package/dist/core/tool-view.js +3 -2
  51. package/dist/core/use-timeline-item-key.d.ts +8 -0
  52. package/dist/core/use-timeline-item-key.js +29 -0
  53. package/dist/headless.d.ts +6 -4
  54. package/dist/headless.js +1 -1
  55. package/dist/index.d.ts +2 -2
  56. package/dist/index.js +1 -1
  57. package/dist/model-commands.js +65 -40
  58. package/dist/parakeet.js +17 -18
  59. package/dist/provider-choices.js +10 -7
  60. package/dist/session-commands.d.ts +0 -9
  61. package/dist/session-commands.js +39 -68
  62. package/dist/session.d.ts +1 -1
  63. package/dist/store/types.d.ts +10 -4
  64. package/dist/styles.css +105 -117
  65. package/docs/api/commands/slash.md +24 -21
  66. package/docs/api/headless.md +375 -232
  67. package/docs/api/index.md +168 -60
  68. package/package.json +16 -10
  69. package/src/styles.css +4 -9
  70. package/dist/components/still-frame-cache.d.ts +0 -16
  71. package/dist/components/still-frame-cache.js +0 -63
@@ -0,0 +1,173 @@
1
+ import { openAppAgent } from "@fungi.computer/agar/agents";
2
+ import { seedSessionQuery } from "./headless.js";
3
+ import { createShiitakeClient, } from "@fungi.computer/shiitake/client";
4
+ import { ShiitakeError } from "@fungi.computer/shiitake";
5
+ import { EventSource as EventSourceImplementation, } from "eventsource";
6
+ /** Seed Parakeet's shared query with the bounded read before mounting its watch. */
7
+ function prepareConversation(queryClient, session) {
8
+ let cancelled = false;
9
+ const readBaseline = async () => {
10
+ try {
11
+ return await session.read();
12
+ }
13
+ catch (cause) {
14
+ if (!(cause instanceof ShiitakeError) ||
15
+ cause.boundary !== "runtime" ||
16
+ cause.cause !== "unavailable")
17
+ throw cause;
18
+ await new Promise((resolve) => setTimeout(resolve, 150));
19
+ if (cancelled)
20
+ throw new Error("Conversation opening cancelled", { cause });
21
+ return session.read();
22
+ }
23
+ };
24
+ let cancel;
25
+ const cancelledRead = new Promise((_, reject) => {
26
+ cancel = () => {
27
+ reject(new Error("Conversation opening cancelled"));
28
+ };
29
+ });
30
+ const ready = Promise.race([readBaseline(), cancelledRead]).then((snapshot) => {
31
+ if (cancelled)
32
+ throw new Error("Conversation opening cancelled");
33
+ seedSessionQuery(queryClient, session, snapshot);
34
+ });
35
+ return {
36
+ ready,
37
+ close: () => {
38
+ cancelled = true;
39
+ cancel();
40
+ },
41
+ };
42
+ }
43
+ export function eventSourceFor(fetcher) {
44
+ return class HostEventSource {
45
+ #source;
46
+ constructor(url) {
47
+ this.#source = new EventSourceImplementation(url, {
48
+ fetch: (input, init) => fetcher(input, init),
49
+ });
50
+ }
51
+ get readyState() {
52
+ return this.#source.readyState;
53
+ }
54
+ addEventListener(type, listener, options) {
55
+ this.#source.addEventListener(type, listener, options);
56
+ }
57
+ close() {
58
+ this.#source.close();
59
+ }
60
+ };
61
+ }
62
+ /** Owns the selected conversation's Agent, client and Parakeet query lifetimes. */
63
+ export function createConversationOpeningOwner(options) {
64
+ const { connection, queryClient, services } = options;
65
+ let pending = null;
66
+ let current = null;
67
+ let currentClose = null;
68
+ let disposed = false;
69
+ const same = (left, right) => left?.agentId === right.agentId && left.sessionId === right.sessionId;
70
+ const isCurrent = (opening) => !disposed && pending === opening;
71
+ const select = (agentId, sessionId, fromOpenRequest = false, waitForBaseline = true) => {
72
+ if (disposed)
73
+ return;
74
+ const selection = { agentId, sessionId };
75
+ if (same(pending, selection))
76
+ return;
77
+ pending?.close();
78
+ pending = null;
79
+ options.onPending(null);
80
+ options.onError(null);
81
+ if (same(current, selection))
82
+ return;
83
+ let openedClose = null;
84
+ let closed = false;
85
+ const opening = {
86
+ ...selection,
87
+ close: () => {
88
+ if (closed)
89
+ return;
90
+ closed = true;
91
+ openedClose?.();
92
+ },
93
+ };
94
+ pending = opening;
95
+ options.onPending(selection);
96
+ void (async () => {
97
+ try {
98
+ const transport = await services.openAgent(connection.capabilities, {
99
+ agentId,
100
+ });
101
+ if (!isCurrent(opening)) {
102
+ await transport.close();
103
+ return;
104
+ }
105
+ const client = services.createClient({
106
+ baseUrl: transport.baseUrl,
107
+ eventSource: eventSourceFor(transport.fetch),
108
+ fetch: transport.fetch,
109
+ });
110
+ const session = client.session(sessionId);
111
+ const observation = prepareConversation(queryClient, session);
112
+ openedClose = () => {
113
+ observation.close();
114
+ void Promise.allSettled([client.close(), transport.close()]);
115
+ };
116
+ await observation.ready;
117
+ if (waitForBaseline && options.reportsSelection !== false) {
118
+ await connection.ui.reportAgentSessionSelection({
119
+ agentId,
120
+ sessionId,
121
+ });
122
+ }
123
+ if (!isCurrent(opening))
124
+ return;
125
+ const conversation = { agentId, sessionId, session };
126
+ const previousClose = currentClose;
127
+ pending = null;
128
+ current = conversation;
129
+ currentClose = opening.close;
130
+ options.onConversation(conversation);
131
+ options.onPending(null);
132
+ previousClose?.();
133
+ }
134
+ catch (cause) {
135
+ console.error("Chat Session opening failed", cause);
136
+ opening.close();
137
+ if (isCurrent(opening)) {
138
+ pending = null;
139
+ options.onPending(null);
140
+ options.onError(fromOpenRequest
141
+ ? `That Session could not be opened. ${current ? "The current conversation remains open." : "Choose another Session from the list."}`
142
+ : `Could not open this Session${cause instanceof Error ? `: ${cause.message}` : ""}. ${current ? "The previous conversation remains open." : "Try opening it again."}`);
143
+ }
144
+ }
145
+ })();
146
+ };
147
+ const clear = () => {
148
+ if (disposed)
149
+ return;
150
+ const hadSelection = pending !== null || current !== null;
151
+ pending?.close();
152
+ pending = null;
153
+ currentClose?.();
154
+ currentClose = null;
155
+ current = null;
156
+ options.onPending(null);
157
+ options.onConversation(null);
158
+ options.onError(null);
159
+ if (!hadSelection || options.reportsSelection === false)
160
+ return;
161
+ void connection.ui
162
+ .reportAgentSessionSelection(null)
163
+ .catch((cause) => {
164
+ console.error("Chat Session selection could not be reported", cause);
165
+ return undefined;
166
+ });
167
+ };
168
+ const close = () => {
169
+ clear();
170
+ disposed = true;
171
+ };
172
+ return { select, clear, close };
173
+ }
@@ -1,13 +1,15 @@
1
1
  import { type ParakeetAuthorOf } from "./core/author-labels.js";
2
- import { type PromptDelivery, type SessionContext, type SessionExtensionSlots, type SessionPendingPrompts } from "@fungi.computer/shiitake/client";
2
+ import { type PromptDelivery, type PromptCommand, type SessionContext, type SessionExtensionSlots, type SessionPendingPrompts } from "@fungi.computer/shiitake/client";
3
3
  import type { ClientEntryV1 } from "@fungi.computer/shiitake/client-entry";
4
4
  import { type ClientContribution } from "./core/client-contributions.js";
5
- import { type ParakeetInteractions } from "./commands/interactions.js";
5
+ import { type ParakeetInteractions, type ParakeetToast } from "./commands/interactions.js";
6
6
  import type { WhistleExecutionOutcome } from "@fungi.computer/whistle";
7
7
  import { type ReactNode } from "react";
8
8
  import { type SessionViewState } from "./core/session-view-state.js";
9
9
  import type { ParakeetSession } from "./session.js";
10
10
  import type { TimelineItem } from "./store/types.js";
11
+ import { type QueuedRecallState, type QueuedRecallOutcome } from "./core/queued-recall.js";
12
+ import { type ParakeetDraftIdentity, type ParakeetDraftStorage } from "./core/draft-storage.js";
11
13
  /** Stable model identity presented by the current Session. */
12
14
  export type ParakeetModel = Readonly<{
13
15
  /** Provider model identifier. */
@@ -49,10 +51,16 @@ export type ParakeetObservation = Readonly<{
49
51
  }>;
50
52
  /** Stable history and replaceable work rendered by a conversation client. */
51
53
  export type TimelineController = Readonly<{
54
+ /** Remove one owner-admitted queued prompt, never the running turn or draft. */
55
+ cancelQueued: (commandId: string) => Promise<void>;
56
+ /** A queued-input gesture is waiting for its owner. */
57
+ cancellingQueued: boolean;
52
58
  /** Durable-backed rows whose identity is stable across progress revisions. */
53
59
  settled: readonly TimelineItem[];
54
60
  /** Replaceable progress, queued, and optimistic rows. */
55
61
  tail: readonly TimelineItem[];
62
+ /** Fixed committed Session positions resolved within the loaded history. */
63
+ positionAnchors: ReadonlyMap<number, string | null>;
56
64
  /** Last actually rendered durable row for each loaded command. */
57
65
  commandAnchors: ReadonlyMap<string, string>;
58
66
  /** Current owner-admitted prompts, independent of the visible history page. */
@@ -78,12 +86,35 @@ export type TimelineController = Readonly<{
78
86
  /** Accumulated durable usage. */
79
87
  usage?: ParakeetUsage | undefined;
80
88
  }>;
89
+ /** A host may proceed with restart only after the current journal committed. */
90
+ export type ParakeetDraftCheckpoint = Readonly<{
91
+ /** The latest draft is committed and restart may proceed. */
92
+ status: "saved";
93
+ }> | Readonly<{
94
+ /** Draft persistence failed; the host must not proceed with restart. */
95
+ status: "failed";
96
+ /** Storage failure explanation for the host. */
97
+ reason: string;
98
+ }>;
81
99
  /** Draft and prompt operations for the current conversation. */
82
100
  export type ComposerController = Readonly<{
83
101
  /** Stop the current turn. Queued prompts keep draining; the draft is unchanged. */
84
102
  cancel: () => Promise<void>;
85
103
  /** Current local input. */
86
104
  draft: string;
105
+ /** Image cargo restored with a queued prompt, carried through the same journal. */
106
+ images: PromptCommand["images"];
107
+ /** Shared composer reservation and recall outcome. */
108
+ recallState: QueuedRecallState;
109
+ /** Recall only when the host accepted a plain Up on empty, focused input. */
110
+ recall: () => Promise<QueuedRecallOutcome>;
111
+ /**
112
+ * Commit the latest draft and pending admissions synchronously, even through
113
+ * a callback captured before newer typing. Does not consume, send or withdraw.
114
+ * A resumed provider reads the same Session journal; never replay a copied
115
+ * draft over its newer state. A failed commit leaves the live draft untouched.
116
+ */
117
+ checkpointDraft: () => ParakeetDraftCheckpoint;
87
118
  /** Typed reason the latest prompt did not complete. */
88
119
  failure: "admission" | "failed" | "cancelled" | "interrupted" | undefined;
89
120
  /** Current send could not enter the bounded local admission queue. */
@@ -98,9 +129,10 @@ export type ComposerController = Readonly<{
98
129
  * Run a host command, such as Whistle slash dispatch, for the current draft.
99
130
  * Parakeet clears the draft only when the command handled it without
100
131
  * declining (a refused replacement keeps the text) and the draft was
101
- * not edited while the command ran.
132
+ * not edited while the command ran. The callback can consume that exact
133
+ * draft on dispatch when the command definition requests it.
102
134
  */
103
- submitCommand: (run: (draft: string) => Promise<WhistleExecutionOutcome | undefined>) => Promise<WhistleExecutionOutcome | undefined>;
135
+ submitCommand: (run: (draft: string, consumeDraft: () => void) => Promise<WhistleExecutionOutcome | undefined>) => Promise<WhistleExecutionOutcome | undefined>;
104
136
  /** Replace the local input. */
105
137
  setDraft: (value: string) => void;
106
138
  }>;
@@ -135,11 +167,24 @@ export type ParakeetReconnectOutcome = Readonly<{
135
167
  cause: string;
136
168
  }>;
137
169
  /** Own one renderer-neutral conversation scope over the existing query cache. */
138
- export declare function ParakeetProvider({ children, session, clients, interactionStore, authorOf, }: Readonly<{
170
+ export declare function ParakeetProvider({ children, session, clients, interactionStore, authorOf, onToast, draftStorage, draftIdentity, }: Readonly<{
139
171
  children: ReactNode;
140
172
  session: ParakeetSession;
141
173
  clients?: readonly ClientEntryV1[] | undefined;
142
174
  interactionStore?: ParakeetInteractions | undefined;
175
+ /** Show a transient message; the host owns the toast surface. */
176
+ onToast?: ((toast: ParakeetToast) => void) | undefined;
177
+ /**
178
+ * Browser localStorage when accessible, otherwise session-only drafts.
179
+ * Native hosts supply an atomic file adapter.
180
+ */
181
+ draftStorage?: ParakeetDraftStorage | undefined;
182
+ /**
183
+ * Hosts replacing transport endpoints must supply the durable owner pair.
184
+ * Otherwise the Session's logical address names its journal.
185
+ * Exactly one key is used; no previous-address lookup or migration occurs.
186
+ */
187
+ draftIdentity?: ParakeetDraftIdentity | undefined;
143
188
  /**
144
189
  * Read the author to show from a message's opaque host metadata. Absent,
145
190
  * lines show no author.