@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.
- package/dist/client-contributions.d.ts +1 -0
- package/dist/commands/awaiting-driver.d.ts +20 -0
- package/dist/commands/awaiting-driver.js +121 -0
- package/dist/commands/builtins.js +6 -4
- package/dist/commands/interactions.d.ts +25 -0
- package/dist/commands/interactions.js +76 -2
- package/dist/commands/run.js +3 -3
- package/dist/commands/slash.d.ts +7 -3
- package/dist/commands/slash.js +2 -0
- package/dist/commands/use-slash-commands.d.ts +13 -0
- package/dist/commands/use-slash-commands.js +58 -1
- package/dist/components/ChatComposer.d.ts +2 -0
- package/dist/components/ChatComposer.js +150 -38
- package/dist/components/ClientRows.js +42 -26
- package/dist/components/CompletionMenu.d.ts +54 -0
- package/dist/components/CompletionMenu.js +52 -0
- package/dist/components/DecisionComposer.d.ts +30 -0
- package/dist/components/DecisionComposer.js +146 -0
- package/dist/components/InteractionPanel.d.ts +10 -1
- package/dist/components/InteractionPanel.js +9 -4
- package/dist/components/SlashMenu.d.ts +1 -3
- package/dist/components/SlashMenu.js +5 -13
- package/dist/components/TimelineItem.d.ts +1 -0
- package/dist/components/TimelineItem.js +3 -3
- package/dist/components/ToolGroup.d.ts +1 -0
- package/dist/components/ToolGroup.js +7 -7
- package/dist/conversation.d.ts +5 -1
- package/dist/conversation.js +72 -20
- package/dist/core/approval-decision.d.ts +198 -0
- package/dist/core/approval-decision.js +291 -0
- package/dist/core/awaiting-answer.d.ts +229 -0
- package/dist/core/awaiting-answer.js +303 -0
- package/dist/core/build-timeline.js +56 -5
- package/dist/core/client-actions.d.ts +1 -0
- package/dist/core/client-actions.js +7 -1
- package/dist/core/draft-storage.d.ts +4 -0
- package/dist/core/draft-storage.js +12 -5
- package/dist/core/session-query.d.ts +8 -0
- package/dist/core/session-query.js +13 -11
- package/dist/headless.d.ts +6 -2
- package/dist/headless.js +5 -1
- package/dist/parakeet.d.ts +6 -2
- package/dist/parakeet.js +15 -6
- package/dist/store/types.d.ts +5 -1
- package/dist/styles.css +395 -113
- package/dist/use-approval-awaiting.d.ts +22 -0
- package/dist/use-approval-awaiting.js +81 -0
- package/dist/use-decision.d.ts +20 -0
- package/dist/use-decision.js +79 -0
- package/docs/api/commands/slash.md +30 -26
- package/docs/api/headless.md +822 -41
- package/docs/api/index.md +13 -4
- package/package.json +13 -7
- package/src/completion-menu.css +71 -0
- 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
|
-
? {
|
|
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
|
-
?
|
|
104
|
-
: { ...
|
|
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:
|
|
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
|
-
: "
|
|
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
|
-
|
|
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;
|