@fungi.computer/parakeet 0.4.0 → 0.6.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 (82) hide show
  1. package/README.md +17 -1
  2. package/dist/client-contributions.d.ts +1 -0
  3. package/dist/commands/awaiting-driver.d.ts +20 -0
  4. package/dist/commands/awaiting-driver.js +121 -0
  5. package/dist/commands/builtins.js +6 -4
  6. package/dist/commands/interactions.d.ts +25 -0
  7. package/dist/commands/interactions.js +76 -2
  8. package/dist/commands/run.js +3 -3
  9. package/dist/commands/slash.d.ts +7 -3
  10. package/dist/commands/slash.js +2 -0
  11. package/dist/commands/use-slash-commands.d.ts +13 -0
  12. package/dist/commands/use-slash-commands.js +58 -1
  13. package/dist/components/ChatComposer.d.ts +2 -0
  14. package/dist/components/ChatComposer.js +158 -40
  15. package/dist/components/ClientRows.js +42 -26
  16. package/dist/components/CodeBlock.js +3 -2
  17. package/dist/components/CompletionMenu.d.ts +54 -0
  18. package/dist/components/CompletionMenu.js +52 -0
  19. package/dist/components/ComposerModelKey.d.ts +11 -0
  20. package/dist/components/ComposerModelKey.js +9 -0
  21. package/dist/components/ComposerShell.d.ts +47 -0
  22. package/dist/components/ComposerShell.js +29 -0
  23. package/dist/components/DecisionComposer.d.ts +30 -0
  24. package/dist/components/DecisionComposer.js +145 -0
  25. package/dist/components/InteractionPanel.d.ts +10 -1
  26. package/dist/components/InteractionPanel.js +9 -4
  27. package/dist/components/SlashMenu.d.ts +1 -3
  28. package/dist/components/SlashMenu.js +5 -13
  29. package/dist/components/TimelineItem.d.ts +1 -0
  30. package/dist/components/TimelineItem.js +4 -4
  31. package/dist/components/ToolGroup.d.ts +1 -0
  32. package/dist/components/ToolGroup.js +8 -8
  33. package/dist/conversation-opening.d.ts +1 -1
  34. package/dist/conversation-opening.js +2 -2
  35. package/dist/conversation.d.ts +5 -1
  36. package/dist/conversation.js +84 -29
  37. package/dist/core/approval-decision.d.ts +198 -0
  38. package/dist/core/approval-decision.js +291 -0
  39. package/dist/core/awaiting-answer.d.ts +229 -0
  40. package/dist/core/awaiting-answer.js +303 -0
  41. package/dist/core/build-timeline.js +61 -7
  42. package/dist/core/client-actions.d.ts +1 -0
  43. package/dist/core/client-actions.js +7 -1
  44. package/dist/core/composer-model-name.d.ts +3 -0
  45. package/dist/core/composer-model-name.js +21 -0
  46. package/dist/core/display-text.d.ts +2 -0
  47. package/dist/core/display-text.js +7 -0
  48. package/dist/core/draft-storage.d.ts +6 -0
  49. package/dist/core/draft-storage.js +25 -5
  50. package/dist/core/session-projection.d.ts +1 -0
  51. package/dist/core/session-projection.js +3 -2
  52. package/dist/core/session-query.d.ts +8 -0
  53. package/dist/core/session-query.js +13 -11
  54. package/dist/core/session-view-state.d.ts +2 -4
  55. package/dist/core/session-view-state.js +27 -9
  56. package/dist/core/tool-groups.d.ts +3 -0
  57. package/dist/core/tool-groups.js +37 -1
  58. package/dist/core/tool-view.d.ts +6 -0
  59. package/dist/core/tool-view.js +33 -0
  60. package/dist/core/use-timeline-item-key.d.ts +3 -4
  61. package/dist/core/use-timeline-item-key.js +8 -14
  62. package/dist/headless.d.ts +10 -5
  63. package/dist/headless.js +9 -4
  64. package/dist/index.d.ts +3 -0
  65. package/dist/index.js +3 -0
  66. package/dist/parakeet.d.ts +6 -2
  67. package/dist/parakeet.js +24 -17
  68. package/dist/store/types.d.ts +14 -1
  69. package/dist/styles.css +911 -162
  70. package/dist/use-approval-awaiting.d.ts +22 -0
  71. package/dist/use-approval-awaiting.js +81 -0
  72. package/dist/use-decision.d.ts +20 -0
  73. package/dist/use-decision.js +79 -0
  74. package/docs/api/commands/slash.md +30 -26
  75. package/docs/api/headless.md +917 -54
  76. package/docs/api/index.md +166 -5
  77. package/package.json +17 -8
  78. package/src/completion-menu.css +72 -0
  79. package/src/composer-shell.css +125 -0
  80. package/src/styles.css +99 -53
  81. package/dist/components/WaitingNotice.d.ts +0 -7
  82. package/dist/components/WaitingNotice.js +0 -8
@@ -0,0 +1,303 @@
1
+ /**
2
+ * Order the items by arrival and pick the one the composer shows. A postponed
3
+ * item leaves the composer but stays counted: it is the footer row.
4
+ */
5
+ export function awaitingQueue(items, later) {
6
+ const ordered = items
7
+ .map((item, index) => ({ item, index }))
8
+ .sort((left, right) => left.item.arrivedAt - right.item.arrivedAt || left.index - right.index)
9
+ .map(({ item }) => item);
10
+ const current = ordered.find((item) => !later.has(item.key));
11
+ return {
12
+ items: ordered,
13
+ current,
14
+ position: current === undefined ? 0 : ordered.indexOf(current) + 1,
15
+ total: ordered.length,
16
+ };
17
+ }
18
+ /**
19
+ * The item that owns the composer right now. An empty draft is taken over;
20
+ * a draft with text is never, unless the person asked for the decision.
21
+ */
22
+ export function awaitingTakeover(awaiting, draft) {
23
+ return draft === "" || awaiting.forced ? awaiting.current : undefined;
24
+ }
25
+ /** The state a decision starts in. */
26
+ export function initialDecisionState() {
27
+ return {
28
+ highlight: 0,
29
+ toggled: new Set(),
30
+ other: "",
31
+ phase: "choose",
32
+ details: false,
33
+ };
34
+ }
35
+ const unchanged = (state) => ({ state });
36
+ function onRow(state, prompt, highlight) {
37
+ return {
38
+ ...state,
39
+ highlight,
40
+ phase: highlight === prompt.options.length ? "other" : "choose",
41
+ refusal: undefined,
42
+ };
43
+ }
44
+ /**
45
+ * The one set of rules both renderers follow. Number keys only move the
46
+ * highlight, Enter is the only way to answer, and Esc steps back one level
47
+ * until it reaches the option list, where it postpones the decision.
48
+ */
49
+ export function reduceDecision(state, intent, prompt) {
50
+ const optionCount = prompt.options.length;
51
+ const other = prompt.other;
52
+ const rows = optionCount + (other === undefined ? 0 : 1);
53
+ if (intent.type === "refused")
54
+ return {
55
+ state: { ...state, phase: "refused", refusal: intent.message },
56
+ };
57
+ if (state.phase === "sending")
58
+ return unchanged(state);
59
+ if (state.details && intent.type !== "details" && intent.type !== "escape")
60
+ return unchanged(state);
61
+ switch (intent.type) {
62
+ case "move":
63
+ return {
64
+ state: onRow(state, prompt, (state.highlight + intent.delta + rows) % rows),
65
+ };
66
+ case "pick":
67
+ return intent.number >= 1 && intent.number <= rows
68
+ ? { state: onRow(state, prompt, intent.number - 1) }
69
+ : unchanged(state);
70
+ case "toggle": {
71
+ const option = prompt.options[state.highlight];
72
+ if (prompt.multiple !== true || option === undefined)
73
+ return unchanged(state);
74
+ const toggled = new Set(state.toggled);
75
+ if (!toggled.delete(option.value))
76
+ toggled.add(option.value);
77
+ return {
78
+ state: { ...state, toggled, phase: "choose", refusal: undefined },
79
+ };
80
+ }
81
+ case "type":
82
+ case "append":
83
+ if (other === undefined)
84
+ return unchanged(state);
85
+ return {
86
+ state: {
87
+ ...state,
88
+ other: (intent.type === "append"
89
+ ? `${state.other}${intent.text}`
90
+ : intent.text).slice(0, other.maxLength),
91
+ highlight: optionCount,
92
+ phase: "other",
93
+ refusal: undefined,
94
+ },
95
+ };
96
+ case "details":
97
+ return { state: { ...state, details: !state.details } };
98
+ case "enter": {
99
+ const text = state.other.trim();
100
+ const answerText = text === "" ? {} : { text };
101
+ if (prompt.multiple === true) {
102
+ const selected = prompt.options
103
+ .map((option) => option.value)
104
+ .filter((value) => state.toggled.has(value));
105
+ if (selected.length === 0 && text === "")
106
+ return {
107
+ state: onRow(state, prompt, state.highlight),
108
+ };
109
+ return {
110
+ state: { ...state, phase: "sending", refusal: undefined },
111
+ effect: { type: "answer", selected, ...answerText },
112
+ };
113
+ }
114
+ const option = prompt.options[state.highlight];
115
+ if (option !== undefined)
116
+ return {
117
+ state: { ...state, phase: "sending", refusal: undefined },
118
+ effect: { type: "answer", selected: [option.value] },
119
+ };
120
+ if (text === "") {
121
+ if (other?.blank === undefined)
122
+ return { state: onRow(state, prompt, optionCount) };
123
+ return {
124
+ state: { ...state, phase: "sending", refusal: undefined },
125
+ effect: { type: "answer", selected: [other.blank] },
126
+ };
127
+ }
128
+ return {
129
+ state: { ...state, phase: "sending", refusal: undefined },
130
+ effect: { type: "answer", selected: [], text },
131
+ };
132
+ }
133
+ case "escape":
134
+ if (state.details)
135
+ return { state: { ...state, details: false } };
136
+ if (state.phase === "other" || state.phase === "confirm")
137
+ return {
138
+ state: { ...state, highlight: 0, phase: "choose" },
139
+ effect: { type: "back" },
140
+ };
141
+ if (prompt.escape !== undefined)
142
+ return {
143
+ state: { ...state, phase: "sending", refusal: undefined },
144
+ effect: { type: "answer", selected: [prompt.escape] },
145
+ };
146
+ return { state, effect: { type: "later" } };
147
+ }
148
+ }
149
+ const noun = { question: "question", approval: "approval" };
150
+ /** Project a decision's state into the view every renderer draws. */
151
+ export function decisionView(state, prompt, context) {
152
+ const multiple = prompt.multiple === true;
153
+ const optionCount = prompt.options.length;
154
+ const otherText = state.other.trim();
155
+ const rows = [
156
+ ...prompt.options.map((option, index) => {
157
+ const toggled = state.toggled.has(option.value);
158
+ const marker = multiple
159
+ ? toggled
160
+ ? "[x]"
161
+ : "[ ]"
162
+ : `${String(index + 1)}.`;
163
+ const caption = `${option.label}${index === 0 ? " (recommended)" : ""}`;
164
+ return {
165
+ index,
166
+ kind: "option",
167
+ marker,
168
+ label: option.label,
169
+ description: option.description,
170
+ recommended: index === 0,
171
+ highlighted: state.highlight === index,
172
+ toggled,
173
+ destructive: option.destructive === true,
174
+ caption,
175
+ text: `${marker} ${caption}`,
176
+ };
177
+ }),
178
+ ...(prompt.other === undefined
179
+ ? []
180
+ : [
181
+ ((other) => {
182
+ const toggled = multiple && otherText !== "";
183
+ const marker = multiple
184
+ ? toggled
185
+ ? "[x]"
186
+ : "[ ]"
187
+ : `${String(optionCount + 1)}.`;
188
+ return {
189
+ index: optionCount,
190
+ kind: "other",
191
+ marker,
192
+ label: other.label,
193
+ description: undefined,
194
+ recommended: false,
195
+ highlighted: state.highlight === optionCount,
196
+ toggled,
197
+ destructive: false,
198
+ caption: other.label,
199
+ text: `${marker} ${other.label}`,
200
+ };
201
+ })(prompt.other),
202
+ ]),
203
+ ];
204
+ const typing = state.phase === "other";
205
+ const blankLabel = prompt.options
206
+ .find((option) => option.value === prompt.other?.blank)
207
+ ?.label.toLowerCase();
208
+ const escapeLabel = prompt.options
209
+ .find((option) => option.value === prompt.escape)
210
+ ?.label.toLowerCase();
211
+ const sending = state.phase === "sending";
212
+ const hasDetails = (prompt.details?.length ?? 0) > 0 || prompt.body !== undefined;
213
+ const hints = sending
214
+ ? []
215
+ : state.details
216
+ ? [
217
+ { keys: "d", label: "back", priority: 0 },
218
+ { keys: "Esc", label: "back", priority: 1 },
219
+ ]
220
+ : [
221
+ { keys: "↑↓", label: "move", priority: 1 },
222
+ ...(multiple && !typing
223
+ ? [{ keys: "Space", label: "toggle", priority: 1 }]
224
+ : []),
225
+ {
226
+ keys: "Enter",
227
+ label: typing && otherText === "" && blankLabel !== undefined
228
+ ? blankLabel
229
+ : multiple || typing
230
+ ? "send"
231
+ : state.highlight === optionCount
232
+ ? "write"
233
+ : "choose",
234
+ priority: 0,
235
+ },
236
+ ...(hasDetails
237
+ ? [{ keys: typing ? "Alt+D" : "d", label: "details", priority: 2 }]
238
+ : []),
239
+ {
240
+ keys: "Esc",
241
+ label: typing ? "back" : (escapeLabel ?? "later"),
242
+ priority: 2,
243
+ },
244
+ ];
245
+ return {
246
+ kind: context.kind,
247
+ label: `Agent ${noun[context.kind]} ${String(context.position)} of ${String(context.total)}`,
248
+ title: prompt.message,
249
+ counter: context.total > 1
250
+ ? `${String(context.position)} of ${String(context.total)}`
251
+ : undefined,
252
+ summary: prompt.summary,
253
+ facts: prompt.facts ?? [],
254
+ body: prompt.body,
255
+ bodyLabel: prompt.bodyLabel,
256
+ details: prompt.details ?? [],
257
+ detailsOpen: state.details,
258
+ rows,
259
+ other: prompt.other === undefined
260
+ ? undefined
261
+ : {
262
+ label: prompt.other.label,
263
+ placeholder: prompt.other.placeholder,
264
+ text: state.other,
265
+ focused: typing,
266
+ maxLength: prompt.other.maxLength,
267
+ },
268
+ multiple,
269
+ phase: state.phase,
270
+ canSend: !sending &&
271
+ !state.details &&
272
+ (multiple
273
+ ? state.toggled.size > 0 || otherText !== ""
274
+ : state.highlight < optionCount ||
275
+ otherText !== "" ||
276
+ prompt.other?.blank !== undefined),
277
+ status: sending
278
+ ? { tone: "info", text: "Sending…" }
279
+ : state.phase === "refused"
280
+ ? { tone: "error", text: state.refusal ?? "That answer was refused." }
281
+ : undefined,
282
+ hints,
283
+ };
284
+ }
285
+ /**
286
+ * The plain strings a renderer must show for a view, in reading order. The
287
+ * lockstep tests hold every surface to this list.
288
+ */
289
+ export function decisionViewText(view) {
290
+ if (view.detailsOpen)
291
+ return [view.title];
292
+ return [
293
+ view.title,
294
+ ...(view.counter === undefined ? [] : [view.counter]),
295
+ ...(view.summary === undefined ? [] : [view.summary]),
296
+ ...view.facts.map((fact) => `${fact.label} ${fact.value}`),
297
+ ...view.rows.map((row) => row.text),
298
+ ...(view.other === undefined || view.other.text === ""
299
+ ? []
300
+ : [view.other.text]),
301
+ ...(view.status === undefined ? [] : [view.status.text]),
302
+ ];
303
+ }
@@ -53,11 +53,14 @@ function progressItem(progress) {
53
53
  id: progress.messageId,
54
54
  kind: "assistant_message",
55
55
  pending: true,
56
- text: progress.text,
56
+ text: `${progress.textTruncated ? "…" : ""}${progress.text}`,
57
57
  };
58
58
  return progress.thinking === ""
59
59
  ? base
60
- : { ...base, thinking: progress.thinking };
60
+ : {
61
+ ...base,
62
+ thinking: `${progress.thinkingTruncated ? "…" : ""}${progress.thinking}`,
63
+ };
61
64
  }
62
65
  function collectAssistantBlocks(value) {
63
66
  const text = [];
@@ -97,11 +100,54 @@ function assistantMessageItem(id, value, blocks) {
97
100
  },
98
101
  };
99
102
  const withProviderError = value.stopReason === "error" && value.providerError !== undefined
100
- ? { ...item, providerError: value.providerError }
103
+ ? {
104
+ ...item,
105
+ text: value.errorMessage ?? "This run couldn’t finish.",
106
+ providerError: value.providerError,
107
+ }
101
108
  : item;
109
+ const withDetail = value.stopReason === "error" &&
110
+ value.providerError !== undefined &&
111
+ blocks.text.length > 0
112
+ ? {
113
+ ...withProviderError,
114
+ errorDetail: blocks.text.join("").slice(0, 160),
115
+ }
116
+ : withProviderError;
102
117
  return blocks.thinking.length === 0
103
- ? withProviderError
104
- : { ...withProviderError, thinking: blocks.thinking.join("\n") };
118
+ ? withDetail
119
+ : { ...withDetail, thinking: blocks.thinking.join("\n") };
120
+ }
121
+ /** Presentation-only folding; durable messages and their raw evidence remain intact. */
122
+ function collapseProviderErrors(rows, breakBefore) {
123
+ const items = [];
124
+ const aliases = new Map();
125
+ for (const row of rows) {
126
+ const prior = items.at(-1);
127
+ if (!breakBefore.has(row.id) &&
128
+ row.kind === "assistant_message" &&
129
+ row.isError &&
130
+ prior?.kind === "assistant_message" &&
131
+ prior.isError &&
132
+ row.providerError !== undefined &&
133
+ prior.providerError !== undefined &&
134
+ row.text === prior.text &&
135
+ row.errorDetail === prior.errorDetail &&
136
+ row.thinking === prior.thinking &&
137
+ row.author?.id === prior.author?.id &&
138
+ row.author?.name === prior.author?.name) {
139
+ items[items.length - 1] = {
140
+ ...prior,
141
+ repeatCount: (prior.repeatCount ?? 1) + 1,
142
+ // Details show the latest response, including its request id.
143
+ providerError: row.providerError,
144
+ };
145
+ aliases.set(row.id, prior.id);
146
+ }
147
+ else
148
+ items.push(row);
149
+ }
150
+ return { items, aliases };
105
151
  }
106
152
  function projectAssistant(id, value, author) {
107
153
  const blocks = collectAssistantBlocks(value);
@@ -281,11 +327,17 @@ export function buildDurableTimeline(messages, compactions = [], activity, autho
281
327
  }
282
328
  positionAnchors.set(position, anchor);
283
329
  }
330
+ const collapsed = collapseProviderErrors(grouped, breakBefore);
331
+ for (const [command, anchor] of commandAnchors)
332
+ commandAnchors.set(command, collapsed.aliases.get(anchor) ?? anchor);
333
+ for (const [position, anchor] of positionAnchors)
334
+ if (anchor !== null)
335
+ positionAnchors.set(position, collapsed.aliases.get(anchor) ?? anchor);
284
336
  const base = {
285
337
  commandAnchors,
286
338
  positionAnchors,
287
339
  completedMessageIds: completedMessageIds(messages),
288
- items: grouped,
340
+ items: collapsed.items,
289
341
  commandIds,
290
342
  requestIds: new Set(messages.flatMap((message) => message.requestId === undefined ? [] : [message.requestId])),
291
343
  };
@@ -337,7 +389,9 @@ export function buildTimelineTail(durable, progress, queued, admissions = [], au
337
389
  ? "failed"
338
390
  : admission.status === "accepted"
339
391
  ? "accepted"
340
- : "pending",
392
+ : admission.status === "queued-local"
393
+ ? "queued"
394
+ : "pending",
341
395
  delivery: admission.delivery,
342
396
  text: admission.content,
343
397
  };
@@ -22,6 +22,7 @@ export declare function createClientActions(options: Readonly<{
22
22
  readonly input?: {
23
23
  readonly [x: string]: import("effect/Schema").Json;
24
24
  } | undefined;
25
+ readonly awaiting?: "question";
25
26
  }, Readonly<{
26
27
  /** The same advertised intent is already in flight. */
27
28
  pending: boolean;
@@ -64,6 +64,11 @@ export function createClientActions(options) {
64
64
  !advertisedActions(entry).includes(action) ||
65
65
  (waiting && entry.value.waiting?.cancel !== action))
66
66
  return;
67
+ // A decision answers in the composer; the row only asks for it.
68
+ if (action.awaiting !== undefined) {
69
+ options.interactions.awaiting.focus();
70
+ return;
71
+ }
67
72
  const intent = key(entryId, action);
68
73
  if (intents.get(intent)?.pending)
69
74
  return;
@@ -76,7 +81,8 @@ export function createClientActions(options) {
76
81
  };
77
82
  if (action.input !== undefined)
78
83
  input = { ...input, arguments: action.input };
79
- outcome = await options.commands.execute(`${entryId}:${action.id}`, input);
84
+ // A client row belongs to the extension whose namespace is its entry.
85
+ outcome = await options.commands.execute({ namespace: entryId, id: action.id }, input);
80
86
  }
81
87
  catch (cause) {
82
88
  reportFailure("client row command", cause);
@@ -0,0 +1,3 @@
1
+ import type { ParakeetModel } from "../conversation.js";
2
+ /** A maintained catalog label, or the real identity rather than a guessed name. */
3
+ export declare function composerModelName(model: ParakeetModel): string;
@@ -0,0 +1,21 @@
1
+ import { ANTHROPIC_MODELS } from "@earendil-works/pi-ai/providers/anthropic.models";
2
+ import { CLOUDFLARE_WORKERS_AI_MODELS } from "@earendil-works/pi-ai/providers/cloudflare-workers-ai.models";
3
+ import { OPENAI_MODELS } from "@earendil-works/pi-ai/providers/openai.models";
4
+ import { OPENAI_CODEX_MODELS } from "@earendil-works/pi-ai/providers/openai-codex.models";
5
+ import { formatModelIdentity } from "./model-identity.js";
6
+ // Read the same maintained metadata as the providers, without loading their
7
+ // execution/auth adapters or maintaining another table of display names.
8
+ const names = new Map([
9
+ ...Object.values(ANTHROPIC_MODELS),
10
+ ...Object.values(CLOUDFLARE_WORKERS_AI_MODELS),
11
+ ...Object.values(OPENAI_MODELS),
12
+ ...Object.values(OPENAI_CODEX_MODELS),
13
+ ].map((model) => [
14
+ formatModelIdentity({ provider: model.provider, model: model.id }),
15
+ model.name,
16
+ ]));
17
+ /** A maintained catalog label, or the real identity rather than a guessed name. */
18
+ export function composerModelName(model) {
19
+ const identity = formatModelIdentity(model);
20
+ return names.get(identity) ?? identity;
21
+ }
@@ -0,0 +1,2 @@
1
+ /** Display text keeps shaping characters, never terminal programs or bidi overrides. */
2
+ export declare function sanitizeDisplayText(value: string): string;
@@ -0,0 +1,7 @@
1
+ import stripAnsi from "strip-ansi";
2
+ /** Display text keeps shaping characters, never terminal programs or bidi overrides. */
3
+ export function sanitizeDisplayText(value) {
4
+ return stripAnsi(value)
5
+ .replace(/\r\n?/gu, "\n")
6
+ .replace(/[\p{Cc}\u200b\u200e\u200f\u061c\u202a-\u202e\u2066-\u2069\u2060\ufeff]/gu, (character) => character === "\n" || character === "\t" ? character : "");
7
+ }
@@ -12,6 +12,8 @@ export type ParakeetDraftStorage = Readonly<{
12
12
  getItem: (key: string) => string | null;
13
13
  /** Atomically replace the journal; throw if it cannot be committed. */
14
14
  setItem: (key: string, value: string) => void;
15
+ /** Remove a journal, doing nothing when the key is absent. */
16
+ removeItem: (key: string) => void;
15
17
  }>;
16
18
  declare const journal: z.ZodObject<{
17
19
  draft: z.ZodString;
@@ -39,6 +41,10 @@ declare const journal: z.ZodObject<{
39
41
  }>;
40
42
  }, z.core.$strip>>;
41
43
  pending: z.ZodArray<z.ZodObject<{
44
+ status: z.ZodEnum<{
45
+ "queued-local": "queued-local";
46
+ admitting: "admitting";
47
+ }>;
42
48
  requestId: z.ZodString;
43
49
  content: z.ZodString;
44
50
  submittedDraft: z.ZodString;
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { reportFailure } from "../lib/failure.js";
2
3
  const images = z
3
4
  .array(z.object({
4
5
  type: z.literal("image"),
@@ -15,6 +16,7 @@ const recalledPrompt = z.object({
15
16
  delivery,
16
17
  });
17
18
  const pendingDraft = z.object({
19
+ status: z.enum(["queued-local", "admitting"]),
18
20
  requestId: z.string().min(1),
19
21
  content: z.string(),
20
22
  submittedDraft: z.string(),
@@ -31,15 +33,33 @@ const journal = z.object({
31
33
  });
32
34
  /** One owner-scoped journal commits pending text and composer text together. */
33
35
  export function draftJournal(scope, storage) {
34
- const key = `fungi.parakeet.drafts.v1:${scope}`;
36
+ const key = `fungi.parakeet.drafts.v2:${scope}`;
37
+ let discardObsolete = true;
35
38
  return {
36
39
  read() {
37
40
  if (storage === undefined)
38
41
  return { draft: "", pending: [] };
39
- const saved = storage.getItem(key);
40
- return saved === null
41
- ? { draft: "", pending: [] }
42
- : journal.parse(JSON.parse(saved));
42
+ if (discardObsolete) {
43
+ discardObsolete = false;
44
+ try {
45
+ // Delete preview data without interpreting its superseded grammar.
46
+ storage.removeItem(`fungi.parakeet.drafts.v1:${scope}`);
47
+ }
48
+ catch (cause) {
49
+ reportFailure("discard obsolete draft journal", cause);
50
+ }
51
+ }
52
+ try {
53
+ const saved = storage.getItem(key);
54
+ return saved === null
55
+ ? { draft: "", pending: [] }
56
+ : journal.parse(JSON.parse(saved));
57
+ }
58
+ catch (cause) {
59
+ // Preview journals are disposable; a broken load must not break chat.
60
+ reportFailure("read saved draft", cause);
61
+ return { draft: "", pending: [] };
62
+ }
43
63
  },
44
64
  write(value) {
45
65
  if (storage === undefined)
@@ -8,4 +8,5 @@ export declare function mergeTerminals(current: readonly SessionTerminal[], inco
8
8
  export declare function mergeCompactions(current: readonly SessionCompactionMarker[], incoming: readonly SessionCompactionMarker[]): readonly SessionCompactionMarker[];
9
9
  /** Accumulate compaction notices by identity; a frame never removes one. */
10
10
  export declare function mergeCompactionNotices(current: readonly SessionCompactionNotice[], incoming: readonly SessionCompactionNotice[]): readonly SessionCompactionNotice[];
11
+ /** Apply one already-decoded Shiitake frame to its authoritative projection. */
11
12
  export declare function applySessionFrame(current: SessionSnapshot, frame: SessionFrame): SessionSnapshot;
@@ -1,3 +1,4 @@
1
+ import { appendSessionProgress } from "@fungi.computer/shiitake/client";
1
2
  import { sameRunningWork } from "./session-view-state.js";
2
3
  const durableMessageIds = new WeakMap();
3
4
  /** Derive completed durable message identity once per immutable history revision. */
@@ -103,11 +104,11 @@ function sameActivity(left, right) {
103
104
  right.compaction !== null &&
104
105
  left.compaction.trigger === right.compaction.trigger))));
105
106
  }
106
- /** Apply one already-decoded Shiitake frame to its authoritative projection. */
107
107
  /** Extension slots are small JSON values; compare them by content. */
108
108
  function sameExtensionSlots(next, current) {
109
109
  return next === current || JSON.stringify(next) === JSON.stringify(current);
110
110
  }
111
+ /** Apply one already-decoded Shiitake frame to its authoritative projection. */
111
112
  export function applySessionFrame(current, frame) {
112
113
  if (frame.kind === "reset")
113
114
  return {
@@ -149,7 +150,7 @@ export function applySessionFrame(current, frame) {
149
150
  return activity === current.activity ? current : { ...current, activity };
150
151
  return {
151
152
  ...current,
152
- progress: frame.progress,
153
+ progress: appendSessionProgress(current.progress, frame.progress),
153
154
  activity,
154
155
  };
155
156
  }
@@ -7,6 +7,8 @@ export declare function sessionQueryOptions(session: ParakeetSession): import("@
7
7
  observation: "connecting" | "live";
8
8
  /** Last coherent durable Session state, absent before the first snapshot. */
9
9
  snapshot: SessionSnapshot | undefined;
10
+ /** Last successful watch connect or delivered frame, not a cached seed. */
11
+ lastReplyAt: number | undefined;
10
12
  /** One retryable user prompt, including image cargo, survives page refreshes. */
11
13
  latestPrompt: Readonly<{
12
14
  /** Owner-issued identity of the original prompt command. */
@@ -21,6 +23,8 @@ export declare function sessionQueryOptions(session: ParakeetSession): import("@
21
23
  observation: "connecting" | "live";
22
24
  /** Last coherent durable Session state, absent before the first snapshot. */
23
25
  snapshot: SessionSnapshot | undefined;
26
+ /** Last successful watch connect or delivered frame, not a cached seed. */
27
+ lastReplyAt: number | undefined;
24
28
  /** One retryable user prompt, including image cargo, survives page refreshes. */
25
29
  latestPrompt: Readonly<{
26
30
  /** Owner-issued identity of the original prompt command. */
@@ -36,6 +40,8 @@ export declare function sessionQueryOptions(session: ParakeetSession): import("@
36
40
  observation: "connecting" | "live";
37
41
  /** Last coherent durable Session state, absent before the first snapshot. */
38
42
  snapshot: SessionSnapshot | undefined;
43
+ /** Last successful watch connect or delivered frame, not a cached seed. */
44
+ lastReplyAt: number | undefined;
39
45
  /** One retryable user prompt, including image cargo, survives page refreshes. */
40
46
  latestPrompt: Readonly<{
41
47
  /** Owner-issued identity of the original prompt command. */
@@ -51,6 +57,8 @@ export declare function sessionQueryOptions(session: ParakeetSession): import("@
51
57
  observation: "connecting" | "live";
52
58
  /** Last coherent durable Session state, absent before the first snapshot. */
53
59
  snapshot: SessionSnapshot | undefined;
60
+ /** Last successful watch connect or delivered frame, not a cached seed. */
61
+ lastReplyAt: number | undefined;
54
62
  /** One retryable user prompt, including image cargo, survives page refreshes. */
55
63
  latestPrompt: Readonly<{
56
64
  /** Owner-issued identity of the original prompt command. */