@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,291 @@
1
+ const shorten = (value, width) => {
2
+ if (value.length <= width)
3
+ return value;
4
+ const left = Math.max(1, Math.floor((width - 1) / 2));
5
+ return `${value.slice(0, left)}…${value.slice(-(width - left - 1))}`;
6
+ };
7
+ const lead = (value, width) => value.length <= width ? value : `${value.slice(0, width - 1)}…`;
8
+ const entries = (pairs) => pairs.map(([label, value]) => ({ label, value }));
9
+ function recordDetails(view, now) {
10
+ const { record } = view;
11
+ const remaining = Math.max(0, record.expiresAt - now);
12
+ return [
13
+ [
14
+ "Agent",
15
+ `${record.requester.displayName} · ${record.requester.agentId ?? "none"}`,
16
+ ],
17
+ ["Session", record.requester.sessionId ?? "none"],
18
+ ["Requester", record.requester.principalId],
19
+ ["Request", `${record.module}.${record.operation} · ${record.access}`],
20
+ [
21
+ "Input",
22
+ `${record.description.inputBytes.toLocaleString()} bytes · SHA-256 ${record.description.inputSha256.slice(0, 8)}…${record.description.inputSha256.slice(-6)}`,
23
+ ],
24
+ [
25
+ "Expires",
26
+ `in ${String(Math.floor(remaining / 3_600_000))} h ${String(Math.floor((remaining % 3_600_000) / 60_000))} m`,
27
+ ],
28
+ ["Record", `${record.recordId} · revision ${String(record.revision)}`],
29
+ ];
30
+ }
31
+ function undoDetail(view) {
32
+ return [
33
+ "Undo",
34
+ view.record.reversibility === "irreversible-external"
35
+ ? "Can't be undone"
36
+ : "Can be undone",
37
+ ];
38
+ }
39
+ function connectedToolCard(view, now) {
40
+ const { record } = view;
41
+ return {
42
+ content: {
43
+ title: `Allow ${record.module} · ${record.operation}?`,
44
+ summary: `${record.requester.displayName} wants to use a connected tool.`,
45
+ facts: [
46
+ {
47
+ label: "Effect",
48
+ value: (record.access === "write"
49
+ ? "Changes data in Fungi"
50
+ : "Changes things outside Fungi") +
51
+ (record.reversibility === "irreversible-external"
52
+ ? " · can't be undone"
53
+ : ""),
54
+ },
55
+ ],
56
+ details: entries([undoDetail(view), ...recordDetails(view, now)]),
57
+ },
58
+ };
59
+ }
60
+ function escapeCard(view, review, width, home, now) {
61
+ const prefix = review.prefix?.join(" ");
62
+ const offered = prefix !== undefined && review.alwaysAllowUnavailableReason === undefined;
63
+ const command = review.command;
64
+ const title = `Allow \`${command}\`?`.length <= width
65
+ ? `Allow \`${command}\`?`
66
+ : "Allow this command?";
67
+ const folder = review.cwd === home
68
+ ? "~"
69
+ : review.cwd.startsWith(`${home}/`)
70
+ ? `~${review.cwd.slice(home.length)}`
71
+ : review.cwd;
72
+ const reasonWidth = Math.max(8, (width - 8) * 2 - 2);
73
+ const reason = review.justification.length <= reasonWidth
74
+ ? `“${review.justification}”`
75
+ : `“${review.justification.slice(0, reasonWidth - 1)}…”`;
76
+ const facts = [
77
+ ["Folder", shorten(folder, Math.max(8, width - 10))],
78
+ ["Why", reason],
79
+ ];
80
+ if (review.kind === "extension-install")
81
+ facts.push([
82
+ "Note",
83
+ "Extensions run inside Fungi with access to every Agent.",
84
+ ]);
85
+ // The complete host-provided coverage stays in Details; the main card has a
86
+ // one-line preview so long Git option lists cannot displace the choices.
87
+ if (offered && review.alwaysAllowCoverage !== undefined)
88
+ facts.push([
89
+ "Scope",
90
+ lead(review.alwaysAllowCoverage, Math.max(8, width - 12)),
91
+ ]);
92
+ if (!offered && review.warning !== undefined)
93
+ facts.push(["Warning", review.warning]);
94
+ const always = !offered
95
+ ? undefined
96
+ : `Always allow “${shorten(prefix, Math.max(8, width - 5 - "Always allow “” in this project…".length))}” in this project…`;
97
+ const details = [["Command", command]];
98
+ if (review.argv !== undefined)
99
+ details.push(["Arguments", JSON.stringify(review.argv)]);
100
+ details.push(["Folder", review.cwd], ["Runs", "Outside the sandbox, as you"], undoDetail(view));
101
+ if (!offered && review.alwaysAllowUnavailableReason !== undefined)
102
+ details.push(["Rule", review.alwaysAllowUnavailableReason]);
103
+ else if (review.form === "shell")
104
+ details.push(["Rule", "Shell command: no Always-allow rule can match it."]);
105
+ else if (offered) {
106
+ details.push([
107
+ "Rule",
108
+ `“${prefix}” would match \`${prefix} …\` in this project`,
109
+ ]);
110
+ if (review.alwaysAllowCoverage !== undefined)
111
+ details.push(["Coverage", review.alwaysAllowCoverage]);
112
+ if (review.risk !== undefined)
113
+ details.push(["Risk", review.risk.detail]);
114
+ }
115
+ if (!offered && review.warning !== undefined)
116
+ // Details replace the preview by label; do not repeat the same
117
+ // once-only warning as a second "Risk" row.
118
+ details.push(["Warning", review.warning]);
119
+ details.push(...recordDetails(view, now));
120
+ return {
121
+ content: {
122
+ title,
123
+ body: title === "Allow this command?" ? command : undefined,
124
+ bodyLabel: title === "Allow this command?" ? "Command" : undefined,
125
+ summary: `${view.record.requester.displayName} wants to run this outside the sandbox, as you.`,
126
+ facts: entries(facts),
127
+ details: entries(details),
128
+ },
129
+ always: always === undefined ? undefined : lead(always, Math.max(12, width - 5)),
130
+ confirmation: !offered
131
+ ? undefined
132
+ : {
133
+ title: `Always allow “${prefix}” in this project?`,
134
+ body: lead(review.alwaysAllowCoverage ??
135
+ `Any Workspace Agent here can then run \`${prefix} …\` without asking.`, Math.max(12, (width - 8) * 2 - 2)),
136
+ },
137
+ confirmWarning: offered ? review.risk?.summary : undefined,
138
+ confirmYes: !offered
139
+ ? undefined
140
+ : `Always allow “${prefix}” and run it now`,
141
+ };
142
+ }
143
+ /**
144
+ * The words of one approval. A connected tool gets its effect; a command the
145
+ * Agent wants to run outside the sandbox gets its folder, reason and rule.
146
+ * `home` shortens paths inside it to `~`; `now` dates the expiry.
147
+ */
148
+ export function approvalCardCopy(view, review, options) {
149
+ // A prompt's title is at most sixty characters.
150
+ const width = Math.min(options.width, 60);
151
+ return review === undefined
152
+ ? connectedToolCard(view, options.now)
153
+ : escapeCard(view, review, width, options.home, options.now);
154
+ }
155
+ /** What an answer was refused for, in words for the person. */
156
+ export function approvalRefusal(reason, decision) {
157
+ switch (reason) {
158
+ case "stale-revision":
159
+ case "already-answered":
160
+ return "This request changed. Look again before answering.";
161
+ case "expired":
162
+ return "This request expired. Nothing ran.";
163
+ case "not-eligible":
164
+ if (decision === "always-allow")
165
+ return "Always allow isn't available for this command. Approve once or deny.";
166
+ break;
167
+ default:
168
+ break;
169
+ }
170
+ return `Couldn't answer (${reason}). Nothing ran.`;
171
+ }
172
+ const failureMessage = "Couldn't send your answer. It may not have been recorded.";
173
+ /**
174
+ * Ask the person about one approval through a conversation's decision
175
+ * prompts, then send the answer. Nothing is sent until Enter answers, Always
176
+ * allow and the family rule each need a second, separate confirmation whose
177
+ * default is Back, and an
178
+ * answer is only sent while the record is still the one that was shown.
179
+ */
180
+ export async function askApproval(interaction, input) {
181
+ const { view, source } = input;
182
+ const card = approvalCardCopy(view, source.review, {
183
+ width: input.width,
184
+ home: input.home,
185
+ now: input.now,
186
+ });
187
+ const record = view.record;
188
+ try {
189
+ let decision;
190
+ let denialNote;
191
+ let familyOn = false;
192
+ while (decision === undefined) {
193
+ const answer = await interaction.prompt({
194
+ type: "decision",
195
+ message: card.content.title,
196
+ summary: familyOn
197
+ ? "Automatic approval is on for this family."
198
+ : card.content.summary,
199
+ facts: card.content.facts?.map((fact) => ({ ...fact })),
200
+ body: card.content.body,
201
+ bodyLabel: card.content.bodyLabel,
202
+ details: card.content.details?.map((entry) => ({ ...entry })),
203
+ options: [
204
+ { value: "approve", label: "Approve once" },
205
+ ...(card.always === undefined
206
+ ? []
207
+ : [{ value: "always", label: card.always }]),
208
+ ...(source.family === undefined || familyOn
209
+ ? []
210
+ : [{ value: "family", label: source.family.label }]),
211
+ { value: "deny", label: "Deny" },
212
+ ],
213
+ other: {
214
+ label: "Deny and say why…",
215
+ placeholder: "A short reason for the Agent (optional)",
216
+ maxLength: input.noteLimit,
217
+ blank: "deny",
218
+ },
219
+ });
220
+ if (!input.stillCurrent())
221
+ return { kind: "abandoned" };
222
+ if (answer.text !== undefined) {
223
+ denialNote = answer.text.trim() || undefined;
224
+ decision = "deny";
225
+ }
226
+ else if (answer.selected[0] === "always") {
227
+ const confirmation = card.confirmation;
228
+ const confirmed = await interaction.prompt({
229
+ type: "decision",
230
+ message: confirmation?.title ?? card.content.title,
231
+ summary: card.confirmWarning,
232
+ body: confirmation?.body,
233
+ options: [
234
+ { value: "back", label: "Back" },
235
+ { value: "always-allow", label: card.confirmYes ?? "Always allow" },
236
+ ],
237
+ escape: "back",
238
+ });
239
+ if (!input.stillCurrent())
240
+ return { kind: "abandoned" };
241
+ if (confirmed.selected[0] === "always-allow")
242
+ decision = "always-allow";
243
+ }
244
+ else if (answer.selected[0] === "family" &&
245
+ source.family !== undefined) {
246
+ // A standing rule, like Always allow: it needs its own confirmation.
247
+ const confirmed = await interaction.prompt({
248
+ type: "decision",
249
+ message: `${source.family.label}?`,
250
+ summary: `Later ${record.module} requests that ${record.access === "write" ? "change data in Fungi" : "reach outside Fungi"} will run without asking.`,
251
+ options: [
252
+ { value: "back", label: "Back" },
253
+ { value: "family", label: source.family.label },
254
+ ],
255
+ escape: "back",
256
+ });
257
+ if (!input.stillCurrent())
258
+ return { kind: "abandoned" };
259
+ if (confirmed.selected[0] === "family") {
260
+ familyOn = await source.family.enable();
261
+ if (!input.stillCurrent())
262
+ return { kind: "abandoned" };
263
+ }
264
+ }
265
+ else if (answer.selected[0] === "approve")
266
+ decision = "approve";
267
+ else if (answer.selected[0] === "deny")
268
+ decision = "deny";
269
+ }
270
+ const pinned = {
271
+ recordId: record.recordId,
272
+ decision,
273
+ expectedRevision: record.revision,
274
+ };
275
+ const result = await source.answer(denialNote === undefined ? pinned : { ...pinned, denialNote });
276
+ if (!input.stillCurrent())
277
+ return { kind: "abandoned" };
278
+ return result.kind === "refused"
279
+ ? {
280
+ kind: "refused",
281
+ message: approvalRefusal(result.reason ?? "unknown", decision),
282
+ }
283
+ : { kind: "answered" };
284
+ }
285
+ catch (cause) {
286
+ if (interaction.signal.aborted)
287
+ return { kind: "abandoned" };
288
+ input.onFailure(cause);
289
+ return { kind: "refused", message: failureMessage };
290
+ }
291
+ }
@@ -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[];