@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
@@ -9,6 +9,7 @@ import { atom, createStore, } from "jotai/vanilla";
9
9
  import { atomWithInfiniteQuery, atomWithMutation, atomWithQuery, } from "jotai-tanstack-query";
10
10
  import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useSyncExternalStore, } from "react";
11
11
  import { buildDurableTimeline, buildTimelineTail, } from "./core/build-timeline.js";
12
+ import { overlayToolOutput } from "./core/tool-groups.js";
12
13
  import { mergeCompactionNotices, mergeCompactions, mergeMessages, } from "./core/session-projection.js";
13
14
  import { sessionHistoryQueryOptions, sessionQueryOptions, } from "./core/session-query.js";
14
15
  import { sameSessionViewState, sessionActivityStatus, sessionViewState, } from "./core/session-view-state.js";
@@ -66,7 +67,7 @@ function composerStatus(snapshot, admissions, failure) {
66
67
  return status;
67
68
  if (failure !== undefined || view.kind === "error")
68
69
  return "error";
69
- if (admissions.some(({ status: state }) => state === "accepted"))
70
+ if (admissions.some(({ status: state }) => state === "accepted" || state === "queued-local"))
70
71
  return "queued";
71
72
  if (admissions.some(({ status: state }) => state === "admitting"))
72
73
  return "sending";
@@ -100,12 +101,15 @@ function acquireDraftStorage(supplied) {
100
101
  const browser = globalThis;
101
102
  const storage = browser.localStorage;
102
103
  return storage === undefined
103
- ? { status: "unavailable" }
104
+ ? {
105
+ status: "unavailable",
106
+ cause: new Error("Browser draft storage is unavailable"),
107
+ }
104
108
  : { status: "available", storage };
105
109
  }
106
- catch {
110
+ catch (cause) {
107
111
  // An opaque-origin sandbox denies even reading the browser's getter.
108
- return { status: "unavailable" };
112
+ return { status: "unavailable", cause };
109
113
  }
110
114
  }
111
115
  function createConversationAtoms(session, queryClient, authorOf, interactions, draftStorage, draftScope, contributions, shown) {
@@ -113,13 +117,14 @@ function createConversationAtoms(session, queryClient, authorOf, interactions, d
113
117
  const saved = journal.read();
114
118
  let mounted = false;
115
119
  const timers = new Map();
116
- const recoveryReasons = new Map(saved.pending.map(({ requestId }) => [
117
- requestId,
118
- "Closed before confirmation",
119
- ]));
120
+ const recoveryReasons = new Map();
121
+ for (const pending of saved.pending)
122
+ if (pending.status === "admitting")
123
+ recoveryReasons.set(pending.requestId, "Closed before confirmation");
120
124
  const recovering = new Set();
121
125
  const queryAtom = atomWithQuery(() => sessionQueryOptions(session), () => queryClient);
122
126
  const snapshotAtom = atom((get) => get(queryAtom).data?.snapshot);
127
+ const lastReplyAtAtom = atom((get) => get(queryAtom).data?.lastReplyAt);
123
128
  const pinnedHistoryAtom = atom(undefined);
124
129
  const historyBeforeAtom = atom((get) => (get(pinnedHistoryAtom) ?? get(snapshotAtom))?.oldestCursor ?? null);
125
130
  const historyAtom = atomWithInfiniteQuery((get) => sessionHistoryQueryOptions(session, get(historyBeforeAtom)), () => queryClient);
@@ -279,10 +284,7 @@ function createConversationAtoms(session, queryClient, authorOf, interactions, d
279
284
  const recallStateAtom = atom({ kind: "idle" });
280
285
  let recallOwner;
281
286
  const draftRevisionAtom = atom(0);
282
- const rawAdmissionsAtom = atom(saved.pending.map((pending) => ({
283
- ...pending,
284
- status: "admitting",
285
- })));
287
+ const rawAdmissionsAtom = atom(saved.pending);
286
288
  const persistAtom = atom(null, (get, _set, state) => {
287
289
  // Unavailable storage is session-only, not a pretend durable journal.
288
290
  // A supplied adapter's write failures still refuse durable operations.
@@ -299,16 +301,19 @@ function createConversationAtoms(session, queryClient, authorOf, interactions, d
299
301
  recall: state?.recall === null
300
302
  ? undefined
301
303
  : (state?.recall ?? get(recallReservationAtom)),
302
- pending: (state?.admissions ?? get(rawAdmissionsAtom))
303
- .filter(({ status }) => status === "admitting")
304
- .map(({ requestId, content, submittedDraft, delivery, images, restoreDraft, }) => ({
305
- requestId,
306
- content,
307
- submittedDraft,
308
- delivery,
309
- images,
310
- restoreDraft,
311
- })),
304
+ pending: (state?.admissions ?? get(rawAdmissionsAtom)).flatMap(({ status, requestId, content, submittedDraft, delivery, images, restoreDraft, }) => status === "admitting" || status === "queued-local"
305
+ ? [
306
+ {
307
+ status,
308
+ requestId,
309
+ content,
310
+ submittedDraft,
311
+ delivery,
312
+ images,
313
+ restoreDraft,
314
+ },
315
+ ]
316
+ : []),
312
317
  });
313
318
  });
314
319
  const recallAtom = atom(null, (get, set) => {
@@ -436,6 +441,24 @@ function createConversationAtoms(session, queryClient, authorOf, interactions, d
436
441
  set(draftAtom, value);
437
442
  set(admissionErrorAtom, undefined);
438
443
  });
444
+ const setImagesAtom = atom(null, (get, set, images) => {
445
+ const next = images === undefined || images.length === 0 ? undefined : images;
446
+ try {
447
+ set(persistAtom, {
448
+ draftPayload: { images: next, delivery: get(draftDeliveryAtom) },
449
+ });
450
+ }
451
+ catch (cause) {
452
+ reportFailure("save draft images", cause);
453
+ interactions.toast({
454
+ variant: "error",
455
+ message: `Images not saved: ${errorMessage(cause)}`,
456
+ });
457
+ }
458
+ set(draftImagesAtom, next);
459
+ set(draftRevisionAtom, (revision) => revision + 1);
460
+ set(admissionErrorAtom, undefined);
461
+ });
439
462
  /** Consume only the exact draft a command submitted. */
440
463
  const clearSubmittedDraftAtom = atom(null, (get, set, submitted) => {
441
464
  if (get(draftRevisionAtom) !== submitted.revision ||
@@ -516,7 +539,9 @@ function createConversationAtoms(session, queryClient, authorOf, interactions, d
516
539
  hasMoreHistory: get(hasMoreHistoryAtom),
517
540
  hasNewerHistory: get(hasNewerHistoryAtom),
518
541
  loadingOlderHistory: get(loadingOlderHistoryAtom),
519
- settled: durable.items,
542
+ settled: overlayToolOutput(durable.items, get(toolActivityAtom) === undefined
543
+ ? []
544
+ : (get(snapshotAtom)?.toolOutput ?? [])),
520
545
  tail: get(tailAtom),
521
546
  };
522
547
  return durable.model === undefined || durable.usage === undefined
@@ -616,6 +641,10 @@ function createConversationAtoms(session, queryClient, authorOf, interactions, d
616
641
  set(draftDeliveryAtom, undefined);
617
642
  set(setDraftAtom, "");
618
643
  }
644
+ // The offline prompt is saved alongside the draft. Await a fresh owner
645
+ // baseline before attempting admission.
646
+ if (admission.status === "queued-local")
647
+ return;
619
648
  const prompt = {
620
649
  type: "prompt",
621
650
  text: admission.content,
@@ -730,6 +759,20 @@ function createConversationAtoms(session, queryClient, authorOf, interactions, d
730
759
  const maintainRecoveryAtom = atom(null, (get, set) => {
731
760
  const observation = get(observationAtom).status;
732
761
  const admissions = get(rawAdmissionsAtom);
762
+ if (observation === "live") {
763
+ if (get(admissionErrorAtom) === "offline_queue_full")
764
+ set(admissionErrorAtom, undefined);
765
+ const queued = admissions.find(({ status }) => status === "queued-local");
766
+ if (queued !== undefined) {
767
+ const sending = {
768
+ ...queued,
769
+ status: "admitting",
770
+ };
771
+ set(rawAdmissionsAtom, (current) => current.map((candidate) => candidate.requestId === queued.requestId ? sending : candidate));
772
+ // Preserve the idempotent request id when reconnect admits the prompt.
773
+ set(dispatchAdmissionAtom, { admission: sending, consumeDraft: false });
774
+ }
775
+ }
733
776
  for (const [id, timer] of timers) {
734
777
  if (!admissions.some(({ requestId, status }) => requestId === id && status === "admitting")) {
735
778
  clearTimeout(timer);
@@ -774,6 +817,12 @@ function createConversationAtoms(session, queryClient, authorOf, interactions, d
774
817
  return;
775
818
  const admissions = get(admissionsAtom);
776
819
  const unresolved = admissions.filter(({ status }) => status !== "failed");
820
+ const offline = get(observationAtom).status !== "live";
821
+ if (offline &&
822
+ unresolved.some(({ status }) => status === "queued-local")) {
823
+ set(admissionErrorAtom, "offline_queue_full");
824
+ return;
825
+ }
777
826
  if (unresolved.length >= MAX_UNRESOLVED_LOCAL_ADMISSIONS) {
778
827
  set(admissionErrorAtom, "queue_full");
779
828
  return;
@@ -785,7 +834,7 @@ function createConversationAtoms(session, queryClient, authorOf, interactions, d
785
834
  requestId: nanoid(),
786
835
  submittedDraft: draft,
787
836
  restoreDraft: true,
788
- status: "admitting",
837
+ status: offline ? "queued-local" : "admitting",
789
838
  };
790
839
  set(rawAdmissionsAtom, [...admissions, admission]);
791
840
  set(latestSubmissionIdAtom, admission.requestId);
@@ -991,6 +1040,7 @@ function createConversationAtoms(session, queryClient, authorOf, interactions, d
991
1040
  jumpToLatestAtom,
992
1041
  loadOlderHistoryAtom,
993
1042
  loadNewerHistoryAtom,
1043
+ lastReplyAtAtom,
994
1044
  observationAtom,
995
1045
  observedCommandIdsAtom,
996
1046
  rawAdmissionsAtom,
@@ -1002,6 +1052,7 @@ function createConversationAtoms(session, queryClient, authorOf, interactions, d
1002
1052
  reconnectAtom,
1003
1053
  retryAtom,
1004
1054
  setDraftAtom,
1055
+ setImagesAtom,
1005
1056
  sendAtom,
1006
1057
  snapshotAtom,
1007
1058
  statusAtom,
@@ -1080,11 +1131,8 @@ export function ParakeetProvider({ children, session, clients = noClients, inter
1080
1131
  }, [scope, onToast]);
1081
1132
  useEffect(() => {
1082
1133
  if (acquiredDraftStorage.status === "unavailable")
1083
- scope.interactions.report({
1084
- tone: "info",
1085
- message: "Drafts are session-only because browser storage is unavailable. They will be lost when this conversation closes.",
1086
- });
1087
- }, [scope, acquiredDraftStorage]);
1134
+ reportFailure("acquire draft storage", acquiredDraftStorage.cause);
1135
+ }, [acquiredDraftStorage]);
1088
1136
  useEffect(() => {
1089
1137
  const reconcile = () => {
1090
1138
  scope.store.set(scope.atoms.reconcileQueuedCancellationAtom);
@@ -1180,6 +1228,7 @@ export function useComposer() {
1180
1228
  const failure = useStoreAtom(atoms.failureAtom);
1181
1229
  const admissionError = useStoreAtom(atoms.admissionErrorAtom);
1182
1230
  const setDraft = useStoreSetter(atoms.setDraftAtom);
1231
+ const setImages = useStoreSetter(atoms.setImagesAtom);
1183
1232
  const checkpointDraft = useStoreSetter(atoms.checkpointDraftAtom);
1184
1233
  const send = useStoreSetter(atoms.sendAtom);
1185
1234
  const submitCommand = useStoreSetter(atoms.submitCommandAtom);
@@ -1202,6 +1251,7 @@ export function useComposer() {
1202
1251
  retry: retryLatest,
1203
1252
  send,
1204
1253
  setDraft,
1254
+ setImages,
1205
1255
  submitCommand,
1206
1256
  }), [
1207
1257
  admissionError,
@@ -1216,6 +1266,7 @@ export function useComposer() {
1216
1266
  retryLatest,
1217
1267
  send,
1218
1268
  setDraft,
1269
+ setImages,
1219
1270
  submitCommand,
1220
1271
  ]);
1221
1272
  }
@@ -1250,6 +1301,10 @@ export function useStatus() {
1250
1301
  viewState,
1251
1302
  }), [commandId, context, observation, reconnect, status, viewState]);
1252
1303
  }
1304
+ /** Time of the last delivered watch snapshot or frame, not an SSE comment. */
1305
+ export function useLastSessionReplyAt() {
1306
+ return useStoreAtom(useConversationScope().atoms.lastReplyAtAtom);
1307
+ }
1253
1308
  /** Presentation admitted once from the selected Session's already observed slots. */
1254
1309
  export function useClientContributions() {
1255
1310
  return useStoreAtom(useConversationScope().contributions);
@@ -0,0 +1,198 @@
1
+ import type { WhistleInteraction } from "@fungi.computer/whistle";
2
+ /** The ledger facts an approval card reads; the approvals ledger's own view satisfies it. */
3
+ export type ApprovalFacts = Readonly<{
4
+ /** The durable record. */
5
+ record: Readonly<{
6
+ /** Identity of the record. */
7
+ recordId: string;
8
+ /** Changes whenever the record does; an answer names the revision it saw. */
9
+ revision: number;
10
+ /** Who asked. */
11
+ requester: Readonly<{
12
+ /** Name shown for the requesting Agent. */
13
+ displayName: string;
14
+ /** The Agent, when one asked. */
15
+ agentId: string | null;
16
+ /** The Session, when one asked. */
17
+ sessionId: string | null;
18
+ /** The principal that holds the request. */
19
+ principalId: string;
20
+ }>;
21
+ /** The tool's module. */
22
+ module: string;
23
+ /** The tool's operation. */
24
+ operation: string;
25
+ /** Whether the tool writes inside Fungi or reaches outside it. */
26
+ access: string;
27
+ /** What the tool would be given, as the ledger describes it. */
28
+ description: Readonly<{
29
+ /** Size of the input in bytes. */
30
+ inputBytes: number;
31
+ /** Digest of the input. */
32
+ inputSha256: string;
33
+ }>;
34
+ /** Whether the effect can be undone. */
35
+ reversibility: string;
36
+ /** When the request lapses. */
37
+ expiresAt: number;
38
+ }>;
39
+ /** The holder may answer it. */
40
+ canAnswer: boolean;
41
+ }>;
42
+ /** What the host knows about a command the Agent asked to run outside the sandbox. */
43
+ export type ApprovalReview = Readonly<{
44
+ /** The command line. */
45
+ command: string;
46
+ /** Where it would run. */
47
+ cwd: string;
48
+ /** Why the Agent says it needs it. */
49
+ justification: string;
50
+ /** A direct program, or shell text. */
51
+ form: "direct" | "shell";
52
+ /** A command, or an extension install. */
53
+ kind: "exec" | "extension-install";
54
+ /** The program and its arguments, when direct. */
55
+ argv?: readonly string[];
56
+ /** The command words an Always-allow rule would match. */
57
+ prefix?: readonly string[];
58
+ /** A warning shown when no rule is offered. */
59
+ warning?: string;
60
+ /** Everything an Always-allow rule would cover. */
61
+ alwaysAllowCoverage?: string;
62
+ /** Why no Always-allow rule is offered. */
63
+ alwaysAllowUnavailableReason?: string;
64
+ /** What a reusable rule risks. */
65
+ risk?: Readonly<{
66
+ /** The risk in one sentence, named once above the confirmation. */
67
+ summary: string;
68
+ /** The risk in full, shown under Details. */
69
+ detail: string;
70
+ }>;
71
+ }>;
72
+ /** One labelled value in a card's facts or details. */
73
+ export type ApprovalEntry = Readonly<{
74
+ /** What the value is. */
75
+ label: string;
76
+ /** The value. */
77
+ value: string;
78
+ }>;
79
+ /** The words an approval prompt carries above its choices. */
80
+ export type ApprovalContent = Readonly<{
81
+ /** The title. */
82
+ title: string;
83
+ /** One sentence under it. */
84
+ summary?: string | undefined;
85
+ /** Short labelled lines. */
86
+ facts?: readonly ApprovalEntry[] | undefined;
87
+ /** Long text, when the command is too long for a title. */
88
+ body?: string | undefined;
89
+ /** A short name for `body`. */
90
+ bodyLabel?: string | undefined;
91
+ /** Everything else, shown on demand. */
92
+ details?: readonly ApprovalEntry[] | undefined;
93
+ }>;
94
+ /** Everything one approval shows, and what Always allow would confirm. */
95
+ export type ApprovalCard = Readonly<{
96
+ /** The first prompt's words. */
97
+ content: ApprovalContent;
98
+ /** The Always-allow choice, when a rule is offered. */
99
+ always?: string | undefined;
100
+ /** What the Always-allow confirmation says. */
101
+ confirmation?: (ApprovalContent & {
102
+ /** The rule's coverage, whole. */
103
+ body: string;
104
+ }) | undefined;
105
+ /** The risk named above the confirmation, once. */
106
+ confirmWarning?: string | undefined;
107
+ /** The confirming choice. */
108
+ confirmYes?: string | undefined;
109
+ }>;
110
+ /**
111
+ * The words of one approval. A connected tool gets its effect; a command the
112
+ * Agent wants to run outside the sandbox gets its folder, reason and rule.
113
+ * `home` shortens paths inside it to `~`; `now` dates the expiry.
114
+ */
115
+ export declare function approvalCardCopy(view: ApprovalFacts, review: ApprovalReview | undefined, options: Readonly<{
116
+ width: number;
117
+ home: string;
118
+ now: number;
119
+ }>): ApprovalCard;
120
+ /** What an answer was refused for, in words for the person. */
121
+ export declare function approvalRefusal(reason: string, decision: "approve" | "deny" | "always-allow"): string;
122
+ /** What the person decided. */
123
+ export type ApprovalDecision = "approve" | "deny" | "always-allow";
124
+ /** A decision, pinned to the revision the person saw. */
125
+ export type ApprovalAnswerInput = Readonly<{
126
+ /** The record being answered. */
127
+ recordId: string;
128
+ /** Approve once, deny, or approve and keep a rule. */
129
+ decision: ApprovalDecision;
130
+ /** The revision on screen; a changed record refuses the answer. */
131
+ expectedRevision: number;
132
+ /** Why it was denied, when the person said. */
133
+ denialNote?: string;
134
+ }>;
135
+ /** How the owner took an answer; a refusal names its reason. */
136
+ export type ApprovalAnswerResult = Readonly<{
137
+ /** `refused`, or whatever else the owner calls an accepted answer. */
138
+ kind: string;
139
+ /** Why it was refused. */
140
+ reason?: string;
141
+ }>;
142
+ /** Where approvals come from and where answers go. */
143
+ export type ApprovalSource = Readonly<{
144
+ /** The record being shown, when there is one. */
145
+ snapshot: () => ApprovalFacts | undefined;
146
+ /** Observe the record changing. */
147
+ subscribe: (listener: () => void) => () => void;
148
+ /** What the host knows about the command, for an escape review. */
149
+ review?: ApprovalReview | undefined;
150
+ /** Answer it; a refusal names its reason. */
151
+ answer: (input: ApprovalAnswerInput) => Promise<ApprovalAnswerResult>;
152
+ /** Let the whole family of this tool through from now on, where the surface can. */
153
+ family?: Readonly<{
154
+ /** The choice's name. */
155
+ label: string;
156
+ /** Turn the rule on; false when it was refused. */
157
+ enable: () => Promise<boolean>;
158
+ }> | undefined;
159
+ }>;
160
+ /** The answer was sent. */
161
+ export type ApprovalAnswered = Readonly<{
162
+ /** Discriminant. */
163
+ kind: "answered";
164
+ }>;
165
+ /** The answer was refused, or could not be sent. */
166
+ export type ApprovalRefused = Readonly<{
167
+ /** Discriminant. */
168
+ kind: "refused";
169
+ /** What to tell the person. */
170
+ message: string;
171
+ }>;
172
+ /** The record changed or went away before an answer was sent. */
173
+ export type ApprovalAbandoned = Readonly<{
174
+ /** Discriminant. */
175
+ kind: "abandoned";
176
+ }>;
177
+ /** How asking ended. */
178
+ export type ApprovalOutcome = ApprovalAnswered | ApprovalRefused | ApprovalAbandoned;
179
+ /**
180
+ * Ask the person about one approval through a conversation's decision
181
+ * prompts, then send the answer. Nothing is sent until Enter answers, Always
182
+ * allow and the family rule each need a second, separate confirmation whose
183
+ * default is Back, and an
184
+ * answer is only sent while the record is still the one that was shown.
185
+ */
186
+ export declare function askApproval(interaction: WhistleInteraction, input: Readonly<{
187
+ view: ApprovalFacts;
188
+ source: ApprovalSource;
189
+ home: string;
190
+ width: number;
191
+ now: number;
192
+ /** The longest denial note. */
193
+ noteLimit: number;
194
+ /** The record on screen is still the current one. */
195
+ stillCurrent: () => boolean;
196
+ /** An error reached the person as a refusal; report its cause. */
197
+ onFailure: (cause: unknown) => void;
198
+ }>): Promise<ApprovalOutcome>;