@pi-unipi/ask-user 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 (4) hide show
  1. package/README.md +4 -0
  2. package/ask-ui.ts +97 -90
  3. package/package.json +1 -1
  4. package/tools.ts +12 -10
package/README.md CHANGED
@@ -73,6 +73,10 @@ When the user chooses a `new_session` option:
73
73
 
74
74
  The tool result is rendered as `queued compact → ...` or `queued direct → ...`. If automatic delivery fails, ask-user falls back to editor prefill and warns you to press Enter.
75
75
 
76
+ ### History Expansion
77
+
78
+ Completed `ask_user` calls stay readable in chat history. The collapsed result shows the selected answer; press Ctrl+O on the tool result to expand the original question, context, and options that were presented.
79
+
76
80
  ### Keyboard Controls
77
81
 
78
82
  | Mode | Keys |
package/ask-ui.ts CHANGED
@@ -621,7 +621,7 @@ export function createRenderCall() {
621
621
  * Create a renderResult function for the ask_user tool.
622
622
  */
623
623
  export function createRenderResult() {
624
- return (result: AgentToolResult<unknown>, _options: unknown, theme: Theme, _context: unknown) => {
624
+ return (result: AgentToolResult<unknown>, options: unknown, theme: Theme, _context: unknown) => {
625
625
  const details = result.details as Record<string, unknown> | undefined;
626
626
  if (!details) {
627
627
  const content = result.content as unknown as Array<Record<string, unknown>> | undefined;
@@ -634,103 +634,110 @@ export function createRenderResult() {
634
634
  return new Text(theme.fg("warning", "No response"), 0, 0);
635
635
  }
636
636
 
637
- switch (response.kind) {
638
- case "cancelled":
639
- return new Text(theme.fg("warning", "Cancelled"), 0, 0);
640
- case "timed_out":
641
- return new Text(theme.fg("warning", "Timed out"), 0, 0);
642
- case "freeform":
643
- return new Text(
644
- theme.fg("success", "✓ ") +
637
+ const renderOptionSummary = (): string[] => {
638
+ const rawOptions = (details as { options?: unknown }).options;
639
+ if (!Array.isArray(rawOptions) || rawOptions.length === 0) return [];
640
+ return rawOptions.map((opt, index) => {
641
+ if (typeof opt === "string") {
642
+ return theme.fg("dim", ` ${index + 1}. `) + theme.fg("text", opt);
643
+ }
644
+ const record = opt as Record<string, unknown>;
645
+ const label = String(record.label ?? record.value ?? `Option ${index + 1}`);
646
+ const value = typeof record.value === "string" && record.value !== label
647
+ ? theme.fg("dim", ` (${record.value})`)
648
+ : "";
649
+ const action = typeof record.action === "string" && record.action !== "select"
650
+ ? theme.fg("dim", ` [${record.action}]`)
651
+ : "";
652
+ const description = typeof record.description === "string" && record.description.trim()
653
+ ? `\n${theme.fg("muted", ` ${record.description}`)}`
654
+ : "";
655
+ return theme.fg("dim", ` ${index + 1}. `) + theme.fg("text", label) + value + action + description;
656
+ });
657
+ };
658
+
659
+ const answerText = (() => {
660
+ switch (response.kind) {
661
+ case "cancelled":
662
+ return theme.fg("warning", "Cancelled");
663
+ case "timed_out":
664
+ return theme.fg("warning", "Timed out");
665
+ case "freeform":
666
+ return theme.fg("success", "✓ ") +
645
667
  theme.fg("muted", "(wrote) ") +
646
- theme.fg("accent", response.text || ""),
647
- 0,
648
- 0,
649
- );
650
- case "selection": {
651
- const selections = response.selections || [];
652
- const display =
653
- selections.length === 1
668
+ theme.fg("accent", response.text || "");
669
+ case "selection": {
670
+ const selections = response.selections || [];
671
+ const display = selections.length === 1 ? selections[0] : selections.join(", ");
672
+ return theme.fg("success", "✓ ") + theme.fg("accent", display);
673
+ }
674
+ case "combined": {
675
+ const selections = response.selections || [];
676
+ const selDisplay = selections.length === 1
654
677
  ? selections[0]
655
678
  : selections.join(", ");
656
- return new Text(
657
- theme.fg("success", "✓ ") + theme.fg("accent", display),
658
- 0,
659
- 0,
660
- );
661
- }
662
- case "combined": {
663
- const selections = response.selections || [];
664
- const selDisplay = selections.length === 1
665
- ? selections[0]
666
- : selections.join(", ");
667
- return new Text(
668
- theme.fg("success", "✓ ") +
679
+ return theme.fg("success", "✓ ") +
669
680
  theme.fg("accent", selDisplay) +
670
681
  theme.fg("muted", " and wrote ") +
671
- theme.fg("accent", response.text || ""),
672
- 0,
673
- 0,
674
- );
675
- }
676
- case "end_turn":
677
- return new Text(
678
- theme.fg("success", "✓ ") + theme.fg("muted", "end turn"),
679
- 0,
680
- 0,
681
- );
682
- case "new_session": {
683
- const prefill = response.prefill || "";
684
- if (response.launchStatus === "editor_prefill") {
685
- const label = response.launchedWith === "compact"
686
- ? "⚠ compact editor prefill → "
687
- : "⚠ direct editor prefill → ";
688
- return new Text(
689
- theme.fg("warning", label) + theme.fg("accent", prefill),
690
- 0,
691
- 0,
692
- );
693
- }
694
- if (response.launchStatus === "failed") {
695
- const label = response.launchedWith === "compact"
696
- ? "handoff failed (compact) → "
697
- : "handoff failed (direct) → ";
698
- return new Text(
699
- theme.fg("error", label) + theme.fg("accent", prefill),
700
- 0,
701
- 0,
702
- );
682
+ theme.fg("accent", response.text || "");
703
683
  }
704
- if (response.launchedWith === "compact") {
705
- return new Text(
706
- theme.fg("success", "✓ queued compact → ") +
707
- theme.fg("accent", prefill),
708
- 0,
709
- 0,
710
- );
711
- }
712
- if (response.launchedWith === "direct") {
713
- return new Text(
714
- theme.fg("success", "✓ queued direct → ") +
715
- theme.fg("accent", prefill),
716
- 0,
717
- 0,
718
- );
719
- }
720
- return new Text(
721
- theme.fg("success", "✓ ") +
684
+ case "end_turn":
685
+ return theme.fg("success", "✓ ") + theme.fg("muted", "end turn");
686
+ case "new_session": {
687
+ const prefill = response.prefill || "";
688
+ if (response.launchStatus === "editor_prefill") {
689
+ const label = response.launchedWith === "compact"
690
+ ? "⚠ compact editor prefill → "
691
+ : "⚠ direct editor prefill → ";
692
+ return theme.fg("warning", label) + theme.fg("accent", prefill);
693
+ }
694
+ if (response.launchStatus === "failed") {
695
+ const label = response.launchedWith === "compact"
696
+ ? "handoff failed (compact) → "
697
+ : "handoff failed (direct) → ";
698
+ return theme.fg("error", label) + theme.fg("accent", prefill);
699
+ }
700
+ if (response.launchedWith === "compact") {
701
+ return theme.fg("success", "✓ queued compact → ") + theme.fg("accent", prefill);
702
+ }
703
+ if (response.launchedWith === "direct") {
704
+ return theme.fg("success", "✓ queued direct → ") + theme.fg("accent", prefill);
705
+ }
706
+ return theme.fg("success", "✓ ") +
722
707
  theme.fg("muted", "new session") +
723
- (prefill ? theme.fg("accent", `: ${prefill}`) : ""),
724
- 0,
725
- 0,
726
- );
708
+ (prefill ? theme.fg("accent", `: ${prefill}`) : "");
709
+ }
710
+ default:
711
+ return theme.fg("text", JSON.stringify(response));
727
712
  }
728
- default:
729
- return new Text(
730
- theme.fg("text", JSON.stringify(response)),
731
- 0,
732
- 0,
733
- );
713
+ })();
714
+
715
+ const expanded = typeof options === "object" && options !== null && "expanded" in options
716
+ ? Boolean((options as { expanded?: boolean }).expanded)
717
+ : false;
718
+ if (!expanded) {
719
+ const question = typeof (details as { question?: unknown }).question === "string"
720
+ ? (details as { question: string }).question
721
+ : "";
722
+ const expandHint = question
723
+ ? theme.fg("dim", " · Ctrl+O question/options")
724
+ : "";
725
+ return new Text(answerText + expandHint, 0, 0);
726
+ }
727
+
728
+ const lines = [answerText];
729
+ const question = (details as { question?: unknown }).question;
730
+ const context = (details as { context?: unknown }).context;
731
+ if (typeof question === "string" && question.trim()) {
732
+ lines.push(theme.fg("muted", "Question: ") + theme.fg("text", question));
734
733
  }
734
+ if (typeof context === "string" && context.trim()) {
735
+ lines.push(theme.fg("muted", "Context: ") + theme.fg("text", context));
736
+ }
737
+ const optionLines = renderOptionSummary();
738
+ if (optionLines.length > 0) {
739
+ lines.push(theme.fg("muted", "Options:"), ...optionLines);
740
+ }
741
+ return new Text(lines.join("\n"), 0, 0);
735
742
  };
736
743
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pi-unipi/ask-user",
3
- "version": "2.0.1",
3
+ "version": "2.0.3",
4
4
  "description": "Structured user input tool for Pi coding agent — single-select, multi-select, freeform",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/tools.ts CHANGED
@@ -252,6 +252,13 @@ export function registerAskUserTools(pi: ExtensionAPI): void {
252
252
  action: (opt.action as NormalizedOption["action"]) ?? "select",
253
253
  prefill: opt.prefill,
254
254
  }));
255
+ const detailsBase = {
256
+ question,
257
+ context,
258
+ options: normalizedOptions,
259
+ allowMultiple,
260
+ allowFreeform,
261
+ };
255
262
 
256
263
  // Emit ASK_USER_PROMPT event if notifyOnAsk is enabled
257
264
  if (settings.notifyOnAsk) {
@@ -286,8 +293,7 @@ export function registerAskUserTools(pi: ExtensionAPI): void {
286
293
  },
287
294
  ],
288
295
  details: {
289
- question,
290
- options: normalizedOptions.map((o) => o.label),
296
+ ...detailsBase,
291
297
  response: {
292
298
  kind: "cancelled",
293
299
  } as AskUserResponse,
@@ -348,8 +354,7 @@ export function registerAskUserTools(pi: ExtensionAPI): void {
348
354
  return {
349
355
  content: [{ type: "text", text: "User cancelled the session launch" }],
350
356
  details: {
351
- question,
352
- options: normalizedOptions.map((o) => o.label),
357
+ ...detailsBase,
353
358
  response: {
354
359
  kind: "cancelled",
355
360
  comment: "Session launcher cancelled",
@@ -374,8 +379,7 @@ export function registerAskUserTools(pi: ExtensionAPI): void {
374
379
  return {
375
380
  content: [{ type: "text", text: "Session launch cancelled: no prefill message was provided." }],
376
381
  details: {
377
- question,
378
- options: normalizedOptions.map((o) => o.label),
382
+ ...detailsBase,
379
383
  response: {
380
384
  kind: "cancelled",
381
385
  comment: "Session launcher had no prefill to queue",
@@ -404,8 +408,7 @@ export function registerAskUserTools(pi: ExtensionAPI): void {
404
408
  return {
405
409
  content: [{ type: "text", text: contentText }],
406
410
  details: {
407
- question,
408
- options: normalizedOptions.map((o) => o.label),
411
+ ...detailsBase,
409
412
  response: {
410
413
  ...response,
411
414
  prefill: handoff.prefill ?? prefill,
@@ -421,8 +424,7 @@ export function registerAskUserTools(pi: ExtensionAPI): void {
421
424
  return {
422
425
  content: [{ type: "text", text: contentText }],
423
426
  details: {
424
- question,
425
- options: normalizedOptions.map((o) => o.label),
427
+ ...detailsBase,
426
428
  response,
427
429
  },
428
430
  };