@geoqiao/pi-ask 1.1.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/CHANGELOG.md +194 -0
- package/LICENSE +22 -0
- package/README.md +282 -0
- package/docs/README.md +33 -0
- package/docs/configuration.md +406 -0
- package/docs/contract.md +309 -0
- package/docs/remote-events.md +187 -0
- package/package.json +130 -0
- package/skills/ask-user/SKILL.md +110 -0
- package/src/answer-commands.ts +361 -0
- package/src/answer-extraction.ts +354 -0
- package/src/ask-payload-store.ts +86 -0
- package/src/ask-settings-command.ts +14 -0
- package/src/ask-tool-helpers.ts +172 -0
- package/src/ask-tool.ts +84 -0
- package/src/config/defaults.ts +216 -0
- package/src/config/migrate.ts +70 -0
- package/src/config/migrations/index.ts +139 -0
- package/src/config/migrations/types.ts +10 -0
- package/src/config/schema.ts +287 -0
- package/src/config/store.ts +227 -0
- package/src/constants/keymaps.ts +721 -0
- package/src/constants/text.ts +12 -0
- package/src/constants/ui.ts +22 -0
- package/src/index.ts +30 -0
- package/src/math.ts +3 -0
- package/src/notifications.ts +119 -0
- package/src/remote-ask.ts +563 -0
- package/src/result-format.ts +157 -0
- package/src/result.ts +23 -0
- package/src/schema.ts +74 -0
- package/src/state/answers.ts +251 -0
- package/src/state/create.ts +18 -0
- package/src/state/editor.ts +70 -0
- package/src/state/navigation.ts +86 -0
- package/src/state/normalize.ts +326 -0
- package/src/state/question-type.ts +128 -0
- package/src/state/result.ts +263 -0
- package/src/state/selectors.ts +135 -0
- package/src/state/transitions.ts +330 -0
- package/src/state/view.ts +28 -0
- package/src/text.ts +98 -0
- package/src/types.ts +169 -0
- package/src/ui/auto-submit.ts +36 -0
- package/src/ui/autocomplete.ts +52 -0
- package/src/ui/controller.ts +645 -0
- package/src/ui/dismiss-guard.ts +26 -0
- package/src/ui/input.ts +160 -0
- package/src/ui/render-frame.ts +235 -0
- package/src/ui/render-helpers.ts +385 -0
- package/src/ui/render-question.ts +288 -0
- package/src/ui/render-submit.ts +168 -0
- package/src/ui/render-types.ts +33 -0
- package/src/ui/render.ts +53 -0
- package/src/ui/review-shortcuts.ts +43 -0
- package/src/ui/settings-list.ts +461 -0
- package/src/ui/show-settings.ts +37 -0
- package/src/ui/view-models/question.ts +203 -0
- package/src/ui/view-models/review.ts +100 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: ask-user
|
|
3
|
+
description: "Use ask_user as a decision, research, and requirements gate before ambiguous or high-stakes choices."
|
|
4
|
+
metadata:
|
|
5
|
+
short-description: Decision, research, and requirements gate
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Ask User decision/research gate
|
|
9
|
+
|
|
10
|
+
Use this skill to force explicit user alignment before consequential decisions, preference-sensitive planning, research scoping, or requirements gathering.
|
|
11
|
+
|
|
12
|
+
This skill is for decision control, research scoping, requirements gathering, and preference-sensitive planning, not general chat.
|
|
13
|
+
|
|
14
|
+
## Trigger
|
|
15
|
+
|
|
16
|
+
Classify the next step as one of:
|
|
17
|
+
|
|
18
|
+
- `high_stakes`
|
|
19
|
+
- `ambiguous`
|
|
20
|
+
- `both`
|
|
21
|
+
- `clear`
|
|
22
|
+
|
|
23
|
+
Use `ask_user` when the next step is ambiguous, preference-sensitive, or high-stakes and the user has not already made the decision explicitly.
|
|
24
|
+
|
|
25
|
+
Use `ask_user` for any domain where user input changes the plan, recommendation, research direction, output format, criteria, constraints, or next action.
|
|
26
|
+
|
|
27
|
+
Also use `ask_user` when the user asks to gather requirements, interview them, ask questions, scope research, plan work, compare options, or answer a set of open product/design/architecture/research questions. Do not respond with a plain-text questionnaire unless the user explicitly asks for a checklist or written questionnaire.
|
|
28
|
+
|
|
29
|
+
### Treat as `high_stakes` when the next step changes:
|
|
30
|
+
|
|
31
|
+
- architecture, schema, API contract, deployment, or security posture
|
|
32
|
+
- production-facing behavior in a costly-to-undo way
|
|
33
|
+
- large refactors, migrations, or destructive edits
|
|
34
|
+
- legal, financial, medical, career, hiring, vendor, purchasing, travel, or other costly-to-reverse decisions
|
|
35
|
+
- public-facing claims, sensitive communications, or consequential recommendations
|
|
36
|
+
|
|
37
|
+
### Treat as `ambiguous` when:
|
|
38
|
+
|
|
39
|
+
- requirements, goals, constraints, evaluation criteria, or success criteria are missing/conflicting
|
|
40
|
+
- multiple valid options exist and the trade-off is preference-sensitive
|
|
41
|
+
- research scope, audience, budget, timeline, risk tolerance, or output format is unclear
|
|
42
|
+
- you would otherwise make a material assumption
|
|
43
|
+
|
|
44
|
+
## Handshake (required)
|
|
45
|
+
|
|
46
|
+
1. Gather evidence first from code/docs/tools.
|
|
47
|
+
2. Summarize neutral context (current state, constraints, trade-offs, recommendation).
|
|
48
|
+
3. Ask one focused `ask_user` decision question, or bundle 2-5 closely related questions when the user is explicitly in requirement-gathering/interview mode.
|
|
49
|
+
4. Restate the user decision and proceed explicitly with it.
|
|
50
|
+
5. Re-open only for materially new ambiguity.
|
|
51
|
+
|
|
52
|
+
## Question spew prevention
|
|
53
|
+
|
|
54
|
+
Before sending any assistant response that contains 2+ substantive questions for the user, stop and decide whether those questions should be interactive.
|
|
55
|
+
|
|
56
|
+
Use `ask_user` instead of prose when:
|
|
57
|
+
|
|
58
|
+
- the questions are meant to collect requirements, goals, constraints, preferences, scope, priorities, criteria, or missing context
|
|
59
|
+
- answers will materially change the next artifact, recommendation, research direction, plan, implementation, architecture, schema, UX, stack choice, or decision criteria
|
|
60
|
+
- the user previously corrected you with phrases like "ask those questions", "ask interactively", or "use ask_user"
|
|
61
|
+
|
|
62
|
+
Plain-text questions are acceptable only when:
|
|
63
|
+
|
|
64
|
+
- the user asked for a written checklist/list of open questions
|
|
65
|
+
- the questions are rhetorical or purely explanatory
|
|
66
|
+
- there is exactly one small factual clarification and an interactive flow would be heavier than needed
|
|
67
|
+
|
|
68
|
+
If there are too many questions, group them into the smallest coherent `ask_user` batches and ask the highest-impact batch first.
|
|
69
|
+
|
|
70
|
+
## Question budget and escalation
|
|
71
|
+
|
|
72
|
+
- Max 1 `ask_user` call per decision boundary in normal cases.
|
|
73
|
+
- Max 2 calls for the same boundary if first answer is unclear/cancelled.
|
|
74
|
+
- Never re-ask the same trade-off without new evidence.
|
|
75
|
+
|
|
76
|
+
Attempt 2 (only if needed) must be narrower and include:
|
|
77
|
+
|
|
78
|
+
- `Proceed with recommended option`
|
|
79
|
+
- `Choose another option`
|
|
80
|
+
- `Stop for now`
|
|
81
|
+
|
|
82
|
+
After attempt 2:
|
|
83
|
+
|
|
84
|
+
- for `high_stakes` or `both`: stop as blocked until explicit decision
|
|
85
|
+
- for `ambiguous` only: if user delegates ("your call"), proceed with the most reversible default and state assumptions
|
|
86
|
+
|
|
87
|
+
## ask_user payload quality
|
|
88
|
+
|
|
89
|
+
- Ask one concrete decision at a time.
|
|
90
|
+
- Provide clear, distinct options. Do not add filler options.
|
|
91
|
+
- Choose question type from semantics: `single` means one answer is expected, `multi` means multiple answers could reasonably be selected, and `preview` means options need preview-pane detail with non-empty preview text.
|
|
92
|
+
- Avoid defaulting mechanically; infer from whether options are mutually exclusive, can coexist, or need preview-pane detail.
|
|
93
|
+
- Keep option labels short and outcome-oriented.
|
|
94
|
+
- Include trade-off descriptions when non-obvious.
|
|
95
|
+
- For research/planning, ask about goals, constraints, evaluation criteria, audience, budget, timeline, risk tolerance, and desired output only when they materially affect the result.
|
|
96
|
+
- Prefer non-`preview` questions when a free-form answer may be useful, since those include an internal `Type your own` option.
|
|
97
|
+
|
|
98
|
+
## Guardrails
|
|
99
|
+
|
|
100
|
+
- Do not ask before reading available context.
|
|
101
|
+
- Do not use for trivial formatting/style micro-decisions.
|
|
102
|
+
- Do not continue implementation after unclear high-stakes answers.
|
|
103
|
+
|
|
104
|
+
## Conflict rule
|
|
105
|
+
|
|
106
|
+
If this skill conflicts with implementation behavior or tests, the project contract wins:
|
|
107
|
+
|
|
108
|
+
1. `docs/contract.md`
|
|
109
|
+
2. `tests/*.test.ts`
|
|
110
|
+
3. this skill
|
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BorderedLoader,
|
|
3
|
+
type ExtensionAPI,
|
|
4
|
+
type ExtensionCommandContext,
|
|
5
|
+
type ExtensionContext,
|
|
6
|
+
} from "@earendil-works/pi-coding-agent";
|
|
7
|
+
import {
|
|
8
|
+
extractAskParams,
|
|
9
|
+
selectExtractionModel,
|
|
10
|
+
} from "./answer-extraction.ts";
|
|
11
|
+
import {
|
|
12
|
+
type AskPayloadSource,
|
|
13
|
+
appendAskPayload,
|
|
14
|
+
findLatestPayloadInCurrentBranch,
|
|
15
|
+
} from "./ask-payload-store.ts";
|
|
16
|
+
import {
|
|
17
|
+
invalidPayloadResponse,
|
|
18
|
+
successfulResponse,
|
|
19
|
+
validateParams,
|
|
20
|
+
} from "./ask-tool-helpers.ts";
|
|
21
|
+
import { getAskConfigStore } from "./config/store.ts";
|
|
22
|
+
import type { RemoteAskRuntime, RemoteAskSource } from "./remote-ask.ts";
|
|
23
|
+
import type { AskParams, AskResult } from "./types.ts";
|
|
24
|
+
import { runAskFlow } from "./ui/controller.ts";
|
|
25
|
+
|
|
26
|
+
interface AssistantTextSource {
|
|
27
|
+
entryId: string;
|
|
28
|
+
text: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
type ExtractionSelection = Exclude<
|
|
32
|
+
Awaited<ReturnType<typeof selectExtractionModel>>,
|
|
33
|
+
{ error: string }
|
|
34
|
+
>;
|
|
35
|
+
|
|
36
|
+
type ExtractionUiResult =
|
|
37
|
+
| { cancelled: true }
|
|
38
|
+
| { error: string }
|
|
39
|
+
| { params: AskParams };
|
|
40
|
+
|
|
41
|
+
export function registerAnswerCommands(
|
|
42
|
+
pi: ExtensionAPI,
|
|
43
|
+
remoteAsk?: RemoteAskRuntime
|
|
44
|
+
): void {
|
|
45
|
+
pi.registerCommand("answer", {
|
|
46
|
+
description:
|
|
47
|
+
"Extract questions from the latest assistant message into an ask form",
|
|
48
|
+
handler: async (_args, ctx) => runAnswerCommand(pi, ctx, remoteAsk),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
pi.registerCommand("answer:again", {
|
|
52
|
+
description: "Reopen the previous /answer form on this branch",
|
|
53
|
+
handler: async (_args, ctx) =>
|
|
54
|
+
runReplayCommand(pi, ctx, {
|
|
55
|
+
missingMessage:
|
|
56
|
+
"No previous /answer form found on this branch; use /answer first.",
|
|
57
|
+
noticePrefix: "Reopening previous /answer form on this branch",
|
|
58
|
+
remoteSource: "answer:again",
|
|
59
|
+
source: "answer-extraction",
|
|
60
|
+
remoteAsk,
|
|
61
|
+
}),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
pi.registerCommand("ask:replay", {
|
|
65
|
+
description: "Replay the previous ask_user form on this branch",
|
|
66
|
+
handler: async (_args, ctx) =>
|
|
67
|
+
runReplayCommand(pi, ctx, {
|
|
68
|
+
missingMessage: "No previous ask_user form found on this branch.",
|
|
69
|
+
noticePrefix: "Replaying previous ask_user form on this branch",
|
|
70
|
+
remoteSource: "ask:replay",
|
|
71
|
+
source: "tool",
|
|
72
|
+
remoteAsk,
|
|
73
|
+
}),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function runAnswerCommand(
|
|
78
|
+
pi: ExtensionAPI,
|
|
79
|
+
ctx: ExtensionCommandContext,
|
|
80
|
+
remoteAsk?: RemoteAskRuntime
|
|
81
|
+
): Promise<void> {
|
|
82
|
+
if (ctx.mode !== "tui") {
|
|
83
|
+
ctx.ui.notify("/answer requires interactive TUI mode.", "error");
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const assistant = findLatestAssistantText(ctx);
|
|
88
|
+
if ("error" in assistant) {
|
|
89
|
+
ctx.ui.notify(assistant.error, "error");
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const config = await getAskConfigStore().getConfig();
|
|
94
|
+
const selected = await selectExtractionModel(
|
|
95
|
+
ctx,
|
|
96
|
+
config.answer.extractionModels
|
|
97
|
+
);
|
|
98
|
+
if ("error" in selected) {
|
|
99
|
+
ctx.ui.notify(selected.error, "error");
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (selected.usedFallback) {
|
|
103
|
+
ctx.ui.notify(
|
|
104
|
+
`Configured extraction models unavailable; using current chat model ${selected.model.provider}/${selected.model.id}.`,
|
|
105
|
+
"info"
|
|
106
|
+
);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const params = await extractAndValidateAnswerParams(
|
|
110
|
+
ctx,
|
|
111
|
+
assistant,
|
|
112
|
+
selected,
|
|
113
|
+
config
|
|
114
|
+
);
|
|
115
|
+
if (!params) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
appendAskPayload(pi, {
|
|
120
|
+
params,
|
|
121
|
+
source: "answer-extraction",
|
|
122
|
+
sourceEntryId: assistant.entryId,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
await runAskAndSendSubmittedResult(pi, ctx, params, {
|
|
126
|
+
allowFreeform: true,
|
|
127
|
+
remoteAsk,
|
|
128
|
+
remoteSource: "answer",
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function extractAndValidateAnswerParams(
|
|
133
|
+
ctx: ExtensionCommandContext,
|
|
134
|
+
assistant: AssistantTextSource,
|
|
135
|
+
selected: ExtractionSelection,
|
|
136
|
+
config: Awaited<ReturnType<ReturnType<typeof getAskConfigStore>["getConfig"]>>
|
|
137
|
+
): Promise<AskParams | undefined> {
|
|
138
|
+
const extraction = await withHiddenWorkingRow(ctx, () =>
|
|
139
|
+
runExtractionUi(ctx, assistant, selected, config)
|
|
140
|
+
);
|
|
141
|
+
if ("cancelled" in extraction) {
|
|
142
|
+
ctx.ui.notify("Question extraction cancelled.", "info");
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
if ("error" in extraction) {
|
|
146
|
+
ctx.ui.notify(formatExtractionError(extraction.error), "error");
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (!isAskParamsCandidate(extraction.params)) {
|
|
150
|
+
ctx.ui.notify("Question extraction returned an invalid ask form.", "error");
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (extraction.params.questions.length === 0) {
|
|
154
|
+
ctx.ui.notify(
|
|
155
|
+
"No questions found in the latest assistant message.",
|
|
156
|
+
"info"
|
|
157
|
+
);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
const validation = validateParams(extraction.params, { allowFreeform: true });
|
|
161
|
+
if (!validation.ok) {
|
|
162
|
+
const response = invalidPayloadResponse(
|
|
163
|
+
extraction.params,
|
|
164
|
+
validation.issues
|
|
165
|
+
);
|
|
166
|
+
ctx.ui.notify(response.content[0].text, "error");
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
return extraction.params;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function runExtractionUi(
|
|
173
|
+
ctx: ExtensionCommandContext,
|
|
174
|
+
assistant: AssistantTextSource,
|
|
175
|
+
selected: ExtractionSelection,
|
|
176
|
+
config: Awaited<ReturnType<ReturnType<typeof getAskConfigStore>["getConfig"]>>
|
|
177
|
+
): Promise<ExtractionUiResult> {
|
|
178
|
+
return ctx.ui.custom<ExtractionUiResult>((tui, theme, _keybindings, done) => {
|
|
179
|
+
let completed = false;
|
|
180
|
+
const doneOnce = (result: ExtractionUiResult) => {
|
|
181
|
+
if (completed) {
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
completed = true;
|
|
185
|
+
done(result);
|
|
186
|
+
};
|
|
187
|
+
const loader = new BorderedLoader(
|
|
188
|
+
tui,
|
|
189
|
+
theme,
|
|
190
|
+
`Extracting questions using ${selected.model.provider}/${selected.model.id}...`
|
|
191
|
+
);
|
|
192
|
+
loader.onAbort = () => doneOnce({ cancelled: true });
|
|
193
|
+
extractAskParams({
|
|
194
|
+
assistantText: assistant.text,
|
|
195
|
+
auth: selected.auth,
|
|
196
|
+
model: selected.model,
|
|
197
|
+
onRetry: (attempt, maxRetries) => {
|
|
198
|
+
ctx.ui.notify(
|
|
199
|
+
`Retrying extraction JSON repair (${attempt}/${maxRetries})...`,
|
|
200
|
+
"info"
|
|
201
|
+
);
|
|
202
|
+
},
|
|
203
|
+
retries: config.answer.extractionRetries,
|
|
204
|
+
signal: loader.signal,
|
|
205
|
+
timeoutMs: config.answer.extractionTimeoutMs,
|
|
206
|
+
})
|
|
207
|
+
.then((params) => doneOnce({ params }))
|
|
208
|
+
.catch((error) => doneOnce({ error: formatErrorMessage(error) }));
|
|
209
|
+
return loader;
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function formatErrorMessage(error: unknown): string {
|
|
214
|
+
if (error instanceof Error) {
|
|
215
|
+
return error.name ? `${error.name}: ${error.message}` : error.message;
|
|
216
|
+
}
|
|
217
|
+
return String(error);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function isAskParamsCandidate(value: unknown): value is AskParams {
|
|
221
|
+
return (
|
|
222
|
+
!!value &&
|
|
223
|
+
typeof value === "object" &&
|
|
224
|
+
Array.isArray((value as { questions?: unknown }).questions)
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function formatExtractionError(error: string): string {
|
|
229
|
+
if (error.includes("valid JSON")) {
|
|
230
|
+
return error;
|
|
231
|
+
}
|
|
232
|
+
if (error.includes("cancelled")) {
|
|
233
|
+
return "Question extraction cancelled.";
|
|
234
|
+
}
|
|
235
|
+
if (
|
|
236
|
+
error.includes("aborted") ||
|
|
237
|
+
error.includes("AbortError") ||
|
|
238
|
+
error.includes("timed out")
|
|
239
|
+
) {
|
|
240
|
+
return "Question extraction timed out. Try again or configure a faster extraction model.";
|
|
241
|
+
}
|
|
242
|
+
return error;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async function runReplayCommand(
|
|
246
|
+
pi: ExtensionAPI,
|
|
247
|
+
ctx: ExtensionCommandContext,
|
|
248
|
+
options: {
|
|
249
|
+
missingMessage: string;
|
|
250
|
+
noticePrefix: string;
|
|
251
|
+
remoteAsk?: RemoteAskRuntime;
|
|
252
|
+
remoteSource: RemoteAskSource;
|
|
253
|
+
source: AskPayloadSource;
|
|
254
|
+
}
|
|
255
|
+
): Promise<void> {
|
|
256
|
+
if (ctx.mode !== "tui") {
|
|
257
|
+
ctx.ui.notify("Ask replay requires interactive TUI mode.", "error");
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
const lookup = findLatestPayloadInCurrentBranch(ctx, options.source);
|
|
261
|
+
if (!lookup.data) {
|
|
262
|
+
ctx.ui.notify(
|
|
263
|
+
lookup.invalidMatchFound
|
|
264
|
+
? "Previous form exists on this branch but is no longer compatible."
|
|
265
|
+
: options.missingMessage,
|
|
266
|
+
"info"
|
|
267
|
+
);
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
ctx.ui.notify(
|
|
271
|
+
`${options.noticePrefix}: ${lookup.data.params.questions.length} question(s).`,
|
|
272
|
+
"info"
|
|
273
|
+
);
|
|
274
|
+
await runAskAndSendSubmittedResult(pi, ctx, lookup.data.params, {
|
|
275
|
+
allowFreeform: options.source === "answer-extraction",
|
|
276
|
+
remoteAsk: options.remoteAsk,
|
|
277
|
+
remoteSource: options.remoteSource,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function runAskAndSendSubmittedResult(
|
|
282
|
+
pi: Pick<ExtensionAPI, "sendUserMessage">,
|
|
283
|
+
ctx: ExtensionContext,
|
|
284
|
+
params: AskParams,
|
|
285
|
+
options: {
|
|
286
|
+
allowFreeform: boolean;
|
|
287
|
+
remoteAsk?: RemoteAskRuntime;
|
|
288
|
+
remoteSource: RemoteAskSource;
|
|
289
|
+
}
|
|
290
|
+
): Promise<void> {
|
|
291
|
+
const result = await withHiddenWorkingRow(ctx, () =>
|
|
292
|
+
runAskFlow(ctx, params, {
|
|
293
|
+
allowFreeform: options.allowFreeform,
|
|
294
|
+
remote: options.remoteAsk
|
|
295
|
+
? { runtime: options.remoteAsk, source: options.remoteSource }
|
|
296
|
+
: undefined,
|
|
297
|
+
})
|
|
298
|
+
);
|
|
299
|
+
if (result.cancelled) {
|
|
300
|
+
ctx.ui.notify("Ask form cancelled.", "info");
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
sendAskResult(pi, result, ctx);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
async function withHiddenWorkingRow<T>(
|
|
307
|
+
ctx: ExtensionContext,
|
|
308
|
+
run: () => Promise<T>
|
|
309
|
+
): Promise<T> {
|
|
310
|
+
ctx.ui.setWorkingVisible(false);
|
|
311
|
+
try {
|
|
312
|
+
return await run();
|
|
313
|
+
} finally {
|
|
314
|
+
ctx.ui.setWorkingVisible(true);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function sendAskResult(
|
|
319
|
+
pi: Pick<ExtensionAPI, "sendUserMessage">,
|
|
320
|
+
result: AskResult,
|
|
321
|
+
ctx: Pick<ExtensionContext, "isIdle">
|
|
322
|
+
): void {
|
|
323
|
+
const response = successfulResponse(result);
|
|
324
|
+
const text = response.content[0];
|
|
325
|
+
pi.sendUserMessage(
|
|
326
|
+
text.text,
|
|
327
|
+
ctx.isIdle() ? undefined : { deliverAs: "followUp" }
|
|
328
|
+
);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function findLatestAssistantText(
|
|
332
|
+
ctx: Pick<ExtensionContext, "sessionManager">
|
|
333
|
+
): AssistantTextSource | { error: string } {
|
|
334
|
+
const branch = ctx.sessionManager.getBranch();
|
|
335
|
+
for (let index = branch.length - 1; index >= 0; index--) {
|
|
336
|
+
const entry = branch[index];
|
|
337
|
+
if (entry.type !== "message") {
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
const message = entry.message;
|
|
341
|
+
if (!("role" in message) || message.role !== "assistant") {
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
if (message.stopReason !== "stop") {
|
|
345
|
+
return {
|
|
346
|
+
error: `Latest assistant message is incomplete (${message.stopReason}); wait for it to finish, then run /answer again.`,
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
const text = message.content
|
|
350
|
+
.filter(
|
|
351
|
+
(part): part is { text: string; type: "text" } => part.type === "text"
|
|
352
|
+
)
|
|
353
|
+
.map((part) => part.text)
|
|
354
|
+
.join("\n")
|
|
355
|
+
.trim();
|
|
356
|
+
if (text) {
|
|
357
|
+
return { entryId: entry.id, text };
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return { error: "No assistant message found to extract questions from." };
|
|
361
|
+
}
|