@fungi.computer/parakeet 0.4.0 → 0.5.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 (55) hide show
  1. package/dist/client-contributions.d.ts +1 -0
  2. package/dist/commands/awaiting-driver.d.ts +20 -0
  3. package/dist/commands/awaiting-driver.js +121 -0
  4. package/dist/commands/builtins.js +6 -4
  5. package/dist/commands/interactions.d.ts +25 -0
  6. package/dist/commands/interactions.js +76 -2
  7. package/dist/commands/run.js +3 -3
  8. package/dist/commands/slash.d.ts +7 -3
  9. package/dist/commands/slash.js +2 -0
  10. package/dist/commands/use-slash-commands.d.ts +13 -0
  11. package/dist/commands/use-slash-commands.js +58 -1
  12. package/dist/components/ChatComposer.d.ts +2 -0
  13. package/dist/components/ChatComposer.js +150 -38
  14. package/dist/components/ClientRows.js +42 -26
  15. package/dist/components/CompletionMenu.d.ts +54 -0
  16. package/dist/components/CompletionMenu.js +52 -0
  17. package/dist/components/DecisionComposer.d.ts +30 -0
  18. package/dist/components/DecisionComposer.js +146 -0
  19. package/dist/components/InteractionPanel.d.ts +10 -1
  20. package/dist/components/InteractionPanel.js +9 -4
  21. package/dist/components/SlashMenu.d.ts +1 -3
  22. package/dist/components/SlashMenu.js +5 -13
  23. package/dist/components/TimelineItem.d.ts +1 -0
  24. package/dist/components/TimelineItem.js +3 -3
  25. package/dist/components/ToolGroup.d.ts +1 -0
  26. package/dist/components/ToolGroup.js +7 -7
  27. package/dist/conversation.d.ts +5 -1
  28. package/dist/conversation.js +72 -20
  29. package/dist/core/approval-decision.d.ts +198 -0
  30. package/dist/core/approval-decision.js +291 -0
  31. package/dist/core/awaiting-answer.d.ts +229 -0
  32. package/dist/core/awaiting-answer.js +303 -0
  33. package/dist/core/build-timeline.js +56 -5
  34. package/dist/core/client-actions.d.ts +1 -0
  35. package/dist/core/client-actions.js +7 -1
  36. package/dist/core/draft-storage.d.ts +4 -0
  37. package/dist/core/draft-storage.js +12 -5
  38. package/dist/core/session-query.d.ts +8 -0
  39. package/dist/core/session-query.js +13 -11
  40. package/dist/headless.d.ts +6 -2
  41. package/dist/headless.js +5 -1
  42. package/dist/parakeet.d.ts +6 -2
  43. package/dist/parakeet.js +15 -6
  44. package/dist/store/types.d.ts +5 -1
  45. package/dist/styles.css +395 -113
  46. package/dist/use-approval-awaiting.d.ts +22 -0
  47. package/dist/use-approval-awaiting.js +81 -0
  48. package/dist/use-decision.d.ts +20 -0
  49. package/dist/use-decision.js +79 -0
  50. package/docs/api/commands/slash.md +30 -26
  51. package/docs/api/headless.md +822 -41
  52. package/docs/api/index.md +13 -4
  53. package/package.json +13 -7
  54. package/src/completion-menu.css +71 -0
  55. package/src/styles.css +174 -46
@@ -0,0 +1,229 @@
1
+ import type { WhistlePromptOf } from "@fungi.computer/whistle/wire";
2
+ import type { WhistleInteractionHost } from "@fungi.computer/whistle/interaction";
3
+ import type { KeyHint } from "../commands/hints.js";
4
+ /** What the Agent needs from the person. */
5
+ export type AwaitingKind = "question" | "approval";
6
+ /**
7
+ * One decision waiting for the person. Its `host` is the renderer-neutral
8
+ * conversation that holds the current `decision` prompt and takes the answer.
9
+ */
10
+ export type AwaitingItem = Readonly<{
11
+ /** Stable identity, kept while the item waits. */
12
+ key: string;
13
+ /** Question or tool approval; both share one queue. */
14
+ kind: AwaitingKind;
15
+ /** When it joined the queue; items stay in arrival order. */
16
+ arrivedAt: number;
17
+ /** The conversation to draw and answer. */
18
+ host: WhistleInteractionHost;
19
+ }>;
20
+ /** Which item the composer shows and where it stands among all of them. */
21
+ export type AwaitingQueue = Readonly<{
22
+ /** Every waiting item in arrival order. */
23
+ items: readonly AwaitingItem[];
24
+ /** The first item not postponed with Later, in arrival order. */
25
+ current?: AwaitingItem | undefined;
26
+ /** One-based place of `current` among every waiting item. */
27
+ position: number;
28
+ /** Every waiting item, questions and approvals together. */
29
+ total: number;
30
+ }>;
31
+ /**
32
+ * Order the items by arrival and pick the one the composer shows. A postponed
33
+ * item leaves the composer but stays counted: it is the footer row.
34
+ */
35
+ export declare function awaitingQueue(items: readonly AwaitingItem[], later: ReadonlySet<string>): AwaitingQueue;
36
+ /**
37
+ * The item that owns the composer right now. An empty draft is taken over;
38
+ * a draft with text is never, unless the person asked for the decision.
39
+ */
40
+ export declare function awaitingTakeover(awaiting: AwaitingQueue & Readonly<{
41
+ forced: boolean;
42
+ }>, draft: string): AwaitingItem | undefined;
43
+ /** The decision prompt both renderers draw. */
44
+ export type DecisionPrompt = WhistlePromptOf<"decision">;
45
+ /** Where one decision is in its short life. */
46
+ export type DecisionPhase = "choose" | "other" | "confirm" | "sending" | "refused";
47
+ /** Everything the reducer remembers about one decision prompt. */
48
+ export type DecisionState = Readonly<{
49
+ /** Highlighted row: an option, or the Other row after the last option. */
50
+ highlight: number;
51
+ /** Option values toggled so far (multiple decisions only). */
52
+ toggled: ReadonlySet<string>;
53
+ /** Text typed on the Other row; kept when the person leaves the row. */
54
+ other: string;
55
+ /** What the person is doing now. */
56
+ phase: DecisionPhase;
57
+ /** Why the last answer came back, while `phase` is `refused`. */
58
+ refusal?: string | undefined;
59
+ /** The long text and details replace the rows. */
60
+ details: boolean;
61
+ }>;
62
+ /** What the person did, independent of which key or click did it. */
63
+ export type DecisionIntent = Readonly<{
64
+ /** Step the highlight, wrapping at either end. */
65
+ type: "move";
66
+ /** Which way. */
67
+ delta: -1 | 1;
68
+ }> | Readonly<{
69
+ /** Move the highlight to a numbered row; never answers. */
70
+ type: "pick";
71
+ /** One-based row number, the Other row last. */
72
+ number: number;
73
+ }> | Readonly<{
74
+ /** Toggle the highlighted option of a multiple decision. */
75
+ type: "toggle";
76
+ }> | Readonly<{
77
+ /** Replace the Other line's text, e.g. from a field. */
78
+ type: "type";
79
+ /** The whole text now. */
80
+ text: string;
81
+ }> | Readonly<{
82
+ /** Add typed characters to the Other line and move onto it. */
83
+ type: "append";
84
+ /** The characters. */
85
+ text: string;
86
+ }> | Readonly<{
87
+ /** Confirm: the one way to answer. */
88
+ type: "enter";
89
+ }> | Readonly<{
90
+ /** Step back one level; from the option list it postpones the decision. */
91
+ type: "escape";
92
+ }> | Readonly<{
93
+ /** Show or hide the long text and details. */
94
+ type: "details";
95
+ }> | Readonly<{
96
+ /** The owner of the decision refused the answer. */
97
+ type: "refused";
98
+ /** Why, in words for the person. */
99
+ message: string;
100
+ }>;
101
+ /** What the owner of the decision must now do. */
102
+ export type DecisionEffect = Readonly<{
103
+ /** Send this answer. */
104
+ type: "answer";
105
+ /** The option values picked. */
106
+ selected: string[];
107
+ /** What the person wrote on the Other line, if anything. */
108
+ text?: string;
109
+ }> | Readonly<{
110
+ /** Postpone: leave the composer, stay waiting, answer nothing. */
111
+ type: "later";
112
+ }> | Readonly<{
113
+ /** The reducer stepped back one level; nothing for the owner to do. */
114
+ type: "back";
115
+ }>;
116
+ /** The next state, and what the decision's owner must do about the intent. */
117
+ export type DecisionOutcome = Readonly<{
118
+ /** The state to keep. */
119
+ state: DecisionState;
120
+ /** What the owner must do now, when anything. */
121
+ effect?: DecisionEffect;
122
+ }>;
123
+ /** The state a decision starts in. */
124
+ export declare function initialDecisionState(): DecisionState;
125
+ /**
126
+ * The one set of rules both renderers follow. Number keys only move the
127
+ * highlight, Enter is the only way to answer, and Esc steps back one level
128
+ * until it reaches the option list, where it postpones the decision.
129
+ */
130
+ export declare function reduceDecision(state: DecisionState, intent: DecisionIntent, prompt: DecisionPrompt): DecisionOutcome;
131
+ /** One drawn row of a decision. */
132
+ export type DecisionRowView = Readonly<{
133
+ /** Position among the rows; the last one is Other. */
134
+ index: number;
135
+ /** An offered choice, or the always-present Other row. */
136
+ kind: "option" | "other";
137
+ /** The number, or `[x]` / `[ ]` for a multiple decision. */
138
+ marker: string;
139
+ /** The choice, or the Other row's label. */
140
+ label: string;
141
+ /** Why or what, shown for the highlighted row when space is short. */
142
+ description?: string | undefined;
143
+ /** The first choice is the recommended one. */
144
+ recommended: boolean;
145
+ /** The row the Enter key acts on. */
146
+ highlighted: boolean;
147
+ /** A multiple decision's pick, or Other with text. */
148
+ toggled: boolean;
149
+ /** Drawn in the error tone. */
150
+ destructive: boolean;
151
+ /** The row's words without its marker, for a renderer that draws the marker as a control. */
152
+ caption: string;
153
+ /** The whole row as one plain line, marker first; renderers must show it. */
154
+ text: string;
155
+ }>;
156
+ /** What both renderers must draw for one decision. */
157
+ export type DecisionView = Readonly<{
158
+ /** Question or approval. */
159
+ kind: AwaitingKind;
160
+ /** Accessible name of the region, e.g. `Agent question 1 of 3`. */
161
+ label: string;
162
+ /** The prompt's title. */
163
+ title: string;
164
+ /** `1 of 3`, shown only when more than one decision waits. */
165
+ counter?: string | undefined;
166
+ /** One plain sentence under the title. */
167
+ summary?: string | undefined;
168
+ /** Short labelled lines under the summary, as `label value`. */
169
+ facts: readonly Readonly<{
170
+ /** What the value is. */
171
+ label: string;
172
+ /** The value. */
173
+ value: string;
174
+ }>[];
175
+ /** Long text the person wrote or the Agent asked, cut by renderers. */
176
+ body?: string | undefined;
177
+ /** A short name for the body. */
178
+ bodyLabel?: string | undefined;
179
+ /** Everything else, shown on demand. */
180
+ details: readonly Readonly<{
181
+ /** What the value is. */
182
+ label: string;
183
+ /** The value. */
184
+ value: string;
185
+ }>[];
186
+ /** The long text and details replace the rows. */
187
+ detailsOpen: boolean;
188
+ /** The options in order, then the Other row. */
189
+ rows: readonly DecisionRowView[];
190
+ /** The Other row's field; absent when only the options are offered. */
191
+ other?: Readonly<{
192
+ /** The row's name, e.g. `Other…`. */
193
+ label: string;
194
+ /** Hint shown in the empty field. */
195
+ placeholder?: string | undefined;
196
+ /** What the person has written. */
197
+ text: string;
198
+ /** The person is typing on this row. */
199
+ focused: boolean;
200
+ /** The longest answer accepted. */
201
+ maxLength: number;
202
+ }> | undefined;
203
+ /** More than one option may be picked. */
204
+ multiple: boolean;
205
+ /** What the person is doing now. */
206
+ phase: DecisionPhase;
207
+ /** Whether Enter would send an answer now. */
208
+ canSend: boolean;
209
+ /** `Sending…` or the refusal, marked in words rather than colour alone. */
210
+ status?: Readonly<{
211
+ /** Plain or alarming. */
212
+ tone: "info" | "error";
213
+ /** The words. */
214
+ text: string;
215
+ }> | undefined;
216
+ /** Keys that work now, with Esc named for what it will do. */
217
+ hints: readonly KeyHint[];
218
+ }>;
219
+ /** Project a decision's state into the view every renderer draws. */
220
+ export declare function decisionView(state: DecisionState, prompt: DecisionPrompt, context: Readonly<{
221
+ kind: AwaitingKind;
222
+ position: number;
223
+ total: number;
224
+ }>): DecisionView;
225
+ /**
226
+ * The plain strings a renderer must show for a view, in reading order. The
227
+ * lockstep tests hold every surface to this list.
228
+ */
229
+ export declare function decisionViewText(view: DecisionView): readonly string[];
@@ -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
+ }
@@ -97,11 +97,54 @@ function assistantMessageItem(id, value, blocks) {
97
97
  },
98
98
  };
99
99
  const withProviderError = value.stopReason === "error" && value.providerError !== undefined
100
- ? { ...item, providerError: value.providerError }
100
+ ? {
101
+ ...item,
102
+ text: value.errorMessage ?? "This run couldn’t finish.",
103
+ providerError: value.providerError,
104
+ }
101
105
  : item;
106
+ const withDetail = value.stopReason === "error" &&
107
+ value.providerError !== undefined &&
108
+ blocks.text.length > 0
109
+ ? {
110
+ ...withProviderError,
111
+ errorDetail: blocks.text.join("").slice(0, 160),
112
+ }
113
+ : withProviderError;
102
114
  return blocks.thinking.length === 0
103
- ? withProviderError
104
- : { ...withProviderError, thinking: blocks.thinking.join("\n") };
115
+ ? withDetail
116
+ : { ...withDetail, thinking: blocks.thinking.join("\n") };
117
+ }
118
+ /** Presentation-only folding; durable messages and their raw evidence remain intact. */
119
+ function collapseProviderErrors(rows, breakBefore) {
120
+ const items = [];
121
+ const aliases = new Map();
122
+ for (const row of rows) {
123
+ const prior = items.at(-1);
124
+ if (!breakBefore.has(row.id) &&
125
+ row.kind === "assistant_message" &&
126
+ row.isError &&
127
+ prior?.kind === "assistant_message" &&
128
+ prior.isError &&
129
+ row.providerError !== undefined &&
130
+ prior.providerError !== undefined &&
131
+ row.text === prior.text &&
132
+ row.errorDetail === prior.errorDetail &&
133
+ row.thinking === prior.thinking &&
134
+ row.author?.id === prior.author?.id &&
135
+ row.author?.name === prior.author?.name) {
136
+ items[items.length - 1] = {
137
+ ...prior,
138
+ repeatCount: (prior.repeatCount ?? 1) + 1,
139
+ // Details show the latest response, including its request id.
140
+ providerError: row.providerError,
141
+ };
142
+ aliases.set(row.id, prior.id);
143
+ }
144
+ else
145
+ items.push(row);
146
+ }
147
+ return { items, aliases };
105
148
  }
106
149
  function projectAssistant(id, value, author) {
107
150
  const blocks = collectAssistantBlocks(value);
@@ -281,11 +324,17 @@ export function buildDurableTimeline(messages, compactions = [], activity, autho
281
324
  }
282
325
  positionAnchors.set(position, anchor);
283
326
  }
327
+ const collapsed = collapseProviderErrors(grouped, breakBefore);
328
+ for (const [command, anchor] of commandAnchors)
329
+ commandAnchors.set(command, collapsed.aliases.get(anchor) ?? anchor);
330
+ for (const [position, anchor] of positionAnchors)
331
+ if (anchor !== null)
332
+ positionAnchors.set(position, collapsed.aliases.get(anchor) ?? anchor);
284
333
  const base = {
285
334
  commandAnchors,
286
335
  positionAnchors,
287
336
  completedMessageIds: completedMessageIds(messages),
288
- items: grouped,
337
+ items: collapsed.items,
289
338
  commandIds,
290
339
  requestIds: new Set(messages.flatMap((message) => message.requestId === undefined ? [] : [message.requestId])),
291
340
  };
@@ -337,7 +386,9 @@ export function buildTimelineTail(durable, progress, queued, admissions = [], au
337
386
  ? "failed"
338
387
  : admission.status === "accepted"
339
388
  ? "accepted"
340
- : "pending",
389
+ : admission.status === "queued-local"
390
+ ? "queued"
391
+ : "pending",
341
392
  delivery: admission.delivery,
342
393
  text: admission.content,
343
394
  };
@@ -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);
@@ -39,6 +39,10 @@ declare const journal: z.ZodObject<{
39
39
  }>;
40
40
  }, z.core.$strip>>;
41
41
  pending: z.ZodArray<z.ZodObject<{
42
+ status: z.ZodEnum<{
43
+ "queued-local": "queued-local";
44
+ admitting: "admitting";
45
+ }>;
42
46
  requestId: z.ZodString;
43
47
  content: z.ZodString;
44
48
  submittedDraft: z.ZodString;