@fitchmultz/pi-ask-question 0.6.0 → 0.6.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 CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.1 — 2026-10-31
4
+
5
+ - Disable the AFK timeout after TUI interaction or a submitted RPC choice, so an in-progress answer remains open.
6
+
3
7
  ## 0.6.0 — 2026-10-31
4
8
 
5
9
  - 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,7 +2,7 @@
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
- ![Pi calls ask_question, shows choices, and returns your answer to the agent. A separate five-minute timeout returns an AFK reply.](.github/readme/question-flow.png)
5
+ ![Pi calls ask_question, shows choices, and returns your answer to the agent. Five minutes without interaction returns an AFK reply.](.github/readme/question-flow.png)
6
6
 
7
7
  ## Install
8
8
 
@@ -33,7 +33,9 @@ Escape leaves the custom answer editor; otherwise, it cancels the question. Your
33
33
 
34
34
  ## Limits
35
35
 
36
- Each call has one five-minute limit for all questions. At timeout, the agent receives an AFK reply and can use its best judgment.
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
- deadline: number,
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 || Date.now() >= deadline) return undefined;
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, deadline,
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 with an AFK reply.", { style: (text) => theme.fg("dim", text) });
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 timeout = new AbortController();
928
+ const timeoutController = new AbortController();
908
929
  const deadline = Date.now() + QUESTION_TIMEOUT_MS;
909
- const timer = setTimeout(() => timeout.abort(), QUESTION_TIMEOUT_MS);
910
- const questionSignal = signal ? AbortSignal.any([signal, timeout.signal]) : timeout.signal;
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, deadline, questionSignal)
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 && timeout.signal.aborted && !signal?.aborted;
948
+ const timedOut = result.cancelled && timeoutController.signal.aborted && !signal?.aborted;
917
949
  const cancelled = result.cancelled && !timedOut;
918
950
 
919
951
  return {
@@ -10,6 +10,7 @@ type Target = {
10
10
 
11
11
  type Waiting = Target & {
12
12
  status: "waiting";
13
+ timeoutActive: boolean;
13
14
  question: string;
14
15
  options: string[];
15
16
  multiSelect: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fitchmultz/pi-ask-question",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
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",