@fyeeme/pi-ask-user-lite 2.0.1 → 2.0.3

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 +13 -0
  2. package/index.ts +91 -4
  3. package/package.json +5 -5
package/CHANGELOG.md CHANGED
@@ -2,6 +2,19 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [2.0.3] - 2026-10-01
6
+
7
+ ### Changed
8
+
9
+ - `ask_user` now registers with pi 0.99 `exposure: "model-only"`: declared to the model, unreachable from codemode scripts and nested tool execution (ask-the-user is a model-level decision). No `outputSchema` is declared — model-only tools have no programmatic consumer of structured results. Model-facing behavior is unchanged.
10
+ - Peer dependency floor raised to `@earendil-works/pi-coding-agent >= 0.99.0`; dev toolchain pinned to 0.99.2.
11
+
12
+ ## [2.0.2] - 2026-09-09
13
+
14
+ ### Fixed
15
+
16
+ - `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.
17
+
5
18
  ## [2.0.1] - 2026-08-27
6
19
 
7
20
  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
@@ -15,7 +15,7 @@
15
15
  * `/tree` inspection.
16
16
  */
17
17
 
18
- import type { ExtensionAPI, ExtensionCommandContext, ExtensionUIContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
18
+ import type { ExtensionAPI, ExtensionCommandContext, ExtensionToolContext, ExtensionUIContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
19
19
  import type { Theme } from "@earendil-works/pi-coding-agent";
20
20
  import { Editor, type EditorTheme, Key, matchesKey, Text, type TUI, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
21
21
  import { Type } from "typebox";
@@ -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];
@@ -687,7 +761,12 @@ const askUserTool: ToolDefinition<typeof AskParamsSchema, AskUserDetails> = {
687
761
  "Ask the user one or more clarifying questions with selectable options. " +
688
762
  "Use when choices have materially different tradeoffs the user must decide. " +
689
763
  "The user can always provide free-form input via an automatic 'Other' option.",
690
- promptSnippet: "Ask the user structured questions with options during execution",
764
+ // pi 0.99: model-only — declared to the model, unreachable from codemode
765
+ // scripts / nested tool execution (ask-the-user is a model-level decision;
766
+ // spec: ask-user-exposure). No outputSchema: model-only tools have no
767
+ // programmatic consumer of structuredContent.
768
+ exposure: "model-only",
769
+ promptSnippet: "Ask the user structured questions with options during execution",
691
770
  promptGuidelines: [
692
771
  "Use ask_user only after exhausting repo conventions, configs, and docs; reserve it for decisions whose options have materially different tradeoffs.",
693
772
  ],
@@ -725,7 +804,12 @@ const askUserTool: ToolDefinition<typeof AskParamsSchema, AskUserDetails> = {
725
804
  const deadline =
726
805
  typeof params.timeoutSeconds === "number" && params.timeoutSeconds > 0 ? Date.now() + params.timeoutSeconds * 1000 : undefined;
727
806
 
728
- const outcome = await runAskDialog(ctx.ui, questions, { deadline, signal });
807
+ // ctx.ui.custom() is TUI-only (Mode Behavior): RPC hosts resolve it to
808
+ // undefined, so degrade to native select/input dialogs there.
809
+ const outcome =
810
+ ctx.mode === "tui"
811
+ ? await runAskDialog(ctx.ui, questions, { deadline, signal })
812
+ : await runAskDialogRpc(ctx.ui, questions, { deadline, signal });
729
813
 
730
814
  if (outcome.kind === "chat") {
731
815
  const questionTexts = questions.map((q) => q.question).join("\n");
@@ -889,7 +973,10 @@ async function runAskDemo(ctx: ExtensionCommandContext): Promise<void> {
889
973
  const run = async (label: string, params: AskParams): Promise<string> => {
890
974
  ctx.ui.notify(`ask-demo ${label} — follow the dialog instructions`, "info");
891
975
  try {
892
- const result = await askUserTool.execute(`demo-${label}`, params, undefined, undefined, ctx);
976
+ // pi 0.99 split tool and command contexts into sibling branches; the demo
977
+ // only exercises ExtensionContext members (ui/abort), so the cast is safe.
978
+ const toolCtx = ctx as unknown as ExtensionToolContext;
979
+ const result = await askUserTool.execute(`demo-${label}`, params, undefined, undefined, toolCtx);
893
980
  const text = result.content[0]?.type === "text" ? result.content[0].text : "";
894
981
  ctx.ui.notify(`ask-demo ${label} → ${text.split("\n").filter(Boolean).join(" | ").slice(0, 200)}`, "info");
895
982
  return text;
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.3",
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",
@@ -43,13 +43,13 @@
43
43
  "typecheck": "tsc"
44
44
  },
45
45
  "peerDependencies": {
46
- "@earendil-works/pi-coding-agent": ">=0.84.1",
47
- "@earendil-works/pi-tui": ">=0.84.1",
46
+ "@earendil-works/pi-coding-agent": ">=0.99.0",
47
+ "@earendil-works/pi-tui": ">=0.99.0",
48
48
  "typebox": ">=1.0.0"
49
49
  },
50
50
  "devDependencies": {
51
- "@earendil-works/pi-coding-agent": "0.84.1",
52
- "@earendil-works/pi-tui": "0.84.1",
51
+ "@earendil-works/pi-coding-agent": "0.99.2",
52
+ "@earendil-works/pi-tui": "0.99.2",
53
53
  "@types/node": "22.19.19",
54
54
  "typebox": "1.1.38",
55
55
  "typescript": "5.9.3",