@fitchmultz/pi-ask-question 0.6.0 → 0.6.2
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 +21 -0
- package/README.md +5 -3
- package/extensions/ask-question.ts +45 -13
- package/extensions/lib/question-observer.ts +1 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
## 0.6.2 - 2026-10-31
|
|
6
|
+
|
|
7
|
+
<!-- npm-release:unreleased:start -->
|
|
8
|
+
- Clarify compatibility and release guidance and remove outdated Git-only advice for the cooperative question bridge.
|
|
9
|
+
<!-- npm-release:unreleased:end -->
|
|
10
|
+
|
|
11
|
+
- Automate reviewed npm patch releases (#51) ([e00bcb5](https://github.com/fitchmultz/pi-ask-question/commit/e00bcb54c99e0144ff0bfdc2249bfeef350ac413))
|
|
12
|
+
|
|
13
|
+
### What's Changed
|
|
14
|
+
* Clarify compatibility and release guidance by @fitchmultz in https://github.com/fitchmultz/pi-ask-question/pull/50
|
|
15
|
+
* Automate reviewed npm patch releases by @fitchmultz in https://github.com/fitchmultz/pi-ask-question/pull/51
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
**Full Changelog**: https://github.com/fitchmultz/pi-ask-question/compare/v0.6.1...v0.6.2
|
|
19
|
+
|
|
20
|
+
## 0.6.1 — 2026-10-31
|
|
21
|
+
|
|
22
|
+
- Disable the AFK timeout after TUI interaction or a submitted RPC choice, so an in-progress answer remains open.
|
|
23
|
+
|
|
3
24
|
## 0.6.0 — 2026-10-31
|
|
4
25
|
|
|
5
26
|
- Keep preceding conversation text visible above TUI questions and let users read the transcript without cancelling or losing their answer.
|
package/README.md
CHANGED
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
This [Pi](https://github.com/earendil-works/pi) extension asks for your decisions before the agent continues a task. Use `/grill-me` to guide the agent through unclear requirements.
|
|
4
4
|
|
|
5
|
-

|
|
6
6
|
|
|
7
7
|
## Install
|
|
8
8
|
|
|
9
|
-
Requires
|
|
9
|
+
Requires Node.js and a supported Pi release. See the [compatibility requirements](https://github.com/fitchmultz/pi-ask-question/blob/main/docs/reference.md#installation-and-updates). Official Pi releases and Mitch's Pi fork are supported.
|
|
10
10
|
|
|
11
11
|
Run these commands:
|
|
12
12
|
|
|
@@ -33,7 +33,9 @@ Escape leaves the custom answer editor; otherwise, it cancels the question. Your
|
|
|
33
33
|
|
|
34
34
|
## Limits
|
|
35
35
|
|
|
36
|
-
|
|
36
|
+
An untouched question times out after five minutes, and the agent receives an AFK reply. Once you interact, there is no time limit for that question or question set. Each new call starts its own timer.
|
|
37
|
+
|
|
38
|
+
In RPC mode, Pi learns about interaction when the client submits a dialog choice; navigating an unsubmitted dialog is not visible to Pi.
|
|
37
39
|
|
|
38
40
|
The tool keeps saved answers. It does not submit unsaved custom answer text or select an answer for you.
|
|
39
41
|
|
|
@@ -39,6 +39,12 @@ type GrillMeState = {
|
|
|
39
39
|
|
|
40
40
|
type GrillMeAction = "toggle" | "enable" | "disable" | "status" | "invalid";
|
|
41
41
|
|
|
42
|
+
type QuestionTimeout = {
|
|
43
|
+
deadline: number;
|
|
44
|
+
readonly active: boolean;
|
|
45
|
+
stop(): boolean;
|
|
46
|
+
};
|
|
47
|
+
|
|
42
48
|
const QUESTION_TIMEOUT_MS = 5 * 60 * 1000;
|
|
43
49
|
const TIMEOUT_MESSAGE = "Mitch is currently AFK. Use your best judgement to choose the option Mitch would choose.";
|
|
44
50
|
const CUSTOM_OPTION = "Type a custom answer";
|
|
@@ -176,7 +182,7 @@ async function askWithKeyboard(
|
|
|
176
182
|
ui: ExtensionContext["ui"],
|
|
177
183
|
pi: ExtensionAPI,
|
|
178
184
|
observer: QuestionObserver,
|
|
179
|
-
|
|
185
|
+
timeout: QuestionTimeout,
|
|
180
186
|
signal?: AbortSignal,
|
|
181
187
|
): Promise<{ answers: Answer[]; cancelled: boolean }> {
|
|
182
188
|
if (signal?.aborted) return { answers: [], cancelled: true };
|
|
@@ -250,6 +256,7 @@ async function askWithKeyboard(
|
|
|
250
256
|
};
|
|
251
257
|
const unsubscribeInput = tui.addInputListener((data) => {
|
|
252
258
|
if (matchesKey(data, Key.ctrl("o")) && (readingContext || overlayHandle?.isFocused())) {
|
|
259
|
+
markInteraction();
|
|
253
260
|
toggleContext();
|
|
254
261
|
return { consume: true };
|
|
255
262
|
}
|
|
@@ -263,9 +270,11 @@ async function askWithKeyboard(
|
|
|
263
270
|
const acceptedRequests = new Set<string>();
|
|
264
271
|
function waitingState() {
|
|
265
272
|
const question = current();
|
|
266
|
-
if (!announced || finished || signal?.aborted || editing || !question ||
|
|
273
|
+
if (!announced || finished || signal?.aborted || editing || !question ||
|
|
274
|
+
(timeout.active && Date.now() >= timeout.deadline)) return undefined;
|
|
267
275
|
return {
|
|
268
|
-
status: "waiting" as const, ...observer.identity(), promptId, questionId: question.id, revision,
|
|
276
|
+
status: "waiting" as const, ...observer.identity(), promptId, questionId: question.id, revision,
|
|
277
|
+
deadline: timeout.deadline, timeoutActive: timeout.active,
|
|
269
278
|
question: question.question, options: [...question.options], multiSelect: question.multiSelect,
|
|
270
279
|
};
|
|
271
280
|
}
|
|
@@ -280,6 +289,11 @@ async function askWithKeyboard(
|
|
|
280
289
|
closed = false;
|
|
281
290
|
pi.events.emit("pi-ask-question:state", waiting);
|
|
282
291
|
}
|
|
292
|
+
function markInteraction() {
|
|
293
|
+
if (!timeout.stop()) return;
|
|
294
|
+
refresh();
|
|
295
|
+
publishState();
|
|
296
|
+
}
|
|
283
297
|
function stateChanged() {
|
|
284
298
|
revision += 1;
|
|
285
299
|
if (publishQueued) return;
|
|
@@ -307,7 +321,7 @@ async function askWithKeyboard(
|
|
|
307
321
|
const question = current();
|
|
308
322
|
if (finished || signal?.aborted || editing || !question || request.questionId !== question.id ||
|
|
309
323
|
request.revision !== revision || acceptedRequests.has(request.requestId) ||
|
|
310
|
-
!observer.matches(request, deadline) || Date.now() >= deadline) {
|
|
324
|
+
!observer.matches(request, timeout.deadline) || (timeout.active && Date.now() >= timeout.deadline)) {
|
|
311
325
|
receipt("stale");
|
|
312
326
|
return;
|
|
313
327
|
}
|
|
@@ -322,8 +336,9 @@ async function askWithKeyboard(
|
|
|
322
336
|
receipt("invalid");
|
|
323
337
|
return;
|
|
324
338
|
}
|
|
325
|
-
const target = { promptId, questionId: question.id, revision, deadline };
|
|
339
|
+
const target = { promptId, questionId: question.id, revision, deadline: timeout.deadline };
|
|
326
340
|
acceptedRequests.add(request.requestId);
|
|
341
|
+
timeout.stop();
|
|
327
342
|
if (typeof answer === "string") {
|
|
328
343
|
if (question.multiSelect) {
|
|
329
344
|
saveMultiCustomAnswer(question, answer.trim());
|
|
@@ -462,6 +477,7 @@ async function askWithKeyboard(
|
|
|
462
477
|
|
|
463
478
|
function handleInput(data: string) {
|
|
464
479
|
if (finished) return;
|
|
480
|
+
markInteraction();
|
|
465
481
|
if (editing) {
|
|
466
482
|
if (maxQuestionScroll && matchesKey(data, Key.shift("pageUp"))) {
|
|
467
483
|
questionScroll = Math.max(0, questionScroll - Math.max(1, questionPageRows - 1));
|
|
@@ -674,7 +690,9 @@ async function askWithKeyboard(
|
|
|
674
690
|
}
|
|
675
691
|
|
|
676
692
|
if (!editing) add();
|
|
677
|
-
addWrapped("Auto-continues after 5 minutes
|
|
693
|
+
addWrapped(timeout.active ? "Auto-continues after 5 minutes without interaction." : "No time limit while you answer.", {
|
|
694
|
+
style: (text) => theme.fg("dim", text),
|
|
695
|
+
});
|
|
678
696
|
addWrapped(
|
|
679
697
|
editing
|
|
680
698
|
? `${keyText("tui.input.submit")} save answer • ${keyText("tui.select.cancel")} cancel edit`
|
|
@@ -775,6 +793,7 @@ async function askWithKeyboard(
|
|
|
775
793
|
render,
|
|
776
794
|
handleInput,
|
|
777
795
|
handleMouse(event: TuiMouseEvent) {
|
|
796
|
+
markInteraction();
|
|
778
797
|
if (event.type === "click" && event.button === "left" &&
|
|
779
798
|
event.y >= contextHintStart && event.y < contextHintEnd &&
|
|
780
799
|
event.x < contextHintWidth) {
|
|
@@ -799,6 +818,7 @@ async function askWithKeyboard(
|
|
|
799
818
|
async function askWithDialogs(
|
|
800
819
|
questions: NormalizedQuestion[],
|
|
801
820
|
ui: ExtensionContext["ui"],
|
|
821
|
+
timeout: QuestionTimeout,
|
|
802
822
|
signal?: AbortSignal,
|
|
803
823
|
): Promise<{ answers: Answer[]; cancelled: boolean }> {
|
|
804
824
|
const answers = new Map<string, Answer>();
|
|
@@ -818,6 +838,7 @@ async function askWithDialogs(
|
|
|
818
838
|
if (question.multiSelect && selected.length) choices.push(doneOption);
|
|
819
839
|
const choice = await ui.select(question.question, choices, { signal });
|
|
820
840
|
if (choice === undefined) return { answers: orderedAnswers(questions, answers), cancelled: true };
|
|
841
|
+
timeout.stop();
|
|
821
842
|
if (choice === doneOption) break;
|
|
822
843
|
|
|
823
844
|
let answer = options[labels.indexOf(choice)] ?? choice;
|
|
@@ -888,7 +909,7 @@ function restoreGrillMeState(entries: Iterable<unknown>): boolean {
|
|
|
888
909
|
const askQuestionTool = (pi: ExtensionAPI, observer: QuestionObserver) => defineTool({
|
|
889
910
|
name: "ask_question",
|
|
890
911
|
label: "Ask Question",
|
|
891
|
-
description: "Ask the user one or more clarifying questions through pi's UI. After 5 minutes, returns an AFK reply so you can continue using your best judgement.",
|
|
912
|
+
description: "Ask the user one or more clarifying questions through pi's UI. After 5 minutes without interaction, returns an AFK reply so you can continue using your best judgement.",
|
|
892
913
|
promptSnippet: "Ask the user clarifying questions through pi's UI",
|
|
893
914
|
promptGuidelines: [
|
|
894
915
|
"For ask_question, list options from most recommended to least; the first option is the recommended choice, and do not label it as recommended.",
|
|
@@ -904,16 +925,27 @@ const askQuestionTool = (pi: ExtensionAPI, observer: QuestionObserver) => define
|
|
|
904
925
|
if (!ctx.hasUI) throw new Error("ask_question needs Pi TUI or RPC UI support.");
|
|
905
926
|
|
|
906
927
|
observer.identify(ctx);
|
|
907
|
-
const
|
|
928
|
+
const timeoutController = new AbortController();
|
|
908
929
|
const deadline = Date.now() + QUESTION_TIMEOUT_MS;
|
|
909
|
-
const timer = setTimeout(() =>
|
|
910
|
-
|
|
930
|
+
const timer = setTimeout(() => timeoutController.abort(), QUESTION_TIMEOUT_MS);
|
|
931
|
+
let active = true;
|
|
932
|
+
const timeout: QuestionTimeout = {
|
|
933
|
+
deadline,
|
|
934
|
+
get active() { return active; },
|
|
935
|
+
stop() {
|
|
936
|
+
if (!active) return false;
|
|
937
|
+
active = false;
|
|
938
|
+
clearTimeout(timer);
|
|
939
|
+
return true;
|
|
940
|
+
},
|
|
941
|
+
};
|
|
942
|
+
const questionSignal = signal ? AbortSignal.any([signal, timeoutController.signal]) : timeoutController.signal;
|
|
911
943
|
try {
|
|
912
944
|
const result = ctx.mode === "tui"
|
|
913
|
-
? await askWithKeyboard(questions, ctx.ui, pi, observer,
|
|
914
|
-
: await askWithDialogs(questions, ctx.ui, questionSignal);
|
|
945
|
+
? await askWithKeyboard(questions, ctx.ui, pi, observer, timeout, questionSignal)
|
|
946
|
+
: await askWithDialogs(questions, ctx.ui, timeout, questionSignal);
|
|
915
947
|
const answers = new Map(result.answers.map((answer) => [answer.id, answer]));
|
|
916
|
-
const timedOut = result.cancelled &&
|
|
948
|
+
const timedOut = result.cancelled && timeoutController.signal.aborted && !signal?.aborted;
|
|
917
949
|
const cancelled = result.cancelled && !timedOut;
|
|
918
950
|
|
|
919
951
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fitchmultz/pi-ask-question",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.2",
|
|
4
4
|
"description": "Pi extension that adds an interactive ask_question tool and /grill-me pressure-test mode",
|
|
5
5
|
"author": "Mitch Fultz (https://github.com/fitchmultz)",
|
|
6
6
|
"license": "MIT",
|