@geoqiao/pi-ask 1.2.0 → 1.2.1
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/CHANGELOG.md +7 -0
- package/README.md +3 -0
- package/docs/contract.md +6 -4
- package/docs/remote-events.md +2 -2
- package/package.json +1 -1
- package/src/ask-tool.ts +4 -2
- package/src/rpc/controller.ts +218 -119
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
## [1.2.1](https://github.com/geoqiao/pi-ask/compare/v1.2.0...v1.2.1) (2026-08-16)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* harden RPC fallback interactions ([4cceb3b](https://github.com/geoqiao/pi-ask/commit/4cceb3b2ab8d19b46f4688a53f8b7ae52e2b06ae))
|
|
7
|
+
|
|
1
8
|
# [1.2.0](https://github.com/geoqiao/pi-ask/compare/v1.1.0...v1.2.0) (2026-08-16)
|
|
2
9
|
|
|
3
10
|
|
package/README.md
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
# @geoqiao/pi-ask
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@geoqiao/pi-ask)
|
|
6
|
+
[](https://github.com/geoqiao/pi-ask/actions/workflows/ci.yml)
|
|
6
7
|
[](https://github.com/geoqiao/pi-ask/commits/main)
|
|
7
8
|
[](https://github.com/geoqiao/pi-ask/stargazers)
|
|
8
9
|
|
|
@@ -206,6 +207,8 @@ When Pi runs in RPC mode with portable extension UI support, `ask_user` keeps th
|
|
|
206
207
|
- multi-select repeats `select` with `[ ]` / `[x]` markers until `Finish selection` is chosen
|
|
207
208
|
- descriptions and preview content are flattened into readable option strings
|
|
208
209
|
- multiple questions include `[current/total]` progress in each dialog title
|
|
210
|
+
- dismissing a question or notes-menu `select` cancels the flow, while dismissing a nested answer/note editor returns to its parent dialog
|
|
211
|
+
- tool abort signals close portable `select`/`input` dialogs; Pi's portable `editor` cannot be interrupted until it resolves
|
|
209
212
|
|
|
210
213
|
RPC intentionally does not reproduce the tabbed same-screen form, native checkbox cards, custom preview pane, question-type hotkeys, settings overlay, or final Submit/Elaborate review tab. The fallback completes in `submit` mode after the sequential questions. `/answer`, `/answer:again`, `/ask:replay`, and `/ask-settings` remain TUI-only.
|
|
211
214
|
|
package/docs/contract.md
CHANGED
|
@@ -160,7 +160,7 @@ This document defines the stable external behavior. It does not explain internal
|
|
|
160
160
|
|
|
161
161
|
## Output rules
|
|
162
162
|
|
|
163
|
-
- `cancelled: true` means the user dismissed the flow, UI was unavailable, or the payload was invalid before UI opened
|
|
163
|
+
- `cancelled: true` means the user dismissed a flow-level selection, chose Cancel, aborted the flow, UI was unavailable, or the payload was invalid before UI opened
|
|
164
164
|
- invalid payloads return `error.kind === "invalid_input"` with structured `issues` and a transcript-friendly `Invalid ask_user payload:` message
|
|
165
165
|
- `mode: "submit"` is normal completion; `mode: "elaborate"` means the user asked the agent to continue with follow-up clarification based on notes
|
|
166
166
|
- unanswered questions are omitted from `answers`
|
|
@@ -175,7 +175,7 @@ This document defines the stable external behavior. It does not explain internal
|
|
|
175
175
|
- `customText` stores the free-form answer
|
|
176
176
|
- on single-select questions, saving free-form text clears selected options for that question
|
|
177
177
|
- on multi-select questions, `values` and `labels` include both selected options and `customText` when both are present
|
|
178
|
-
- on multi-select questions, selected options keep
|
|
178
|
+
- on multi-select questions, selected options keep the user's selection order and `customText` is appended last
|
|
179
179
|
- submitting free-form text on a multi-select question stays on the same question tab and marks the custom row selected
|
|
180
180
|
- on multi-select questions, toggling an empty custom row opens the free-form editor, while toggling a custom row with saved free-form text selects or deselects it without opening the editor or clearing the text
|
|
181
181
|
- saving or clearing free-form text on a multi-select question does not clear other selected options
|
|
@@ -263,10 +263,12 @@ The rich ask flow uses `ctx.ui.custom()` only in TUI mode. RPC mode never calls
|
|
|
263
263
|
- short and multiline custom answers use `input` and `editor`
|
|
264
264
|
- every question exposes Skip explicitly; required remains advisory and its Skip label says so
|
|
265
265
|
- after each question, a portable action dialog can add/edit a short or multiline question note, or an editor note for a selected option
|
|
266
|
-
- multi-select repeatedly calls `select`, showing `[ ]` / `[x]` option markers and a `Finish selection` action; selections are collected locally in
|
|
266
|
+
- multi-select repeatedly calls `select`, showing `[ ]` / `[x]` option markers and a `Finish selection` action; selections are collected locally in the user's selection order
|
|
267
267
|
- descriptions and preview content are flattened into option strings instead of using a custom preview pane
|
|
268
268
|
- multiple questions are sequential and every title includes `[current/total]` progress
|
|
269
|
-
- dismissing a
|
|
269
|
+
- dismissing a question or optional-notes action `select`, choosing Cancel, or aborting the tool returns `cancelled: true`
|
|
270
|
+
- dismissing a nested custom-answer or note `input`/`editor` abandons that edit and returns to its parent action dialog without changing the answer
|
|
271
|
+
- `select` and `input` observe the tool abort signal; Pi's portable `editor` API has no signal or timeout option, so an abort is observed only after an open editor resolves
|
|
270
272
|
|
|
271
273
|
RPC does not provide the tabbed same-screen form, native checkbox cards, custom preview pane, question-type hotkeys, settings overlay, or final Submit/Elaborate review tab. RPC completion uses `mode: "submit"`. `/answer`, `/answer:again`, `/ask:replay`, and `/ask-settings` remain TUI-only.
|
|
272
274
|
|
package/docs/remote-events.md
CHANGED
|
@@ -18,7 +18,7 @@ Remote submit:
|
|
|
18
18
|
|
|
19
19
|
## Started
|
|
20
20
|
|
|
21
|
-
Emitted after a validated ask
|
|
21
|
+
Emitted after a validated TUI ask flow opens. The RPC portable-dialog fallback does not start the remote event bridge or emit lifecycle events.
|
|
22
22
|
|
|
23
23
|
```ts
|
|
24
24
|
type PiAskStartedEvent = {
|
|
@@ -104,7 +104,7 @@ Correlate by `requestId` and `flowId`. Do not depend on strict ordering between
|
|
|
104
104
|
|
|
105
105
|
## Completed
|
|
106
106
|
|
|
107
|
-
Emitted when the flow resolves.
|
|
107
|
+
Emitted when the TUI flow resolves. RPC portable-dialog completion is returned through the normal tool result instead.
|
|
108
108
|
|
|
109
109
|
```ts
|
|
110
110
|
type PiAskCompletedEvent = {
|
package/package.json
CHANGED
package/src/ask-tool.ts
CHANGED
|
@@ -51,7 +51,7 @@ async function executeAskTool(
|
|
|
51
51
|
pi: Pick<ExtensionAPI, "appendEntry">,
|
|
52
52
|
toolCallId: string,
|
|
53
53
|
params: AskParams,
|
|
54
|
-
|
|
54
|
+
signal: AbortSignal | undefined,
|
|
55
55
|
_onUpdate: unknown,
|
|
56
56
|
ctx: ExtensionContext,
|
|
57
57
|
remoteAsk?: RemoteAskRuntime
|
|
@@ -70,7 +70,9 @@ async function executeAskTool(
|
|
|
70
70
|
});
|
|
71
71
|
if (ctx.mode !== "tui") {
|
|
72
72
|
if (ctx.mode === "rpc" && ctx.hasUI) {
|
|
73
|
-
return successfulResponse(
|
|
73
|
+
return successfulResponse(
|
|
74
|
+
await runRpcAskFlow(ctx, validation.state, { signal })
|
|
75
|
+
);
|
|
74
76
|
}
|
|
75
77
|
return nonInteractiveResponse(validation.state);
|
|
76
78
|
}
|
package/src/rpc/controller.ts
CHANGED
|
@@ -20,11 +20,21 @@ import type {
|
|
|
20
20
|
|
|
21
21
|
type RpcUi = Pick<ExtensionContext["ui"], "editor" | "input" | "select">;
|
|
22
22
|
|
|
23
|
-
interface
|
|
24
|
-
|
|
25
|
-
state: AskState;
|
|
23
|
+
interface RpcFlowOptions {
|
|
24
|
+
signal?: AbortSignal;
|
|
26
25
|
}
|
|
27
26
|
|
|
27
|
+
type RpcCompletionResult =
|
|
28
|
+
| { kind: "cancelled"; state: AskState }
|
|
29
|
+
| { kind: "completed"; state: AskState };
|
|
30
|
+
|
|
31
|
+
type RpcActionResult = RpcCompletionResult | { kind: "back"; state: AskState };
|
|
32
|
+
|
|
33
|
+
type RpcEditResult =
|
|
34
|
+
| { kind: "back"; state: AskState }
|
|
35
|
+
| { kind: "cancelled"; state: AskState }
|
|
36
|
+
| { kind: "saved"; state: AskState };
|
|
37
|
+
|
|
28
38
|
interface SelectAction {
|
|
29
39
|
kind:
|
|
30
40
|
| "cancel"
|
|
@@ -56,23 +66,40 @@ const FINISH_SELECTION_LABEL = "Finish selection";
|
|
|
56
66
|
|
|
57
67
|
export async function runRpcAskFlow(
|
|
58
68
|
ctx: { ui: RpcUi },
|
|
59
|
-
initialState: AskState
|
|
69
|
+
initialState: AskState,
|
|
70
|
+
options: RpcFlowOptions = {}
|
|
60
71
|
): Promise<AskResult> {
|
|
61
72
|
let state = initialState;
|
|
62
73
|
|
|
63
74
|
for (const [questionIndex] of state.questions.entries()) {
|
|
64
|
-
|
|
75
|
+
if (options.signal?.aborted) {
|
|
76
|
+
return cancelledResult(state);
|
|
77
|
+
}
|
|
78
|
+
const answerStep = await askQuestion(
|
|
79
|
+
ctx.ui,
|
|
80
|
+
state,
|
|
81
|
+
questionIndex,
|
|
82
|
+
options.signal
|
|
83
|
+
);
|
|
65
84
|
state = answerStep.state;
|
|
66
|
-
if (answerStep.cancelled) {
|
|
85
|
+
if (answerStep.kind === "cancelled") {
|
|
67
86
|
return cancelledResult(state);
|
|
68
87
|
}
|
|
69
88
|
|
|
70
|
-
const noteStep = await askForOptionalNotes(
|
|
89
|
+
const noteStep = await askForOptionalNotes(
|
|
90
|
+
ctx.ui,
|
|
91
|
+
state,
|
|
92
|
+
questionIndex,
|
|
93
|
+
options.signal
|
|
94
|
+
);
|
|
71
95
|
state = noteStep.state;
|
|
72
|
-
if (noteStep.cancelled) {
|
|
96
|
+
if (noteStep.kind === "cancelled") {
|
|
73
97
|
return cancelledResult(state);
|
|
74
98
|
}
|
|
75
99
|
}
|
|
100
|
+
if (options.signal?.aborted) {
|
|
101
|
+
return cancelledResult(state);
|
|
102
|
+
}
|
|
76
103
|
|
|
77
104
|
return toAskResult({
|
|
78
105
|
...state,
|
|
@@ -84,72 +111,94 @@ export async function runRpcAskFlow(
|
|
|
84
111
|
async function askQuestion(
|
|
85
112
|
ui: RpcUi,
|
|
86
113
|
state: AskState,
|
|
87
|
-
questionIndex: number
|
|
88
|
-
|
|
114
|
+
questionIndex: number,
|
|
115
|
+
signal?: AbortSignal
|
|
116
|
+
): Promise<RpcCompletionResult> {
|
|
89
117
|
const question = state.questions[questionIndex];
|
|
90
118
|
if (!question) {
|
|
91
|
-
return {
|
|
119
|
+
return { kind: "completed", state };
|
|
92
120
|
}
|
|
93
121
|
if (question.type === "multi") {
|
|
94
|
-
return await askMultiQuestion(ui, state, question, questionIndex);
|
|
122
|
+
return await askMultiQuestion(ui, state, question, questionIndex, signal);
|
|
95
123
|
}
|
|
96
|
-
return await askSingleQuestion(ui, state, question, questionIndex);
|
|
124
|
+
return await askSingleQuestion(ui, state, question, questionIndex, signal);
|
|
97
125
|
}
|
|
98
126
|
|
|
99
127
|
async function askSingleQuestion(
|
|
100
128
|
ui: RpcUi,
|
|
101
|
-
|
|
129
|
+
initialState: AskState,
|
|
102
130
|
question: AskQuestion,
|
|
103
|
-
questionIndex: number
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
131
|
+
questionIndex: number,
|
|
132
|
+
signal?: AbortSignal
|
|
133
|
+
): Promise<RpcCompletionResult> {
|
|
134
|
+
let state = initialState;
|
|
135
|
+
while (true) {
|
|
136
|
+
if (signal?.aborted) {
|
|
137
|
+
return { kind: "cancelled", state };
|
|
138
|
+
}
|
|
139
|
+
const actions: SelectAction[] = [...question.options.entries()]
|
|
140
|
+
.filter(([, option]) => !option.freeform)
|
|
141
|
+
.map(([optionIndex, option]) => ({
|
|
142
|
+
kind: "option" as const,
|
|
143
|
+
label: formatOption(option, optionIndex),
|
|
144
|
+
optionIndex,
|
|
145
|
+
}));
|
|
146
|
+
appendCommonActions(actions, question);
|
|
113
147
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
148
|
+
const action = await selectAction(
|
|
149
|
+
ui,
|
|
150
|
+
formatTitle(state, question, questionIndex),
|
|
151
|
+
actions,
|
|
152
|
+
signal
|
|
153
|
+
);
|
|
154
|
+
const result = await applySingleAction({
|
|
155
|
+
action,
|
|
156
|
+
question,
|
|
157
|
+
questionIndex,
|
|
158
|
+
signal,
|
|
159
|
+
state,
|
|
160
|
+
ui,
|
|
161
|
+
});
|
|
162
|
+
if (result.kind !== "back") {
|
|
163
|
+
return result;
|
|
164
|
+
}
|
|
165
|
+
state = result.state;
|
|
166
|
+
}
|
|
126
167
|
}
|
|
127
168
|
|
|
128
169
|
async function applySingleAction(args: {
|
|
129
170
|
action: SelectAction | undefined;
|
|
130
171
|
question: AskQuestion;
|
|
131
172
|
questionIndex: number;
|
|
173
|
+
signal?: AbortSignal;
|
|
132
174
|
state: AskState;
|
|
133
175
|
ui: RpcUi;
|
|
134
|
-
}): Promise<
|
|
135
|
-
const { action, question, questionIndex, state, ui } = args;
|
|
176
|
+
}): Promise<RpcActionResult> {
|
|
177
|
+
const { action, question, questionIndex, signal, state, ui } = args;
|
|
136
178
|
if (!action || action.kind === "cancel") {
|
|
137
|
-
return {
|
|
179
|
+
return { kind: "cancelled", state };
|
|
138
180
|
}
|
|
139
181
|
if (action.kind === "skip") {
|
|
140
|
-
return {
|
|
182
|
+
return {
|
|
183
|
+
kind: "completed",
|
|
184
|
+
state: clearAnswer(state, question.id),
|
|
185
|
+
};
|
|
141
186
|
}
|
|
142
187
|
if (action.kind === "custom-input" || action.kind === "custom-editor") {
|
|
143
|
-
|
|
188
|
+
const editResult = await askForCustomAnswer(
|
|
144
189
|
ui,
|
|
145
190
|
state,
|
|
146
191
|
question,
|
|
147
192
|
questionIndex,
|
|
148
|
-
action.kind === "custom-editor"
|
|
193
|
+
action.kind === "custom-editor",
|
|
194
|
+
signal
|
|
149
195
|
);
|
|
196
|
+
return editResult.kind === "saved"
|
|
197
|
+
? { kind: "completed", state: editResult.state }
|
|
198
|
+
: editResult;
|
|
150
199
|
}
|
|
151
200
|
if (action.kind !== "option") {
|
|
152
|
-
return {
|
|
201
|
+
return { kind: "cancelled", state };
|
|
153
202
|
}
|
|
154
203
|
|
|
155
204
|
return selectSingleOption(state, question, action.optionIndex);
|
|
@@ -159,14 +208,14 @@ function selectSingleOption(
|
|
|
159
208
|
state: AskState,
|
|
160
209
|
question: AskQuestion,
|
|
161
210
|
optionIndex: number | undefined
|
|
162
|
-
):
|
|
211
|
+
): RpcCompletionResult {
|
|
163
212
|
const option =
|
|
164
213
|
optionIndex === undefined ? undefined : question.options[optionIndex];
|
|
165
214
|
if (!option || optionIndex === undefined) {
|
|
166
|
-
return {
|
|
215
|
+
return { kind: "cancelled", state };
|
|
167
216
|
}
|
|
168
217
|
return {
|
|
169
|
-
|
|
218
|
+
kind: "completed",
|
|
170
219
|
state: updateAnswer(state, question.id, (answer) =>
|
|
171
220
|
setSingleSelection(answer, option, optionIndex)
|
|
172
221
|
),
|
|
@@ -177,25 +226,31 @@ async function askMultiQuestion(
|
|
|
177
226
|
ui: RpcUi,
|
|
178
227
|
initialState: AskState,
|
|
179
228
|
question: AskQuestion,
|
|
180
|
-
questionIndex: number
|
|
181
|
-
|
|
229
|
+
questionIndex: number,
|
|
230
|
+
signal?: AbortSignal
|
|
231
|
+
): Promise<RpcCompletionResult> {
|
|
182
232
|
let state = initialState;
|
|
183
233
|
while (true) {
|
|
234
|
+
if (signal?.aborted) {
|
|
235
|
+
return { kind: "cancelled", state };
|
|
236
|
+
}
|
|
184
237
|
const actions = createMultiActions(question, state.answers[question.id]);
|
|
185
238
|
const action = await selectAction(
|
|
186
239
|
ui,
|
|
187
240
|
formatTitle(state, question, questionIndex, "Select all that apply"),
|
|
188
|
-
actions
|
|
241
|
+
actions,
|
|
242
|
+
signal
|
|
189
243
|
);
|
|
190
244
|
const actionResult = await applyMultiAction({
|
|
191
245
|
action,
|
|
192
246
|
question,
|
|
193
247
|
questionIndex,
|
|
248
|
+
signal,
|
|
194
249
|
state,
|
|
195
250
|
ui,
|
|
196
251
|
});
|
|
197
|
-
if (actionResult.
|
|
198
|
-
return actionResult
|
|
252
|
+
if (actionResult.kind !== "back") {
|
|
253
|
+
return actionResult;
|
|
199
254
|
}
|
|
200
255
|
state = actionResult.state;
|
|
201
256
|
}
|
|
@@ -205,9 +260,9 @@ function createMultiActions(
|
|
|
205
260
|
question: AskQuestion,
|
|
206
261
|
answer: AskStateAnswer | undefined
|
|
207
262
|
): SelectAction[] {
|
|
208
|
-
const actions: SelectAction[] = question.options
|
|
209
|
-
.filter((option) => !option.freeform)
|
|
210
|
-
.map((
|
|
263
|
+
const actions: SelectAction[] = [...question.options.entries()]
|
|
264
|
+
.filter(([, option]) => !option.freeform)
|
|
265
|
+
.map(([optionIndex, option]) => ({
|
|
211
266
|
kind: "option" as const,
|
|
212
267
|
label: formatOption(option, optionIndex, {
|
|
213
268
|
selected: isOptionSelected(answer, option.value),
|
|
@@ -228,41 +283,39 @@ function createMultiActions(
|
|
|
228
283
|
return actions;
|
|
229
284
|
}
|
|
230
285
|
|
|
231
|
-
type MultiActionResult =
|
|
232
|
-
| { done: false; state: AskState }
|
|
233
|
-
| { done: true; step: RpcStepResult };
|
|
234
|
-
|
|
235
286
|
async function applyMultiAction(args: {
|
|
236
287
|
action: SelectAction | undefined;
|
|
237
288
|
question: AskQuestion;
|
|
238
289
|
questionIndex: number;
|
|
290
|
+
signal?: AbortSignal;
|
|
239
291
|
state: AskState;
|
|
240
292
|
ui: RpcUi;
|
|
241
|
-
}): Promise<
|
|
242
|
-
const { action, question, questionIndex, state, ui } = args;
|
|
293
|
+
}): Promise<RpcActionResult> {
|
|
294
|
+
const { action, question, questionIndex, signal, state, ui } = args;
|
|
243
295
|
if (!action || action.kind === "cancel") {
|
|
244
|
-
return {
|
|
296
|
+
return { kind: "cancelled", state };
|
|
245
297
|
}
|
|
246
298
|
if (action.kind === "finish") {
|
|
247
|
-
return {
|
|
299
|
+
return { kind: "completed", state };
|
|
248
300
|
}
|
|
249
301
|
if (action.kind === "skip") {
|
|
250
302
|
return {
|
|
251
|
-
|
|
252
|
-
|
|
303
|
+
kind: "completed",
|
|
304
|
+
state: clearAnswer(state, question.id),
|
|
253
305
|
};
|
|
254
306
|
}
|
|
255
307
|
if (action.kind === "custom-input" || action.kind === "custom-editor") {
|
|
256
|
-
const
|
|
308
|
+
const editResult = await askForCustomAnswer(
|
|
257
309
|
ui,
|
|
258
310
|
state,
|
|
259
311
|
question,
|
|
260
312
|
questionIndex,
|
|
261
|
-
action.kind === "custom-editor"
|
|
313
|
+
action.kind === "custom-editor",
|
|
314
|
+
signal
|
|
262
315
|
);
|
|
263
|
-
return
|
|
264
|
-
?
|
|
265
|
-
: {
|
|
316
|
+
return editResult.kind === "cancelled"
|
|
317
|
+
? editResult
|
|
318
|
+
: { kind: "back", state: editResult.state };
|
|
266
319
|
}
|
|
267
320
|
return applyMultiOptionAction(state, question, action);
|
|
268
321
|
}
|
|
@@ -271,24 +324,18 @@ function applyMultiOptionAction(
|
|
|
271
324
|
state: AskState,
|
|
272
325
|
question: AskQuestion,
|
|
273
326
|
action: SelectAction
|
|
274
|
-
):
|
|
327
|
+
): RpcActionResult {
|
|
275
328
|
const optionIndex = action.optionIndex;
|
|
276
329
|
const option =
|
|
277
330
|
optionIndex === undefined ? undefined : question.options[optionIndex];
|
|
278
331
|
if (!option || optionIndex === undefined) {
|
|
279
|
-
return {
|
|
332
|
+
return { kind: "cancelled", state };
|
|
280
333
|
}
|
|
281
334
|
return {
|
|
282
|
-
|
|
283
|
-
state: updateAnswer(state, question.id, (currentAnswer) =>
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
...nextAnswer,
|
|
287
|
-
selected: [...nextAnswer.selected].sort(
|
|
288
|
-
(left, right) => left.index - right.index
|
|
289
|
-
),
|
|
290
|
-
};
|
|
291
|
-
}),
|
|
335
|
+
kind: "back",
|
|
336
|
+
state: updateAnswer(state, question.id, (currentAnswer) =>
|
|
337
|
+
toggleSelection(currentAnswer, option, optionIndex)
|
|
338
|
+
),
|
|
292
339
|
};
|
|
293
340
|
}
|
|
294
341
|
|
|
@@ -297,8 +344,12 @@ async function askForCustomAnswer(
|
|
|
297
344
|
state: AskState,
|
|
298
345
|
question: AskQuestion,
|
|
299
346
|
questionIndex: number,
|
|
300
|
-
multiline: boolean
|
|
301
|
-
|
|
347
|
+
multiline: boolean,
|
|
348
|
+
signal?: AbortSignal
|
|
349
|
+
): Promise<RpcEditResult> {
|
|
350
|
+
if (signal?.aborted) {
|
|
351
|
+
return { kind: "cancelled", state };
|
|
352
|
+
}
|
|
302
353
|
const currentText = state.answers[question.id]?.customText;
|
|
303
354
|
const title = formatTitle(
|
|
304
355
|
state,
|
|
@@ -308,13 +359,16 @@ async function askForCustomAnswer(
|
|
|
308
359
|
);
|
|
309
360
|
const value = multiline
|
|
310
361
|
? await ui.editor(title, currentText ?? "")
|
|
311
|
-
: await ui.input(title, formatInputPlaceholder(currentText));
|
|
362
|
+
: await ui.input(title, formatInputPlaceholder(currentText), { signal });
|
|
363
|
+
if (signal?.aborted) {
|
|
364
|
+
return { kind: "cancelled", state };
|
|
365
|
+
}
|
|
312
366
|
if (value === undefined) {
|
|
313
|
-
return {
|
|
367
|
+
return { kind: "back", state };
|
|
314
368
|
}
|
|
315
369
|
|
|
316
370
|
return {
|
|
317
|
-
|
|
371
|
+
kind: "saved",
|
|
318
372
|
state: updateAnswer(state, question.id, (answer) =>
|
|
319
373
|
saveCustomText(
|
|
320
374
|
answer,
|
|
@@ -328,31 +382,37 @@ async function askForCustomAnswer(
|
|
|
328
382
|
async function askForOptionalNotes(
|
|
329
383
|
ui: RpcUi,
|
|
330
384
|
initialState: AskState,
|
|
331
|
-
questionIndex: number
|
|
332
|
-
|
|
385
|
+
questionIndex: number,
|
|
386
|
+
signal?: AbortSignal
|
|
387
|
+
): Promise<RpcCompletionResult> {
|
|
333
388
|
let state = initialState;
|
|
334
389
|
const question = state.questions[questionIndex];
|
|
335
390
|
if (!question) {
|
|
336
|
-
return {
|
|
391
|
+
return { kind: "completed", state };
|
|
337
392
|
}
|
|
338
393
|
|
|
339
394
|
while (true) {
|
|
395
|
+
if (signal?.aborted) {
|
|
396
|
+
return { kind: "cancelled", state };
|
|
397
|
+
}
|
|
340
398
|
const answer = state.answers[question.id];
|
|
341
399
|
const actions = createNoteActions(answer);
|
|
342
400
|
const action = await selectNoteAction(
|
|
343
401
|
ui,
|
|
344
402
|
formatTitle(state, question, questionIndex, "Optional notes"),
|
|
345
|
-
actions
|
|
403
|
+
actions,
|
|
404
|
+
signal
|
|
346
405
|
);
|
|
347
406
|
const actionResult = await applyNoteAction({
|
|
348
407
|
action,
|
|
349
408
|
question,
|
|
350
409
|
questionIndex,
|
|
410
|
+
signal,
|
|
351
411
|
state,
|
|
352
412
|
ui,
|
|
353
413
|
});
|
|
354
|
-
if (actionResult.
|
|
355
|
-
return actionResult
|
|
414
|
+
if (actionResult.kind !== "back") {
|
|
415
|
+
return actionResult;
|
|
356
416
|
}
|
|
357
417
|
state = actionResult.state;
|
|
358
418
|
}
|
|
@@ -387,38 +447,47 @@ function createNoteActions(answer: AskStateAnswer | undefined): NoteAction[] {
|
|
|
387
447
|
kind: "option-note" as const,
|
|
388
448
|
optionValue: selection.value,
|
|
389
449
|
label: answer?.optionNotes?.[selection.value]
|
|
390
|
-
? `Edit note for selected option: ${compactText(selection.label)}…`
|
|
391
|
-
: `Add note for selected option: ${compactText(selection.label)}…`,
|
|
450
|
+
? `Edit note for selected option: ${selection.index}. ${compactText(selection.label)}…`
|
|
451
|
+
: `Add note for selected option: ${selection.index}. ${compactText(selection.label)}…`,
|
|
392
452
|
})),
|
|
393
453
|
{ kind: "cancel", label: CANCEL_LABEL },
|
|
394
454
|
];
|
|
395
455
|
}
|
|
396
456
|
|
|
397
|
-
type NoteActionResult =
|
|
398
|
-
| { done: false; state: AskState }
|
|
399
|
-
| { done: true; step: RpcStepResult };
|
|
400
|
-
|
|
401
457
|
async function applyNoteAction(args: {
|
|
402
458
|
action: NoteAction | undefined;
|
|
403
459
|
question: AskQuestion;
|
|
404
460
|
questionIndex: number;
|
|
461
|
+
signal?: AbortSignal;
|
|
405
462
|
state: AskState;
|
|
406
463
|
ui: RpcUi;
|
|
407
|
-
}): Promise<
|
|
408
|
-
const { action, question, questionIndex, state, ui } = args;
|
|
464
|
+
}): Promise<RpcActionResult> {
|
|
465
|
+
const { action, question, questionIndex, signal, state, ui } = args;
|
|
409
466
|
if (!action || action.kind === "cancel") {
|
|
410
|
-
return {
|
|
467
|
+
return { kind: "cancelled", state };
|
|
411
468
|
}
|
|
412
469
|
if (action.kind === "continue") {
|
|
413
|
-
return {
|
|
470
|
+
return { kind: "completed", state };
|
|
414
471
|
}
|
|
415
472
|
if (action.kind === "question-input") {
|
|
416
|
-
return
|
|
473
|
+
return noteEditActionResult(
|
|
474
|
+
await editQuestionNote(ui, state, question, questionIndex, false, signal)
|
|
475
|
+
);
|
|
417
476
|
}
|
|
418
477
|
if (action.kind === "question-editor") {
|
|
419
|
-
return
|
|
478
|
+
return noteEditActionResult(
|
|
479
|
+
await editQuestionNote(ui, state, question, questionIndex, true, signal)
|
|
480
|
+
);
|
|
420
481
|
}
|
|
421
|
-
return
|
|
482
|
+
return noteEditActionResult(
|
|
483
|
+
await editOptionNote(ui, state, question, questionIndex, action, signal)
|
|
484
|
+
);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function noteEditActionResult(result: RpcEditResult): RpcActionResult {
|
|
488
|
+
return result.kind === "cancelled"
|
|
489
|
+
? result
|
|
490
|
+
: { kind: "back", state: result.state };
|
|
422
491
|
}
|
|
423
492
|
|
|
424
493
|
async function editQuestionNote(
|
|
@@ -426,18 +495,25 @@ async function editQuestionNote(
|
|
|
426
495
|
state: AskState,
|
|
427
496
|
question: AskQuestion,
|
|
428
497
|
questionIndex: number,
|
|
429
|
-
multiline: boolean
|
|
430
|
-
|
|
498
|
+
multiline: boolean,
|
|
499
|
+
signal?: AbortSignal
|
|
500
|
+
): Promise<RpcEditResult> {
|
|
501
|
+
if (signal?.aborted) {
|
|
502
|
+
return { kind: "cancelled", state };
|
|
503
|
+
}
|
|
431
504
|
const currentNote = state.answers[question.id]?.note;
|
|
432
505
|
const title = formatTitle(state, question, questionIndex, "Question note");
|
|
433
506
|
const value = multiline
|
|
434
507
|
? await ui.editor(title, currentNote ?? "")
|
|
435
|
-
: await ui.input(title, formatNotePlaceholder(currentNote));
|
|
508
|
+
: await ui.input(title, formatNotePlaceholder(currentNote), { signal });
|
|
509
|
+
if (signal?.aborted) {
|
|
510
|
+
return { kind: "cancelled", state };
|
|
511
|
+
}
|
|
436
512
|
if (value === undefined) {
|
|
437
|
-
return {
|
|
513
|
+
return { kind: "back", state };
|
|
438
514
|
}
|
|
439
515
|
return {
|
|
440
|
-
|
|
516
|
+
kind: "saved",
|
|
441
517
|
state: updateAnswer(state, question.id, (answer) =>
|
|
442
518
|
saveQuestionNote(answer, value)
|
|
443
519
|
),
|
|
@@ -449,14 +525,18 @@ async function editOptionNote(
|
|
|
449
525
|
state: AskState,
|
|
450
526
|
question: AskQuestion,
|
|
451
527
|
questionIndex: number,
|
|
452
|
-
action: NoteAction
|
|
453
|
-
|
|
528
|
+
action: NoteAction,
|
|
529
|
+
signal?: AbortSignal
|
|
530
|
+
): Promise<RpcEditResult> {
|
|
531
|
+
if (signal?.aborted) {
|
|
532
|
+
return { kind: "cancelled", state };
|
|
533
|
+
}
|
|
454
534
|
const optionValue = action.optionValue;
|
|
455
535
|
const option = question.options.find(
|
|
456
536
|
(candidate) => candidate.value === optionValue
|
|
457
537
|
);
|
|
458
538
|
if (!(optionValue && option)) {
|
|
459
|
-
return {
|
|
539
|
+
return { kind: "cancelled", state };
|
|
460
540
|
}
|
|
461
541
|
const value = await ui.editor(
|
|
462
542
|
formatTitle(
|
|
@@ -467,11 +547,14 @@ async function editOptionNote(
|
|
|
467
547
|
),
|
|
468
548
|
state.answers[question.id]?.optionNotes?.[optionValue] ?? ""
|
|
469
549
|
);
|
|
550
|
+
if (signal?.aborted) {
|
|
551
|
+
return { kind: "cancelled", state };
|
|
552
|
+
}
|
|
470
553
|
if (value === undefined) {
|
|
471
|
-
return {
|
|
554
|
+
return { kind: "back", state };
|
|
472
555
|
}
|
|
473
556
|
return {
|
|
474
|
-
|
|
557
|
+
kind: "saved",
|
|
475
558
|
state: updateAnswer(state, question.id, (answer) =>
|
|
476
559
|
saveOptionNote(answer, optionValue, value)
|
|
477
560
|
),
|
|
@@ -481,24 +564,40 @@ async function editOptionNote(
|
|
|
481
564
|
async function selectAction(
|
|
482
565
|
ui: RpcUi,
|
|
483
566
|
title: string,
|
|
484
|
-
actions: SelectAction[]
|
|
567
|
+
actions: SelectAction[],
|
|
568
|
+
signal?: AbortSignal
|
|
485
569
|
): Promise<SelectAction | undefined> {
|
|
570
|
+
if (signal?.aborted) {
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
486
573
|
const selected = await ui.select(
|
|
487
574
|
title,
|
|
488
|
-
actions.map((action) => action.label)
|
|
575
|
+
actions.map((action) => action.label),
|
|
576
|
+
{ signal }
|
|
489
577
|
);
|
|
578
|
+
if (signal?.aborted) {
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
490
581
|
return actions.find((action) => action.label === selected);
|
|
491
582
|
}
|
|
492
583
|
|
|
493
584
|
async function selectNoteAction(
|
|
494
585
|
ui: RpcUi,
|
|
495
586
|
title: string,
|
|
496
|
-
actions: NoteAction[]
|
|
587
|
+
actions: NoteAction[],
|
|
588
|
+
signal?: AbortSignal
|
|
497
589
|
): Promise<NoteAction | undefined> {
|
|
590
|
+
if (signal?.aborted) {
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
498
593
|
const selected = await ui.select(
|
|
499
594
|
title,
|
|
500
|
-
actions.map((action) => action.label)
|
|
595
|
+
actions.map((action) => action.label),
|
|
596
|
+
{ signal }
|
|
501
597
|
);
|
|
598
|
+
if (signal?.aborted) {
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
502
601
|
return actions.find((action) => action.label === selected);
|
|
503
602
|
}
|
|
504
603
|
|