@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,3 @@
1
+ import type { AssistantMessageItem } from "../store/types.js";
2
+ /** One rule for whether an assistant row has content either renderer shows. */
3
+ export declare function assistantMessageVisible(item: AssistantMessageItem): boolean;
@@ -0,0 +1,4 @@
1
+ /** One rule for whether an assistant row has content either renderer shows. */
2
+ export function assistantMessageVisible(item) {
3
+ return item.text.trim() !== "" || (item.thinking?.trim() ?? "") !== "";
4
+ }
@@ -0,0 +1,16 @@
1
+ import type { SessionMessage } from "@fungi.computer/shiitake/client";
2
+ /** Who wrote a timeline line, as the host chose to show them. */
3
+ export type ParakeetAuthor = Readonly<{
4
+ /** A stable identity that tells two same-named authors apart. */
5
+ id: string;
6
+ /** The name shown beside the line. */
7
+ name: string;
8
+ }>;
9
+ /**
10
+ * The host's reading of a message's opaque metadata: the author to show, or
11
+ * none. Parakeet never interprets metadata itself.
12
+ */
13
+ export type ParakeetAuthorOf = (meta: SessionMessage["meta"]) => ParakeetAuthor | undefined;
14
+ /** The default reading: metadata names no author Parakeet can show. */
15
+ export declare const noAuthor: ParakeetAuthorOf;
16
+ export declare function authorDisplayNames(authors: readonly ParakeetAuthor[]): ReadonlyMap<string, string>;
@@ -0,0 +1,16 @@
1
+ /** The default reading: metadata names no author Parakeet can show. */
2
+ export const noAuthor = () => undefined;
3
+ export function authorDisplayNames(authors) {
4
+ const identities = new Map();
5
+ for (const { id, name } of authors) {
6
+ const known = identities.get(name) ?? new Set();
7
+ known.add(id);
8
+ identities.set(name, known);
9
+ }
10
+ return new Map(authors.map(({ id, name }) => [
11
+ id,
12
+ (identities.get(name)?.size ?? 0) > 1
13
+ ? `${name} · ${id.slice(-6)}`
14
+ : name,
15
+ ]));
16
+ }
@@ -0,0 +1,36 @@
1
+ import { type ParakeetAuthorOf } from "./author-labels.js";
2
+ import type { SessionActivity, SessionCompactionMarker, SessionCompactionNotice, SessionMessage, SessionPendingPrompt, SessionProgress } from "@fungi.computer/shiitake/client";
3
+ import type { LocalPromptAdmission, TimelineItem } from "../store/types.js";
4
+ export type TimelineProjection = Readonly<{
5
+ items: readonly TimelineItem[];
6
+ model?: Readonly<{
7
+ model: string;
8
+ provider: string;
9
+ }>;
10
+ usage?: Readonly<{
11
+ cost: number;
12
+ tokens: number;
13
+ }>;
14
+ }>;
15
+ /**
16
+ * The client request identity of a prompt this composer submitted, by its
17
+ * owner-issued command, so one row keeps its identity from submit to durable.
18
+ */
19
+ export type PromptRequestIdOf = (commandId: string) => string | undefined;
20
+ export type DurableTimelineProjection = TimelineProjection & Readonly<{
21
+ completedMessageIds: ReadonlySet<string>;
22
+ commandIds: ReadonlySet<string>;
23
+ commandAnchors: ReadonlyMap<string, string>;
24
+ }>;
25
+ /**
26
+ * Project one durable Session revision into timeline rows, folding consecutive
27
+ * tool calls into groups. `compactions` places one divider row immediately
28
+ * before the message it marks; `notices` places a short row at a compaction
29
+ * that changed nothing. The model's own context is unaffected by this purely
30
+ * client-side projection.
31
+ */
32
+ export declare function buildDurableTimeline(messages: readonly SessionMessage[], compactions?: readonly SessionCompactionMarker[], activity?: SessionActivity, authorOf?: ParakeetAuthorOf, requestIdOf?: PromptRequestIdOf, notices?: readonly SessionCompactionNotice[]): DurableTimelineProjection;
33
+ /** Project replaceable live work without copying settled history. */
34
+ export declare function buildTimelineTail(durable: DurableTimelineProjection, progress: SessionProgress | null, queued: readonly SessionPendingPrompt[], admissions?: readonly LocalPromptAdmission[], authorOf?: ParakeetAuthorOf, requestIdOf?: PromptRequestIdOf): readonly TimelineItem[];
35
+ /** Overlay live progress and an optimistic prompt over stable durable rows. */
36
+ export declare function buildTimeline(durable: DurableTimelineProjection, progress: SessionProgress | null, queued: readonly SessionPendingPrompt[], admissions?: readonly LocalPromptAdmission[]): TimelineProjection;
@@ -0,0 +1,328 @@
1
+ import { noAuthor, } from "./author-labels.js";
2
+ import { completedMessageIds } from "./session-projection.js";
3
+ import { ToolArgumentsSchema } from "./tool-contract.js";
4
+ import { assistantMessageVisible } from "./assistant-visibility.js";
5
+ import { groupToolCalls } from "./tool-groups.js";
6
+ function orderedSources(messages, compactions, notices) {
7
+ const sources = [
8
+ ...messages.map((message) => ({
9
+ sequence: message.sequence,
10
+ tie: 1,
11
+ kind: "message",
12
+ message,
13
+ })),
14
+ ...compactions.map((marker) => ({
15
+ sequence: marker.sequence,
16
+ tie: 0,
17
+ kind: "marker",
18
+ marker,
19
+ })),
20
+ ...notices.map((notice) => ({
21
+ sequence: notice.sequence,
22
+ tie: 2,
23
+ kind: "notice",
24
+ notice,
25
+ })),
26
+ ];
27
+ return sources.sort((left, right) => left.sequence - right.sequence || left.tie - right.tie);
28
+ }
29
+ const noRequestId = () => undefined;
30
+ /** Carry a local prompt's request identity onto its owner-reported row. */
31
+ function withRequestId(item, requestId) {
32
+ return requestId === undefined ? item : { ...item, requestId };
33
+ }
34
+ function userText(content) {
35
+ return Array.isArray(content)
36
+ ? content
37
+ .flatMap((part) => (part.type === "text" ? [part.text] : []))
38
+ .join("\n")
39
+ : content;
40
+ }
41
+ function toolItem(call, startedAt) {
42
+ return {
43
+ args: ToolArgumentsSchema.parse(call.arguments),
44
+ id: call.id,
45
+ kind: "tool",
46
+ name: call.name,
47
+ startedAt,
48
+ status: "unknown",
49
+ };
50
+ }
51
+ function progressItem(progress) {
52
+ const base = {
53
+ id: progress.messageId,
54
+ kind: "assistant_message",
55
+ pending: true,
56
+ text: progress.text,
57
+ };
58
+ return progress.thinking === ""
59
+ ? base
60
+ : { ...base, thinking: progress.thinking };
61
+ }
62
+ function collectAssistantBlocks(value) {
63
+ const text = [];
64
+ const thinking = [];
65
+ const tools = [];
66
+ for (const block of value.content) {
67
+ if (block.type === "text")
68
+ text.push(block.text);
69
+ if (block.type === "thinking")
70
+ thinking.push(block.thinking);
71
+ if (block.type === "toolCall")
72
+ tools.push(toolItem(block, value.timestamp));
73
+ }
74
+ return { text, thinking, tools };
75
+ }
76
+ function assistantMessageItem(id, value, blocks) {
77
+ if (blocks.text.length === 0 &&
78
+ blocks.thinking.length === 0 &&
79
+ blocks.tools.length > 0)
80
+ return undefined;
81
+ const item = {
82
+ id,
83
+ isError: value.stopReason === "error" || value.stopReason === "aborted",
84
+ kind: "assistant_message",
85
+ pending: false,
86
+ at: value.timestamp,
87
+ text: blocks.text.length === 0
88
+ ? value.stopReason === "error"
89
+ ? "This run couldn’t finish."
90
+ : value.stopReason === "aborted"
91
+ ? "The run was cancelled."
92
+ : ""
93
+ : blocks.text.join(""),
94
+ usage: {
95
+ cost: value.usage.cost.total,
96
+ tokens: value.usage.totalTokens,
97
+ },
98
+ };
99
+ return blocks.thinking.length === 0
100
+ ? item
101
+ : { ...item, thinking: blocks.thinking.join("\n") };
102
+ }
103
+ function projectAssistant(id, value, author) {
104
+ const blocks = collectAssistantBlocks(value);
105
+ const message = assistantMessageItem(id, value, blocks);
106
+ return {
107
+ items: message === undefined
108
+ ? blocks.tools
109
+ : [{ ...message, author }, ...blocks.tools],
110
+ model: { model: value.model, provider: value.provider },
111
+ usage: { cost: value.usage.cost.total, tokens: value.usage.totalTokens },
112
+ };
113
+ }
114
+ function appendAssistant(items, toolIndexes, assistant) {
115
+ for (const item of assistant.items) {
116
+ if (item.kind === "tool")
117
+ toolIndexes.set(item.id, items.length);
118
+ items.push(item);
119
+ }
120
+ }
121
+ function completeTool(items, toolIndexes, value) {
122
+ const toolIndex = toolIndexes.get(value.toolCallId);
123
+ if (toolIndex === undefined)
124
+ return;
125
+ const tool = items[toolIndex];
126
+ if (tool?.kind !== "tool")
127
+ return;
128
+ items[toolIndex] = {
129
+ ...tool,
130
+ result: value,
131
+ status: value.isError ? "error" : "completed",
132
+ };
133
+ }
134
+ /**
135
+ * Project one durable Session revision into timeline rows, folding consecutive
136
+ * tool calls into groups. `compactions` places one divider row immediately
137
+ * before the message it marks; `notices` places a short row at a compaction
138
+ * that changed nothing. The model's own context is unaffected by this purely
139
+ * client-side projection.
140
+ */
141
+ export function buildDurableTimeline(messages, compactions = [], activity, authorOf = noAuthor, requestIdOf = noRequestId, notices = []) {
142
+ const items = [];
143
+ const toolIndexes = new Map();
144
+ const commandIds = new Set();
145
+ const itemCommands = new Map();
146
+ let model;
147
+ let tokens = 0;
148
+ let cost = 0;
149
+ for (const source of orderedSources(messages, compactions, notices)) {
150
+ if (source.kind === "marker") {
151
+ items.push({
152
+ id: source.marker.id,
153
+ kind: "compaction_divider",
154
+ at: source.marker.at,
155
+ tokensBefore: source.marker.tokensBefore,
156
+ tokensAfter: source.marker.tokensAfter,
157
+ });
158
+ continue;
159
+ }
160
+ if (source.kind === "notice") {
161
+ items.push({
162
+ id: source.notice.id,
163
+ kind: "compaction_notice",
164
+ at: source.notice.at,
165
+ trigger: source.notice.trigger,
166
+ result: source.notice.result,
167
+ diagnostic: source.notice.diagnostic,
168
+ });
169
+ continue;
170
+ }
171
+ const message = source.message;
172
+ const value = message.value;
173
+ if (message.commandId !== null)
174
+ commandIds.add(message.commandId);
175
+ const firstItem = items.length;
176
+ switch (value.role) {
177
+ case "user": {
178
+ const commandId = message.commandId ?? message.id;
179
+ const confirmed = {
180
+ id: message.id,
181
+ kind: "user_message",
182
+ commandId,
183
+ status: "confirmed",
184
+ at: value.timestamp,
185
+ text: userText(value.content),
186
+ author: authorOf(message.meta),
187
+ origin: message.origin,
188
+ };
189
+ items.push(withRequestId(confirmed, requestIdOf(commandId)));
190
+ break;
191
+ }
192
+ case "assistant": {
193
+ const assistant = projectAssistant(message.id, value, authorOf(message.meta));
194
+ model = assistant.model;
195
+ tokens += assistant.usage.tokens;
196
+ cost += assistant.usage.cost;
197
+ appendAssistant(items, toolIndexes, assistant);
198
+ break;
199
+ }
200
+ case "toolResult":
201
+ completeTool(items, toolIndexes, value);
202
+ if (message.commandId !== null) {
203
+ const commands = itemCommands.get(value.toolCallId) ?? new Set();
204
+ commands.add(message.commandId);
205
+ itemCommands.set(value.toolCallId, commands);
206
+ }
207
+ break;
208
+ }
209
+ if (message.commandId !== null) {
210
+ for (const item of items.slice(firstItem))
211
+ itemCommands.set(item.id, new Set([message.commandId]));
212
+ }
213
+ }
214
+ // A durable call without a result is not evidence that it still runs.
215
+ // Only the current command's last matching call can borrow live tool activity.
216
+ const queue = activity?.queue;
217
+ if (queue?.state === "running" && activity?.activeTool !== null) {
218
+ let currentCommand = false;
219
+ let toolIndex = -1;
220
+ for (let index = 0; index < items.length; index++) {
221
+ const item = items[index];
222
+ if (item?.kind === "user_message") {
223
+ currentCommand = item.commandId === queue.commandId;
224
+ if (currentCommand)
225
+ toolIndex = -1;
226
+ }
227
+ if (currentCommand &&
228
+ item?.kind === "tool" &&
229
+ item.status === "unknown" &&
230
+ item.name === activity?.activeTool)
231
+ toolIndex = index;
232
+ }
233
+ const active = items[toolIndex];
234
+ if (active?.kind === "tool")
235
+ items[toolIndex] = { ...active, status: "running" };
236
+ }
237
+ const grouped = groupToolCalls(items);
238
+ const commandAnchors = new Map();
239
+ for (const row of grouped) {
240
+ if (row.kind === "assistant_message" && !assistantMessageVisible(row))
241
+ continue;
242
+ const members = row.kind === "tool_group" ? row.tools : [row];
243
+ for (const member of members)
244
+ for (const command of itemCommands.get(member.id) ?? [])
245
+ commandAnchors.set(command, row.id);
246
+ }
247
+ const base = {
248
+ commandAnchors,
249
+ completedMessageIds: completedMessageIds(messages),
250
+ items: grouped,
251
+ commandIds,
252
+ };
253
+ return model === undefined
254
+ ? base
255
+ : { ...base, model, usage: { cost, tokens } };
256
+ }
257
+ /** Project replaceable live work without copying settled history. */
258
+ export function buildTimelineTail(durable, progress, queued, admissions = [], authorOf = noAuthor, requestIdOf = noRequestId) {
259
+ const visibleProgress = progress === null ||
260
+ durable.completedMessageIds.has(progress.messageId) ||
261
+ (progress.text === "" && progress.thinking === "")
262
+ ? undefined
263
+ : progress;
264
+ if (admissions.length === 0 &&
265
+ visibleProgress === undefined &&
266
+ queued.length === 0)
267
+ return [];
268
+ const items = [];
269
+ const queuedCommandIds = new Set(queued.map(({ commandId }) => commandId));
270
+ for (const prompt of queued) {
271
+ if (durable.commandIds.has(prompt.commandId))
272
+ continue;
273
+ const waiting = {
274
+ id: prompt.commandId,
275
+ kind: "user_message",
276
+ commandId: prompt.commandId,
277
+ status: "queued",
278
+ delivery: prompt.delivery,
279
+ text: prompt.text,
280
+ origin: prompt.origin,
281
+ author: authorOf(prompt.meta),
282
+ };
283
+ items.push(withRequestId(waiting, requestIdOf(prompt.commandId)));
284
+ }
285
+ for (const admission of admissions) {
286
+ if (admission.commandId !== undefined &&
287
+ (queuedCommandIds.has(admission.commandId) ||
288
+ durable.commandIds.has(admission.commandId)))
289
+ continue;
290
+ const baseItem = {
291
+ id: admission.requestId,
292
+ kind: "user_message",
293
+ requestId: admission.requestId,
294
+ status: admission.status === "failed"
295
+ ? "failed"
296
+ : admission.status === "accepted"
297
+ ? "accepted"
298
+ : "pending",
299
+ delivery: admission.delivery,
300
+ text: admission.content,
301
+ };
302
+ const commandItem = admission.commandId === undefined
303
+ ? baseItem
304
+ : { ...baseItem, commandId: admission.commandId };
305
+ const item = admission.error === undefined
306
+ ? commandItem
307
+ : { ...commandItem, error: admission.error };
308
+ items.push(item);
309
+ }
310
+ // Group only unresolved input; settled history and live progress keep their
311
+ // places. Filtering preserves the owner's order within each delivery group.
312
+ return [
313
+ ...(visibleProgress === undefined ? [] : [progressItem(visibleProgress)]),
314
+ ...items.filter((item) => item.status !== "failed" && item.delivery === "steer"),
315
+ ...items.filter((item) => item.status !== "failed" && item.delivery === "followUp"),
316
+ ...items.filter((item) => item.status === "failed"),
317
+ ];
318
+ }
319
+ /** Overlay live progress and an optimistic prompt over stable durable rows. */
320
+ export function buildTimeline(durable, progress, queued, admissions = []) {
321
+ const tail = buildTimelineTail(durable, progress, queued, admissions);
322
+ if (tail.length === 0)
323
+ return durable;
324
+ const items = [...durable.items, ...tail];
325
+ if (durable.model === undefined || durable.usage === undefined)
326
+ return { items };
327
+ return { items, model: durable.model, usage: durable.usage };
328
+ }
@@ -0,0 +1,35 @@
1
+ import type { ClientContributionV1 } from "@fungi.computer/shiitake/client-entry";
2
+ import { type WhistleRuntime } from "@fungi.computer/whistle";
3
+ import type { ParakeetInteractions } from "../commands/interactions.js";
4
+ import type { ClientContribution } from "./client-contributions.js";
5
+ /** Action state belongs to the selected Session, never the rendered row. */
6
+ export type ClientActionState = Readonly<{
7
+ /** The same advertised intent is already in flight. */
8
+ pending: boolean;
9
+ /** Visible execution failure; cleared when this intent is retried. */
10
+ error?: string;
11
+ }>;
12
+ /** Run admitted row intents through the ordinary advertised Whistle commands. */
13
+ export declare function createClientActions(options: Readonly<{
14
+ contributions: () => readonly ClientContribution[];
15
+ online: () => boolean;
16
+ commands: Pick<WhistleRuntime, "execute">;
17
+ interactions: ParakeetInteractions;
18
+ }>): {
19
+ snapshot: () => ReadonlyMap<{
20
+ readonly label: string;
21
+ readonly id: string;
22
+ readonly input?: {
23
+ readonly [x: string]: import("effect/Schema").Json;
24
+ } | undefined;
25
+ }, Readonly<{
26
+ /** The same advertised intent is already in flight. */
27
+ pending: boolean;
28
+ /** Visible execution failure; cleared when this intent is retried. */
29
+ error?: string;
30
+ }>>;
31
+ refresh: () => void;
32
+ subscribe: (listener: () => void) => () => void;
33
+ execute: (entryId: string, action: NonNullable<ClientContributionV1["waiting"]>["cancel"], waiting?: boolean) => Promise<void>;
34
+ dispose: () => void;
35
+ };
@@ -0,0 +1,128 @@
1
+ import { WhistleActionError, } from "@fungi.computer/whistle";
2
+ import { reportFailure } from "../lib/failure.js";
3
+ function advertisedActions(entry) {
4
+ return [
5
+ ...entry.value.panel.flatMap((row) => row.actions ?? []),
6
+ ...entry.value.timeline.cells.flatMap((cell) => cell.rows.flatMap((row) => row.actions ?? [])),
7
+ ...(entry.value.waiting === null ? [] : [entry.value.waiting.cancel]),
8
+ ];
9
+ }
10
+ /** Run admitted row intents through the ordinary advertised Whistle commands. */
11
+ export function createClientActions(options) {
12
+ const controller = new AbortController();
13
+ // Abort may change while the advertised command is awaiting its owner.
14
+ const retired = () => controller.signal.aborted;
15
+ const listeners = new Set();
16
+ let state = new Map();
17
+ // Admission decodes fresh row objects on a committed slot change. Pending
18
+ // work belongs to the advertised command intent, not that object's address.
19
+ const intents = new Map();
20
+ const key = (entryId, action) => JSON.stringify([entryId, action.id, action.input ?? {}]);
21
+ const refresh = () => {
22
+ if (retired())
23
+ return;
24
+ const current = new Map();
25
+ const visible = new Set();
26
+ for (const entry of options.contributions()) {
27
+ for (const action of advertisedActions(entry)) {
28
+ const id = key(entry.entryId, action);
29
+ visible.add(id);
30
+ const value = intents.get(id);
31
+ if (value !== undefined)
32
+ current.set(action, value);
33
+ }
34
+ }
35
+ for (const [id, value] of intents)
36
+ if (!visible.has(id) && !value.pending)
37
+ intents.delete(id);
38
+ state = current;
39
+ for (const listener of listeners)
40
+ listener();
41
+ };
42
+ const publish = (id, next) => {
43
+ if (retired())
44
+ return;
45
+ intents.set(id, next);
46
+ refresh();
47
+ };
48
+ return {
49
+ snapshot: () => state,
50
+ refresh,
51
+ subscribe: (listener) => {
52
+ listeners.add(listener);
53
+ return () => {
54
+ listeners.delete(listener);
55
+ };
56
+ },
57
+ execute: async (entryId, action, waiting = false) => {
58
+ const entry = options
59
+ .contributions()
60
+ .find((value) => value.entryId === entryId);
61
+ if (retired() ||
62
+ !options.online() ||
63
+ entry === undefined ||
64
+ !advertisedActions(entry).includes(action) ||
65
+ (waiting && entry.value.waiting?.cancel !== action))
66
+ return;
67
+ const intent = key(entryId, action);
68
+ if (intents.get(intent)?.pending)
69
+ return;
70
+ publish(intent, { pending: true });
71
+ const notificationOnly = {
72
+ signal: controller.signal,
73
+ prompt: () => Promise.reject(new WhistleActionError("client_waiting_prompt", "A waiting cancellation cannot request an answer")),
74
+ notify: (notice) => {
75
+ if (retired())
76
+ return;
77
+ options.interactions.report({
78
+ tone: notice.type === "success" ? "success" : "info",
79
+ message: notice.type === "handoff"
80
+ ? (notice.message ?? notice.url)
81
+ : notice.message,
82
+ });
83
+ },
84
+ };
85
+ let outcome;
86
+ try {
87
+ let input = {
88
+ origin: "menu",
89
+ signal: controller.signal,
90
+ };
91
+ if (action.input !== undefined)
92
+ input = { ...input, arguments: action.input };
93
+ if (waiting)
94
+ input = { ...input, interaction: notificationOnly };
95
+ outcome = await options.commands.execute(`${entryId}:${action.id}`, input);
96
+ }
97
+ catch (cause) {
98
+ reportFailure("client row command", cause);
99
+ if (retired())
100
+ return;
101
+ const error = cause instanceof Error
102
+ ? cause
103
+ : new Error("Client row command failed", { cause });
104
+ publish(intent, { pending: false, error: error.message });
105
+ options.interactions.report({ tone: "error", message: error.message });
106
+ return;
107
+ }
108
+ if (retired())
109
+ return;
110
+ const error = outcome.status === "handled"
111
+ ? undefined
112
+ : outcome.status === "failed"
113
+ ? outcome.error.message
114
+ : outcome.status === "unavailable"
115
+ ? outcome.reason
116
+ : `Client row command ${outcome.status}`;
117
+ publish(intent, error === undefined ? { pending: false } : { pending: false, error });
118
+ if (error !== undefined)
119
+ options.interactions.report({ tone: "error", message: error });
120
+ },
121
+ dispose: () => {
122
+ controller.abort();
123
+ state = new Map();
124
+ intents.clear();
125
+ listeners.clear();
126
+ },
127
+ };
128
+ }
@@ -0,0 +1,24 @@
1
+ import { type ClientContributionV1, type ClientEntryV1 } from "@fungi.computer/shiitake/client-entry";
2
+ import type { SessionExtensionSlots } from "@fungi.computer/shiitake/client";
3
+ /** Decoded client data, still owned and ordered by its composition root. */
4
+ export type ClientContribution = Readonly<{
5
+ /** Composition-root identity of the producing entry. */
6
+ entryId: string;
7
+ /** Strictly admitted presentation data for the current observed slot. */
8
+ value: ClientContributionV1;
9
+ }>;
10
+ /**
11
+ * The selected-Session evaluation owner. Feed existing observed slots here;
12
+ * renderers subscribe to admitted data and never evaluate an entry in render.
13
+ */
14
+ export declare function createClientContributions(installed: readonly ClientEntryV1[], onError: (entryId: string, error: Error) => void): {
15
+ snapshot: () => readonly Readonly<{
16
+ /** Composition-root identity of the producing entry. */
17
+ entryId: string;
18
+ /** Strictly admitted presentation data for the current observed slot. */
19
+ value: ClientContributionV1;
20
+ }>[];
21
+ subscribe: (listener: () => void) => () => void;
22
+ update: (slots: SessionExtensionSlots) => void;
23
+ dispose: () => void;
24
+ };