@fyeeme/pi-ask-user-lite 2.0.1 → 2.0.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.
Files changed (3) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/index.ts +80 -1
  3. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [2.0.2] - 2026-09-09
6
+
7
+ ### Fixed
8
+
9
+ - `ask_user` no longer crashes in RPC mode: `ctx.ui.custom()` is TUI-only (it resolves to `undefined` under RPC), so RPC hosts now degrade to native `select`/`input` dialogs — multi-select via a "Done selecting" pick loop, "Other" via `input`, and timeout auto-selecting the recommended options.
10
+
5
11
  ## [2.0.1] - 2026-08-27
6
12
 
7
13
  First release under the `@fyeeme/pi-ask-user-lite` name; the entries below describe the 2.0.0 feature set published under the old `@fyeeme/pi-ask-user` name (code is identical apart from the rename).
package/index.ts CHANGED
@@ -658,6 +658,80 @@ async function runAskDialog(
658
658
  return { kind: action.kind, timedOut: action.timedOut === true, state };
659
659
  }
660
660
 
661
+ // ---------------------------------------------------------------------------
662
+ // RPC fallback: native select/input dialogs (no ctx.ui.custom — RPC hosts
663
+ // resolve custom() to undefined; Mode Behavior doc requires a ctx.mode ===
664
+ // "tui" guard around custom()). Mirrors omp's legacy per-question selector:
665
+ // single-select per pick, a "Done selecting" terminal option for multi
666
+ // questions, free-text Other via input, and timeout auto-selection of the
667
+ // recommended options for every unanswered question.
668
+ // ---------------------------------------------------------------------------
669
+
670
+ type RpcAskUi = Pick<ExtensionUIContext, "select" | "input">;
671
+
672
+ const RPC_DONE_LABEL = "Done selecting";
673
+ /** Window after the deadline within which an `undefined` pick is attributed
674
+ * to the UI-enforced timeout rather than a user Escape (omp heuristic). */
675
+ const RPC_TIMEOUT_TOLERANCE_MS = 500;
676
+
677
+ async function runAskDialogRpc(
678
+ ui: RpcAskUi,
679
+ questions: AskQuestion[],
680
+ options_: { deadline?: number; signal?: AbortSignal },
681
+ ): Promise<DialogOutcome> {
682
+ const answers: Array<{ selectedOptions: string[]; customInput?: string } | undefined> = questions.map(() => undefined);
683
+ const notes: Array<string | undefined> = questions.map(() => undefined);
684
+ const state: DialogState = { index: 0, cursors: [], checked: [], answers, notes };
685
+ const remainingMs = () => (options_.deadline === undefined ? undefined : Math.max(0, options_.deadline - Date.now()));
686
+ const timedOutAt = () => options_.deadline !== undefined && Date.now() >= options_.deadline - RPC_TIMEOUT_TOLERANCE_MS;
687
+ const dialogOpts = () => ({ timeout: remainingMs(), signal: options_.signal });
688
+
689
+ for (const [index, q] of questions.entries()) {
690
+ state.index = index;
691
+ const labels = addRecommendedSuffix(q.options, q.multi ? undefined : q.recommended);
692
+ const tail = q.multi ? [RPC_DONE_LABEL, OTHER_OPTION, CHAT_OPTION] : [OTHER_OPTION, CHAT_OPTION];
693
+ const selected = new Set<string>();
694
+
695
+ for (;;) {
696
+ const choice = await ui.select(q.question, [...labels, ...tail], dialogOpts());
697
+ if (choice === undefined) {
698
+ if (timedOutAt()) {
699
+ return { kind: "submit", timedOut: true, state };
700
+ }
701
+ return { kind: "cancel", timedOut: false, state };
702
+ }
703
+ if (choice === CHAT_OPTION) {
704
+ return { kind: "chat", timedOut: false, state };
705
+ }
706
+ if (choice === OTHER_OPTION) {
707
+ const custom = await ui.input(`Your answer for "${q.header ?? q.question}":`, undefined, dialogOpts());
708
+ if (custom === undefined) {
709
+ if (timedOutAt()) return { kind: "submit", timedOut: true, state };
710
+ return { kind: "cancel", timedOut: false, state };
711
+ }
712
+ if (custom.length > 0) {
713
+ answers[index] = { selectedOptions: [], customInput: custom }; // omp: re-selecting replaces
714
+ break;
715
+ }
716
+ continue; // empty submit declines the custom input — back to the rows
717
+ }
718
+ if (q.multi) {
719
+ if (choice === RPC_DONE_LABEL) {
720
+ answers[index] = { selectedOptions: [...selected] };
721
+ break;
722
+ }
723
+ const label = stripRecommendedSuffix(choice);
724
+ if (selected.has(label)) selected.delete(label);
725
+ else selected.add(label);
726
+ continue; // keep picking until Done
727
+ }
728
+ answers[index] = { selectedOptions: [stripRecommendedSuffix(choice)] };
729
+ break;
730
+ }
731
+ }
732
+ return { kind: "submit", timedOut: false, state };
733
+ }
734
+
661
735
  function buildResults(questions: AskQuestion[], state: DialogState, timedOut: boolean): QuestionResult[] {
662
736
  return questions.map((q, i) => {
663
737
  const answer = state.answers[i];
@@ -725,7 +799,12 @@ const askUserTool: ToolDefinition<typeof AskParamsSchema, AskUserDetails> = {
725
799
  const deadline =
726
800
  typeof params.timeoutSeconds === "number" && params.timeoutSeconds > 0 ? Date.now() + params.timeoutSeconds * 1000 : undefined;
727
801
 
728
- const outcome = await runAskDialog(ctx.ui, questions, { deadline, signal });
802
+ // ctx.ui.custom() is TUI-only (Mode Behavior): RPC hosts resolve it to
803
+ // undefined, so degrade to native select/input dialogs there.
804
+ const outcome =
805
+ ctx.mode === "tui"
806
+ ? await runAskDialog(ctx.ui, questions, { deadline, signal })
807
+ : await runAskDialogRpc(ctx.ui, questions, { deadline, signal });
729
808
 
730
809
  if (outcome.kind === "chat") {
731
810
  const questionTexts = questions.map((q) => q.question).join("\n");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fyeeme/pi-ask-user-lite",
3
- "version": "2.0.1",
3
+ "version": "2.0.2",
4
4
  "description": "Structured ask_user tool for pi — lets the LLM surface clarifying questions with options, multi-select, recommended defaults, and free-form Other input.",
5
5
  "type": "module",
6
6
  "license": "MIT",