@ilya-lesikov/pi-pi 0.9.0 → 0.10.0

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 (106) hide show
  1. package/3p/pi-ask-user/index.ts +71 -124
  2. package/3p/pi-ask-user/single-select-layout.ts +3 -14
  3. package/3p/pi-subagents/package.json +13 -7
  4. package/3p/pi-subagents/src/agent-manager.ts +243 -79
  5. package/3p/pi-subagents/src/agent-runner.ts +501 -365
  6. package/3p/pi-subagents/src/agent-types.ts +46 -57
  7. package/3p/pi-subagents/src/cross-extension-rpc.ts +52 -46
  8. package/3p/pi-subagents/src/custom-agents.ts +30 -6
  9. package/3p/pi-subagents/src/default-agents.ts +25 -43
  10. package/3p/pi-subagents/src/enabled-models.ts +180 -0
  11. package/3p/pi-subagents/src/index.ts +599 -839
  12. package/3p/pi-subagents/src/model-resolver.ts +26 -7
  13. package/3p/pi-subagents/src/output-file.ts +21 -2
  14. package/3p/pi-subagents/src/prompts.ts +17 -3
  15. package/3p/pi-subagents/src/schedule-store.ts +153 -0
  16. package/3p/pi-subagents/src/schedule.ts +365 -0
  17. package/3p/pi-subagents/src/settings.ts +261 -0
  18. package/3p/pi-subagents/src/skill-loader.ts +77 -54
  19. package/3p/pi-subagents/src/status-note.ts +25 -0
  20. package/3p/pi-subagents/src/types.ts +97 -6
  21. package/3p/pi-subagents/src/ui/agent-widget.ts +94 -21
  22. package/3p/pi-subagents/src/ui/conversation-viewer.ts +147 -35
  23. package/3p/pi-subagents/src/ui/schedule-menu.ts +104 -0
  24. package/3p/pi-subagents/src/ui/viewer-keys.ts +39 -0
  25. package/3p/pi-subagents/src/usage.ts +60 -0
  26. package/3p/pi-subagents/src/worktree.ts +49 -20
  27. package/extensions/orchestrator/agents/advisor.ts +13 -8
  28. package/extensions/orchestrator/agents/brainstorm-reviewer.ts +8 -3
  29. package/extensions/orchestrator/agents/code-reviewer.ts +8 -3
  30. package/extensions/orchestrator/agents/constraints.test.ts +26 -0
  31. package/extensions/orchestrator/agents/constraints.ts +12 -2
  32. package/extensions/orchestrator/agents/deep-debugger.ts +13 -8
  33. package/extensions/orchestrator/agents/explore.ts +12 -6
  34. package/extensions/orchestrator/agents/librarian.ts +13 -5
  35. package/extensions/orchestrator/agents/plan-reviewer.ts +8 -3
  36. package/extensions/orchestrator/agents/planner.ts +8 -3
  37. package/extensions/orchestrator/agents/pools.test.ts +56 -0
  38. package/extensions/orchestrator/agents/prompts.test.ts +52 -10
  39. package/extensions/orchestrator/agents/registry.test.ts +245 -0
  40. package/extensions/orchestrator/agents/registry.ts +145 -10
  41. package/extensions/orchestrator/agents/reviewer.ts +14 -8
  42. package/extensions/orchestrator/agents/task.ts +12 -6
  43. package/extensions/orchestrator/agents/tool-routing.ts +213 -85
  44. package/extensions/orchestrator/agents/wait-for-completion.test.ts +171 -0
  45. package/extensions/orchestrator/ast-search.test.ts +124 -0
  46. package/extensions/orchestrator/billing-spoof.test.ts +67 -0
  47. package/extensions/orchestrator/billing-spoof.ts +95 -0
  48. package/extensions/orchestrator/cbm.more.test.ts +358 -0
  49. package/extensions/orchestrator/command-handlers.ts +6 -0
  50. package/extensions/orchestrator/commands.test.ts +15 -2
  51. package/extensions/orchestrator/commands.ts +1 -1
  52. package/extensions/orchestrator/config.test.ts +89 -1
  53. package/extensions/orchestrator/config.ts +102 -19
  54. package/extensions/orchestrator/context.test.ts +46 -0
  55. package/extensions/orchestrator/context.ts +18 -5
  56. package/extensions/orchestrator/custom-footer.test.ts +24 -10
  57. package/extensions/orchestrator/custom-footer.ts +4 -2
  58. package/extensions/orchestrator/doctor.more.test.ts +315 -0
  59. package/extensions/orchestrator/doctor.ts +6 -2
  60. package/extensions/orchestrator/event-handlers.more.test.ts +561 -0
  61. package/extensions/orchestrator/event-handlers.test.ts +96 -9
  62. package/extensions/orchestrator/event-handlers.ts +344 -151
  63. package/extensions/orchestrator/exa.more.test.ts +118 -0
  64. package/extensions/orchestrator/flant-infra.more.test.ts +326 -0
  65. package/extensions/orchestrator/flant-infra.test.ts +127 -0
  66. package/extensions/orchestrator/flant-infra.ts +113 -39
  67. package/extensions/orchestrator/index.test.ts +76 -0
  68. package/extensions/orchestrator/index.ts +2 -0
  69. package/extensions/orchestrator/integration.test.ts +183 -65
  70. package/extensions/orchestrator/model-registry.test.ts +2 -1
  71. package/extensions/orchestrator/model-registry.ts +12 -2
  72. package/extensions/orchestrator/orchestrator.test.ts +67 -0
  73. package/extensions/orchestrator/orchestrator.ts +119 -27
  74. package/extensions/orchestrator/phases/brainstorm.test.ts +30 -1
  75. package/extensions/orchestrator/phases/brainstorm.ts +43 -6
  76. package/extensions/orchestrator/phases/implementation.ts +2 -0
  77. package/extensions/orchestrator/phases/machine.test.ts +17 -1
  78. package/extensions/orchestrator/phases/machine.ts +4 -1
  79. package/extensions/orchestrator/phases/planning.test.ts +47 -1
  80. package/extensions/orchestrator/phases/planning.ts +18 -3
  81. package/extensions/orchestrator/phases/review-task.ts +5 -0
  82. package/extensions/orchestrator/phases/review.test.ts +10 -0
  83. package/extensions/orchestrator/phases/review.ts +22 -7
  84. package/extensions/orchestrator/phases/spawn-blocking.test.ts +1 -0
  85. package/extensions/orchestrator/phases/verdict.ts +6 -5
  86. package/extensions/orchestrator/plannotator-open-failure.test.ts +67 -0
  87. package/extensions/orchestrator/plannotator.test.ts +38 -1
  88. package/extensions/orchestrator/plannotator.ts +50 -3
  89. package/extensions/orchestrator/pp-menu.leaves.test.ts +590 -0
  90. package/extensions/orchestrator/pp-menu.more.test.ts +524 -0
  91. package/extensions/orchestrator/pp-menu.test.ts +114 -7
  92. package/extensions/orchestrator/pp-menu.ts +579 -90
  93. package/extensions/orchestrator/pp-state-tools.test.ts +80 -0
  94. package/extensions/orchestrator/pp-state-tools.ts +65 -0
  95. package/extensions/orchestrator/rate-limit-fallback.more.test.ts +241 -0
  96. package/extensions/orchestrator/rate-limit-fallback.test.ts +20 -1
  97. package/extensions/orchestrator/rate-limit-fallback.ts +12 -0
  98. package/extensions/orchestrator/review-files.test.ts +26 -0
  99. package/extensions/orchestrator/review-files.ts +3 -0
  100. package/extensions/orchestrator/state.test.ts +73 -1
  101. package/extensions/orchestrator/state.ts +95 -23
  102. package/extensions/orchestrator/suppress-pierre-theme-spam.test.ts +56 -0
  103. package/extensions/orchestrator/suppress-pierre-theme-spam.ts +45 -0
  104. package/extensions/orchestrator/validate-artifacts.ts +1 -1
  105. package/package.json +6 -2
  106. package/scripts/test-3p.sh +52 -0
@@ -96,7 +96,6 @@ interface AskParams {
96
96
  allowComment?: boolean;
97
97
  displayMode?: AskDisplayMode;
98
98
  overlayToggleKey?: string | null;
99
- commentToggleKey?: string | null;
100
99
  timeout?: number;
101
100
  }
102
101
 
@@ -304,7 +303,7 @@ type ResolvedShortcut =
304
303
 
305
304
  interface ResolvedAskShortcuts {
306
305
  overlayToggle: ResolvedShortcut;
307
- commentToggle: ResolvedShortcut;
306
+ commentSelect: ResolvedShortcut;
308
307
  }
309
308
 
310
309
  const DISABLED_SHORTCUT: ResolvedShortcut = {
@@ -367,9 +366,13 @@ const SINGLE_SELECT_SPLIT_PANE_LEFT_MIN_WIDTH = 32;
367
366
  const SINGLE_SELECT_SPLIT_PANE_RIGHT_MIN_WIDTH = 28;
368
367
  const SINGLE_SELECT_SPLIT_PANE_SEPARATOR = " │ ";
369
368
  const FREEFORM_SENTINEL = "\u270f\ufe0f Type custom response...";
370
- const COMMENT_TOGGLE_LABEL = "Add extra context after selection";
369
+
371
370
  const DEFAULT_OVERLAY_TOGGLE_KEY = "alt+o";
372
- const DEFAULT_COMMENT_TOGGLE_KEY = "ctrl+g";
371
+ // Fixed "select this option AND add extra context" gesture. Plain Enter still
372
+ // selects with no comment. ctrl+e does not collide with select-mode navigation
373
+ // (which is arrows / ctrl+j/k / tab).
374
+ const COMMENT_SELECT_KEY = "ctrl+e";
375
+ const COMMENT_SELECT_LABEL = "add context";
373
376
 
374
377
  // Vim-style aliases for navigating option lists. ctrl+j/k are safe in the
375
378
  // searchable single-select because they don't collide with fuzzy-search input.
@@ -435,15 +438,14 @@ class MultiSelectList implements Component {
435
438
  private allowComment: boolean;
436
439
  private theme: Theme;
437
440
  private keybindings: KeybindingsManager;
438
- private commentToggle: ResolvedShortcut;
441
+ private commentSelect: ResolvedShortcut;
439
442
  private selectedIndex = 0;
440
443
  private checked = new Set<number>();
441
- private commentEnabled = false;
442
444
  private cachedWidth?: number;
443
445
  private cachedLines?: string[];
444
446
 
445
447
  public onCancel?: () => void;
446
- public onSubmit?: (result: string[]) => void;
448
+ public onSubmit?: (result: string[], wantsComment: boolean) => void;
447
449
  public onEnterFreeform?: () => void;
448
450
 
449
451
  constructor(
@@ -452,18 +454,14 @@ class MultiSelectList implements Component {
452
454
  allowComment: boolean,
453
455
  theme: Theme,
454
456
  keybindings: KeybindingsManager,
455
- commentToggle: ResolvedShortcut,
457
+ commentSelect: ResolvedShortcut,
456
458
  ) {
457
459
  this.options = options;
458
460
  this.allowFreeform = allowFreeform;
459
461
  this.allowComment = allowComment;
460
462
  this.theme = theme;
461
463
  this.keybindings = keybindings;
462
- this.commentToggle = commentToggle;
463
- }
464
-
465
- public isCommentEnabled(): boolean {
466
- return this.commentEnabled;
464
+ this.commentSelect = commentSelect;
467
465
  }
468
466
 
469
467
  invalidate(): void {
@@ -472,20 +470,11 @@ class MultiSelectList implements Component {
472
470
  }
473
471
 
474
472
  private getItemCount(): number {
475
- return this.options.length + (this.allowComment ? 1 : 0) + (this.allowFreeform ? 1 : 0);
476
- }
477
-
478
- private getCommentToggleIndex(): number | null {
479
- return this.allowComment ? this.options.length : null;
473
+ return this.options.length + (this.allowFreeform ? 1 : 0);
480
474
  }
481
475
 
482
476
  private getFreeformIndex(): number {
483
- return this.options.length + (this.allowComment ? 1 : 0);
484
- }
485
-
486
- private isCommentToggleRow(index: number): boolean {
487
- const toggleIndex = this.getCommentToggleIndex();
488
- return toggleIndex !== null && index === toggleIndex;
477
+ return this.options.length;
489
478
  }
490
479
 
491
480
  private isFreeformRow(index: number): boolean {
@@ -498,10 +487,13 @@ class MultiSelectList implements Component {
498
487
  else this.checked.add(index);
499
488
  }
500
489
 
501
- private toggleComment(): void {
502
- if (!this.allowComment) return;
503
- this.commentEnabled = !this.commentEnabled;
504
- this.invalidate();
490
+ private currentSelection(): string[] {
491
+ const selectedTitles = Array.from(this.checked)
492
+ .sort((a, b) => a - b)
493
+ .map((i) => this.options[i]?.title)
494
+ .filter((t): t is string => !!t);
495
+ const fallback = this.options[this.selectedIndex]?.title;
496
+ return selectedTitles.length > 0 ? selectedTitles : fallback ? [fallback] : [];
505
497
  }
506
498
 
507
499
  handleInput(data: string): void {
@@ -516,8 +508,14 @@ class MultiSelectList implements Component {
516
508
  return;
517
509
  }
518
510
 
519
- if (this.allowComment && !this.commentToggle.disabled && this.commentToggle.matches(data)) {
520
- this.toggleComment();
511
+ if (
512
+ this.allowComment &&
513
+ !this.commentSelect.disabled &&
514
+ this.commentSelect.matches(data) &&
515
+ !this.isFreeformRow(this.selectedIndex)
516
+ ) {
517
+ const result = this.currentSelection();
518
+ if (result.length > 0) this.onSubmit?.(result, true);
521
519
  return;
522
520
  }
523
521
 
@@ -545,10 +543,6 @@ class MultiSelectList implements Component {
545
543
  }
546
544
 
547
545
  if (matchesKey(data, Key.space)) {
548
- if (this.isCommentToggleRow(this.selectedIndex)) {
549
- this.toggleComment();
550
- return;
551
- }
552
546
  if (this.isFreeformRow(this.selectedIndex)) {
553
547
  this.onEnterFreeform?.();
554
548
  return;
@@ -559,24 +553,13 @@ class MultiSelectList implements Component {
559
553
  }
560
554
 
561
555
  if (this.keybindings.matches(data, "tui.select.confirm")) {
562
- if (this.isCommentToggleRow(this.selectedIndex)) {
563
- this.toggleComment();
564
- return;
565
- }
566
556
  if (this.isFreeformRow(this.selectedIndex)) {
567
557
  this.onEnterFreeform?.();
568
558
  return;
569
559
  }
570
560
 
571
- const selectedTitles = Array.from(this.checked)
572
- .sort((a, b) => a - b)
573
- .map((i) => this.options[i]?.title)
574
- .filter((t): t is string => !!t);
575
-
576
- const fallback = this.options[this.selectedIndex]?.title;
577
- const result = selectedTitles.length > 0 ? selectedTitles : fallback ? [fallback] : [];
578
-
579
- if (result.length > 0) this.onSubmit?.(result);
561
+ const result = this.currentSelection();
562
+ if (result.length > 0) this.onSubmit?.(result, false);
580
563
  else this.onCancel?.();
581
564
  }
582
565
  }
@@ -605,15 +588,6 @@ class MultiSelectList implements Component {
605
588
  const isSelected = i === this.selectedIndex;
606
589
  const prefix = isSelected ? theme.fg("accent", "→") : " ";
607
590
 
608
- if (this.isCommentToggleRow(i)) {
609
- const checkbox = this.commentEnabled ? theme.fg("success", "[✓]") : theme.fg("dim", "[ ]");
610
- const label = isSelected
611
- ? theme.fg("accent", theme.bold(COMMENT_TOGGLE_LABEL))
612
- : theme.fg("text", theme.bold(COMMENT_TOGGLE_LABEL));
613
- lines.push(truncateToWidth(`${prefix} ${checkbox} ${label}`, width, ""));
614
- continue;
615
- }
616
-
617
591
  if (this.isFreeformRow(i)) {
618
592
  const label = theme.fg("text", theme.bold("Type something."));
619
593
  const desc = theme.fg("muted", "Enter a custom response");
@@ -660,16 +634,15 @@ class WrappedSingleSelectList implements Component {
660
634
  private allowComment: boolean;
661
635
  private theme: Theme;
662
636
  private keybindings: KeybindingsManager;
663
- private commentToggle: ResolvedShortcut;
637
+ private commentSelect: ResolvedShortcut;
664
638
  private selectedIndex = 0;
665
639
  private searchQuery = "";
666
- private commentEnabled = false;
667
640
  private maxVisibleRows = 12;
668
641
  private cachedWidth?: number;
669
642
  private cachedLines?: string[];
670
643
 
671
644
  public onCancel?: () => void;
672
- public onSubmit?: (result: string) => void;
645
+ public onSubmit?: (result: string, wantsComment: boolean) => void;
673
646
  public onEnterFreeform?: () => void;
674
647
 
675
648
  constructor(
@@ -678,18 +651,14 @@ class WrappedSingleSelectList implements Component {
678
651
  allowComment: boolean,
679
652
  theme: Theme,
680
653
  keybindings: KeybindingsManager,
681
- commentToggle: ResolvedShortcut,
654
+ commentSelect: ResolvedShortcut,
682
655
  ) {
683
656
  this.options = options;
684
657
  this.allowFreeform = allowFreeform;
685
658
  this.allowComment = allowComment;
686
659
  this.theme = theme;
687
660
  this.keybindings = keybindings;
688
- this.commentToggle = commentToggle;
689
- }
690
-
691
- public isCommentEnabled(): boolean {
692
- return this.commentEnabled;
661
+ this.commentSelect = commentSelect;
693
662
  }
694
663
 
695
664
  setMaxVisibleRows(rows: number): void {
@@ -710,21 +679,11 @@ class WrappedSingleSelectList implements Component {
710
679
  }
711
680
 
712
681
  private getItemCount(filteredOptions: QuestionOption[]): number {
713
- return filteredOptions.length + (this.allowComment ? 1 : 0) + (this.allowFreeform ? 1 : 0);
714
- }
715
-
716
- private isCommentToggleRow(index: number, filteredOptions: QuestionOption[]): boolean {
717
- return this.allowComment && index === filteredOptions.length;
682
+ return filteredOptions.length + (this.allowFreeform ? 1 : 0);
718
683
  }
719
684
 
720
685
  private isFreeformRow(index: number, filteredOptions: QuestionOption[]): boolean {
721
- return this.allowFreeform && index === filteredOptions.length + (this.allowComment ? 1 : 0);
722
- }
723
-
724
- private toggleComment(): void {
725
- if (!this.allowComment) return;
726
- this.commentEnabled = !this.commentEnabled;
727
- this.invalidate();
686
+ return this.allowFreeform && index === filteredOptions.length;
728
687
  }
729
688
 
730
689
  private setSearchQuery(query: string): void {
@@ -822,8 +781,6 @@ class WrappedSingleSelectList implements Component {
822
781
  selectedIndex: this.selectedIndex,
823
782
  width,
824
783
  allowFreeform: this.allowFreeform,
825
- allowComment: this.allowComment,
826
- commentEnabled: this.commentEnabled,
827
784
  maxRows,
828
785
  hideDescriptions,
829
786
  });
@@ -840,11 +797,7 @@ class WrappedSingleSelectList implements Component {
840
797
 
841
798
  let md = "";
842
799
 
843
- if (this.isCommentToggleRow(this.selectedIndex, filteredOptions)) {
844
- md += "## Additional context\n\n";
845
- md += `Currently: **${this.commentEnabled ? "Enabled" : "Disabled"}**\n\n`;
846
- md += "Turn this on when the selected option needs extra explanation before the tool submits.\n";
847
- } else if (this.isFreeformRow(this.selectedIndex, filteredOptions)) {
800
+ if (this.isFreeformRow(this.selectedIndex, filteredOptions)) {
848
801
  md += "## Custom response\n\n";
849
802
  md += "Open the editor to write **any** answer.\n\n";
850
803
  md += "*Use this when none of the listed options fit.*\n";
@@ -903,14 +856,21 @@ class WrappedSingleSelectList implements Component {
903
856
  return;
904
857
  }
905
858
 
906
- if (this.allowComment && !this.commentToggle.disabled && this.commentToggle.matches(data)) {
907
- this.toggleComment();
908
- return;
909
- }
910
-
911
859
  const filteredOptions = this.getFilteredOptions();
912
860
  const count = this.getItemCount(filteredOptions);
913
861
 
862
+ if (
863
+ this.allowComment &&
864
+ !this.commentSelect.disabled &&
865
+ this.commentSelect.matches(data) &&
866
+ count > 0 &&
867
+ !this.isFreeformRow(this.selectedIndex, filteredOptions)
868
+ ) {
869
+ const result = filteredOptions[this.selectedIndex]?.title;
870
+ if (result) this.onSubmit?.(result, true);
871
+ return;
872
+ }
873
+
914
874
  if (matchesSelectUp(data, this.keybindings) && count > 0) {
915
875
  this.selectedIndex = this.selectedIndex === 0 ? count - 1 : this.selectedIndex - 1;
916
876
  this.invalidate();
@@ -933,23 +893,14 @@ class WrappedSingleSelectList implements Component {
933
893
  }
934
894
  }
935
895
 
936
- if (matchesKey(data, Key.space) && count > 0 && this.isCommentToggleRow(this.selectedIndex, filteredOptions)) {
937
- this.toggleComment();
938
- return;
939
- }
940
-
941
896
  if (this.keybindings.matches(data, "tui.select.confirm") && count > 0) {
942
- if (this.isCommentToggleRow(this.selectedIndex, filteredOptions)) {
943
- this.toggleComment();
944
- return;
945
- }
946
897
  if (this.isFreeformRow(this.selectedIndex, filteredOptions)) {
947
898
  this.onEnterFreeform?.();
948
899
  return;
949
900
  }
950
901
 
951
902
  const result = filteredOptions[this.selectedIndex]?.title;
952
- if (result) this.onSubmit?.(result);
903
+ if (result) this.onSubmit?.(result, false);
953
904
  else this.onCancel?.();
954
905
  return;
955
906
  }
@@ -1169,8 +1120,8 @@ class AskComponent extends Container {
1169
1120
  const overlayHint = this.displayMode === "overlay" && !this.shortcuts.overlayToggle.disabled
1170
1121
  ? literalHint(theme, this.shortcuts.overlayToggle.spec, "hide")
1171
1122
  : null;
1172
- const commentHint = this.allowComment && !this.shortcuts.commentToggle.disabled
1173
- ? literalHint(theme, this.shortcuts.commentToggle.spec, "add text")
1123
+ const commentHint = this.allowComment && !this.shortcuts.commentSelect.disabled
1124
+ ? literalHint(theme, this.shortcuts.commentSelect.spec, COMMENT_SELECT_LABEL)
1174
1125
  : null;
1175
1126
  if (this.mode === "freeform" || this.mode === "comment") {
1176
1127
  const alternateCancelKeys = this.keybindings
@@ -1232,9 +1183,9 @@ class AskComponent extends Container {
1232
1183
  this.allowComment,
1233
1184
  this.theme,
1234
1185
  this.keybindings,
1235
- this.shortcuts.commentToggle,
1186
+ this.shortcuts.commentSelect,
1236
1187
  );
1237
- list.onSubmit = (result) => this.handleSelectionSubmit([result], list.isCommentEnabled());
1188
+ list.onSubmit = (result, wantsComment) => this.handleSelectionSubmit([result], wantsComment);
1238
1189
  list.onCancel = () => this.onDone(makeCancel("user"));
1239
1190
  list.onEnterFreeform = () => this.showFreeformMode();
1240
1191
 
@@ -1251,10 +1202,10 @@ class AskComponent extends Container {
1251
1202
  this.allowComment,
1252
1203
  this.theme,
1253
1204
  this.keybindings,
1254
- this.shortcuts.commentToggle,
1205
+ this.shortcuts.commentSelect,
1255
1206
  );
1256
1207
  list.onCancel = () => this.onDone(makeCancel("user"));
1257
- list.onSubmit = (result) => this.handleSelectionSubmit(result, list.isCommentEnabled());
1208
+ list.onSubmit = (result, wantsComment) => this.handleSelectionSubmit(result, wantsComment);
1258
1209
  list.onEnterFreeform = () => this.showFreeformMode();
1259
1210
 
1260
1211
  this.multiSelectList = list;
@@ -1487,7 +1438,6 @@ export async function askUser(
1487
1438
  overlay?: boolean;
1488
1439
  displayMode?: AskDisplayMode;
1489
1440
  overlayToggleKey?: string | null;
1490
- commentToggleKey?: string | null;
1491
1441
  },
1492
1442
  ): Promise<AskResponse | AskCancel | null> {
1493
1443
  const {
@@ -1502,7 +1452,6 @@ export async function askUser(
1502
1452
  overlay,
1503
1453
  displayMode,
1504
1454
  overlayToggleKey,
1505
- commentToggleKey,
1506
1455
  } = opts;
1507
1456
 
1508
1457
  const requestedMode = displayMode ?? (overlay === undefined ? undefined : overlay ? "overlay" : "inline");
@@ -1517,11 +1466,7 @@ export async function askUser(
1517
1466
  process.env.PI_ASK_USER_OVERLAY_TOGGLE_KEY,
1518
1467
  DEFAULT_OVERLAY_TOGGLE_KEY,
1519
1468
  ),
1520
- commentToggle: resolveShortcut(
1521
- commentToggleKey,
1522
- process.env.PI_ASK_USER_COMMENT_TOGGLE_KEY,
1523
- DEFAULT_COMMENT_TOGGLE_KEY,
1524
- ),
1469
+ commentSelect: buildShortcut(COMMENT_SELECT_KEY),
1525
1470
  };
1526
1471
 
1527
1472
  const options = normalizeOptions(rawOptions);
@@ -1603,15 +1548,15 @@ export default function(pi: ExtensionAPI) {
1603
1548
  name: "ask_user",
1604
1549
  label: "Ask User",
1605
1550
  description:
1606
- "Ask the user a question with optional multiple-choice answers. Use this to gather information interactively. Ask exactly one focused question per call. Keep the `question` SHORT — one scannable line; put any reasoning, findings, or detail in your message BEFORE the call, not in the question itself.",
1551
+ "Ask the user a question with optional multiple-choice answers. Use this to gather information interactively. Ask exactly one focused question per call — NEVER bundle multiple decisions into one prompt; spawn several asks in sequence instead. Keep the `question` SHORT — one scannable line; put the substantive context, reasoning, and findings in your message (or the `context` field) BEFORE the call, so the terse question and options are interpretable. Do NOT add an option like 'I'll answer in a comment' — the built-in freeform 'Type something' path already covers custom answers.",
1607
1552
  promptSnippet:
1608
1553
  "Ask the user one short, focused question with optional multiple-choice answers to gather information interactively",
1609
1554
  promptGuidelines: [
1610
1555
  "Keep the `question` field SHORT — a single scannable line (ideally under ~100 chars). The dialogue de-emphasizes it; the user reads your detail from the message rendered above the dialogue.",
1611
- "Put context, reasoning, and findings in your assistant message BEFORE calling ask_user do NOT pack them into the `question` field.",
1556
+ "ALWAYS present substantive context BEFORE the ask (in your assistant message and/or the `context` field): the question and options are intentionally terse, so the surrounding context must make them interpretable.",
1612
1557
  "Use ask_user when the user's intent is ambiguous, when a decision requires explicit user input, or when multiple valid options exist.",
1613
- "Ask exactly one focused question per ask_user call.",
1614
- "Do not combine multiple numbered, multipart, or unrelated questions into one ask_user prompt.",
1558
+ "Ask exactly one focused question per ask_user call. NEVER combine multiple numbered, multipart, or unrelated decisions into one prompt — spawn multiple focused asks in sequence instead.",
1559
+ "NEVER add an option that just says the user will answer in a comment / free text (e.g. 'I'll type my own answer') — the built-in 'Type something' freeform path already covers custom answers.",
1615
1560
  ],
1616
1561
  // Block other tool calls in the same assistant turn until the user answers,
1617
1562
  // so the model can't batch ask_user with bash/edit/write and let those run
@@ -1658,12 +1603,6 @@ export default function(pi: ExtensionAPI) {
1658
1603
  "Shortcut for hiding/showing the overlay popup (overlay mode only), e.g. 'alt+o' or 'ctrl+shift+h'. Pass 'off' to disable. Default: PI_ASK_USER_OVERLAY_TOGGLE_KEY env var if set, otherwise 'alt+o'.",
1659
1604
  }),
1660
1605
  ),
1661
- commentToggleKey: Type.Optional(
1662
- Type.String({
1663
- description:
1664
- "Shortcut for toggling the optional comment/extra-context row when allowComment is true, e.g. 'ctrl+g'. Pass 'off' to disable. Default: PI_ASK_USER_COMMENT_TOGGLE_KEY env var if set, otherwise 'ctrl+g'.",
1665
- }),
1666
- ),
1667
1606
  timeout: Type.Optional(
1668
1607
  Type.Number({ description: "Auto-dismiss after N milliseconds. Returns null (cancelled) when expired." }),
1669
1608
  ),
@@ -1686,7 +1625,6 @@ export default function(pi: ExtensionAPI) {
1686
1625
  allowComment = true,
1687
1626
  displayMode,
1688
1627
  overlayToggleKey,
1689
- commentToggleKey,
1690
1628
  timeout,
1691
1629
  } = params as AskParams;
1692
1630
 
@@ -1712,6 +1650,11 @@ export default function(pi: ExtensionAPI) {
1712
1650
  });
1713
1651
  }
1714
1652
 
1653
+ pi.events.emit("ask:opened", {
1654
+ question,
1655
+ context: normalizedContext,
1656
+ options,
1657
+ });
1715
1658
  let result: AskResponse | AskCancel | null;
1716
1659
  try {
1717
1660
  result = await askUser(ctx, {
@@ -1723,13 +1666,17 @@ export default function(pi: ExtensionAPI) {
1723
1666
  allowComment,
1724
1667
  displayMode,
1725
1668
  overlayToggleKey,
1726
- commentToggleKey,
1727
1669
  timeout,
1728
1670
  signal,
1729
1671
  });
1730
1672
  } catch (error) {
1731
1673
  const message =
1732
1674
  error instanceof Error ? `${error.message}\n${error.stack ?? ""}` : String(error);
1675
+ pi.events.emit("ask:cancelled", {
1676
+ question,
1677
+ context: normalizedContext,
1678
+ options,
1679
+ });
1733
1680
  return {
1734
1681
  content: [{ type: "text" as const, text: `Ask tool failed: ${message}` }],
1735
1682
  isError: true,
@@ -13,8 +13,6 @@ export interface RenderSingleSelectRowsParams {
13
13
  selectedIndex: number;
14
14
  width: number;
15
15
  allowFreeform: boolean;
16
- allowComment?: boolean;
17
- commentEnabled?: boolean;
18
16
  maxRows?: number;
19
17
  hideDescriptions?: boolean;
20
18
  }
@@ -74,25 +72,18 @@ interface ItemBlock {
74
72
 
75
73
  type ListItem =
76
74
  | { type: "option"; option: QuestionOption }
77
- | { type: "comment-toggle"; option: QuestionOption }
78
75
  | { type: "freeform"; option: QuestionOption };
79
76
 
80
77
  function buildItemBlocks(
81
78
  options: QuestionOption[],
82
79
  width: number,
83
80
  allowFreeform: boolean,
84
- allowComment: boolean,
85
- commentEnabled: boolean,
86
81
  selectedIndex: number,
87
82
  hideDescriptions = false,
88
83
  ): ItemBlock[] {
89
84
  const normalizedWidth = Math.max(12, width);
90
85
  const freeformLabel = "Type something. — Enter a custom response";
91
- const commentToggleLabel = `${commentEnabled ? "[✓]" : "[ ]"} Add extra context after selection`;
92
86
  const allItems: ListItem[] = options.map((option) => ({ type: "option", option }));
93
- if (allowComment) {
94
- allItems.push({ type: "comment-toggle", option: { title: commentToggleLabel } });
95
- }
96
87
  if (allowFreeform) {
97
88
  allItems.push({ type: "freeform", option: { title: freeformLabel } });
98
89
  }
@@ -101,7 +92,7 @@ function buildItemBlocks(
101
92
  const pointer = itemIndex === selectedIndex ? "→" : " ";
102
93
  const lines: string[] = [];
103
94
 
104
- if (item.type === "comment-toggle" || item.type === "freeform") {
95
+ if (item.type === "freeform") {
105
96
  const prefix = `${pointer} `;
106
97
  const wrapped = wrapText(item.option.title, Math.max(8, normalizedWidth - prefix.length));
107
98
  wrapped.forEach((line, lineIndex) => {
@@ -146,13 +137,11 @@ export function renderSingleSelectRows({
146
137
  selectedIndex,
147
138
  width,
148
139
  allowFreeform,
149
- allowComment = false,
150
- commentEnabled = false,
151
140
  maxRows,
152
141
  hideDescriptions,
153
142
  }: RenderSingleSelectRowsParams): AnnotatedRow[] {
154
- const itemCount = options.length + (allowComment ? 1 : 0) + (allowFreeform ? 1 : 0);
155
- const blocks = buildItemBlocks(options, width, allowFreeform, allowComment, commentEnabled, selectedIndex, hideDescriptions);
143
+ const itemCount = options.length + (allowFreeform ? 1 : 0);
144
+ const blocks = buildItemBlocks(options, width, allowFreeform, selectedIndex, hideDescriptions);
156
145
  const allRows = flatten(blocks, selectedIndex);
157
146
 
158
147
  if (!Number.isFinite(maxRows) || !maxRows || maxRows <= 0 || allRows.length <= maxRows) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tintinweb/pi-subagents",
3
- "version": "0.5.2",
3
+ "version": "0.13.0",
4
4
  "description": "A pi extension extension that brings smart Claude Code-style autonomous sub-agents to pi.",
5
5
  "author": "tintinweb",
6
6
  "license": "MIT",
@@ -13,31 +13,37 @@
13
13
  "url": "https://github.com/tintinweb/pi-subagents/issues"
14
14
  },
15
15
  "keywords": [
16
+ "pi-package",
16
17
  "pi",
17
18
  "pi-extension",
18
19
  "subagent",
19
20
  "agent",
20
21
  "autonomous"
21
22
  ],
23
+ "peerDependencies": {
24
+ "@earendil-works/pi-ai": ">=0.74.0",
25
+ "@earendil-works/pi-coding-agent": ">=0.74.0",
26
+ "@earendil-works/pi-tui": ">=0.74.0"
27
+ },
22
28
  "dependencies": {
23
- "@earendil-works/pi-ai": "^0.75.5",
24
- "@earendil-works/pi-coding-agent": "^0.75.5",
25
- "@earendil-works/pi-tui": "^0.75.5",
26
- "@sinclair/typebox": "latest"
29
+ "@sinclair/typebox": "^0.34.49",
30
+ "croner": "^10.0.1",
31
+ "nanoid": "^5.0.0"
27
32
  },
28
33
  "scripts": {
29
34
  "build": "tsc",
30
35
  "prepublishOnly": "npm run lint && npm run typecheck && npm run test && npm run build",
31
36
  "test": "vitest run",
32
37
  "test:watch": "vitest",
38
+ "test:e2e": "vitest run e2e --reporter=verbose",
33
39
  "typecheck": "tsc --noEmit",
34
40
  "lint": "biome check src/ test/",
35
41
  "lint:fix": "biome check --fix src/ test/"
36
42
  },
37
43
  "devDependencies": {
38
- "@biomejs/biome": "^2.3.5",
44
+ "@biomejs/biome": "^2.4.14",
39
45
  "@types/node": "^25.5.0",
40
- "typescript": "^5.0.0",
46
+ "typescript": "^6.0.0",
41
47
  "vitest": "^4.0.18"
42
48
  },
43
49
  "pi": {