@oh-my-pi/pi-coding-agent 13.15.3 → 13.16.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.
Files changed (50) hide show
  1. package/CHANGELOG.md +30 -16
  2. package/package.json +7 -7
  3. package/src/commit/agentic/tools/analyze-file.ts +1 -0
  4. package/src/config/model-registry.ts +215 -57
  5. package/src/config/settings-schema.ts +27 -0
  6. package/src/extensibility/custom-tools/types.ts +3 -0
  7. package/src/extensibility/extensions/runner.ts +7 -0
  8. package/src/extensibility/extensions/types.ts +10 -1
  9. package/src/extensibility/hooks/types.ts +1 -1
  10. package/src/internal-urls/docs-index.generated.ts +1 -1
  11. package/src/ipy/cancellation.ts +28 -0
  12. package/src/ipy/executor.ts +252 -77
  13. package/src/ipy/kernel.ts +181 -35
  14. package/src/ipy/modules.ts +39 -4
  15. package/src/modes/acp/acp-agent.ts +1 -0
  16. package/src/modes/components/hook-editor.ts +57 -8
  17. package/src/modes/components/model-selector.ts +48 -29
  18. package/src/modes/components/settings-defs.ts +10 -1
  19. package/src/modes/components/settings-selector.ts +92 -5
  20. package/src/modes/controllers/extension-ui-controller.ts +35 -4
  21. package/src/modes/controllers/input-controller.ts +4 -3
  22. package/src/modes/controllers/selector-controller.ts +2 -2
  23. package/src/modes/interactive-mode.ts +7 -2
  24. package/src/modes/print-mode.ts +1 -0
  25. package/src/modes/prompt-action-autocomplete.ts +5 -3
  26. package/src/modes/rpc/rpc-mode.ts +79 -30
  27. package/src/modes/rpc/rpc-types.ts +9 -1
  28. package/src/modes/theme/theme.ts +70 -0
  29. package/src/modes/types.ts +6 -1
  30. package/src/prompts/system/custom-system-prompt.md +5 -0
  31. package/src/prompts/system/system-prompt.md +6 -0
  32. package/src/prompts/tools/ask.md +1 -0
  33. package/src/prompts/tools/grep.md +1 -1
  34. package/src/prompts/tools/hashline.md +20 -5
  35. package/src/sdk.ts +26 -2
  36. package/src/session/agent-session.ts +18 -11
  37. package/src/system-prompt.ts +63 -2
  38. package/src/task/executor.ts +4 -0
  39. package/src/task/index.ts +2 -0
  40. package/src/tools/ask.ts +109 -61
  41. package/src/tools/ast-edit.ts +2 -16
  42. package/src/tools/ast-grep.ts +2 -17
  43. package/src/tools/browser.ts +35 -17
  44. package/src/tools/find.ts +1 -0
  45. package/src/tools/grep.ts +25 -34
  46. package/src/tools/index.ts +3 -0
  47. package/src/tools/path-utils.ts +7 -0
  48. package/src/tools/python.ts +3 -2
  49. package/src/tools/render-utils.ts +27 -0
  50. package/src/tui/tree-list.ts +51 -22
@@ -12,7 +12,8 @@ import {
12
12
  type TUI,
13
13
  visibleWidth,
14
14
  } from "@oh-my-pi/pi-tui";
15
- import { MODEL_ROLE_IDS, MODEL_ROLES, type ModelRegistry, type ModelRole } from "../../config/model-registry";
15
+ import type { ModelRegistry } from "../../config/model-registry";
16
+ import { getKnownRoleIds, getRoleInfo, MODEL_ROLE_IDS, MODEL_ROLES } from "../../config/model-registry";
16
17
  import { resolveModelRoleValue } from "../../config/model-resolver";
17
18
  import type { Settings } from "../../config/settings";
18
19
  import { type ThemeColor, theme } from "../../modes/theme/theme";
@@ -43,22 +44,13 @@ interface RoleAssignment {
43
44
  thinkingLevel: ThinkingLevel;
44
45
  }
45
46
 
46
- type RoleSelectCallback = (model: Model, role: ModelRole | null, thinkingLevel?: ThinkingLevel) => void;
47
+ type RoleSelectCallback = (model: Model, role: string | null, thinkingLevel?: ThinkingLevel) => void;
47
48
  type CancelCallback = () => void;
48
49
  interface MenuRoleAction {
49
50
  label: string;
50
- role: ModelRole;
51
+ role: string; // now accepts custom role strings
51
52
  }
52
53
 
53
- const MENU_ROLE_ACTIONS: MenuRoleAction[] = MODEL_ROLE_IDS.map(role => {
54
- const roleInfo = MODEL_ROLES[role];
55
- const roleLabel = roleInfo.tag ? `${roleInfo.tag} (${roleInfo.name})` : roleInfo.name;
56
- return {
57
- label: `Set as ${roleLabel}`,
58
- role,
59
- };
60
- });
61
-
62
54
  const ALL_TAB = "ALL";
63
55
 
64
56
  /**
@@ -77,7 +69,7 @@ export class ModelSelectorComponent extends Container {
77
69
  #allModels: ModelItem[] = [];
78
70
  #filteredModels: ModelItem[] = [];
79
71
  #selectedIndex: number = 0;
80
- #roles = {} as Record<ModelRole, RoleAssignment | undefined>;
72
+ #roles = {} as Record<string, RoleAssignment | undefined>;
81
73
  #settings = null as unknown as Settings;
82
74
  #modelRegistry = null as unknown as ModelRegistry;
83
75
  #onSelectCallback = (() => {}) as RoleSelectCallback;
@@ -87,6 +79,8 @@ export class ModelSelectorComponent extends Container {
87
79
  #scopedModels: ReadonlyArray<ScopedModelItem>;
88
80
  #temporaryOnly: boolean;
89
81
 
82
+ #menuRoleActions: MenuRoleAction[] = [];
83
+
90
84
  // Tab state
91
85
  #providers: string[] = [ALL_TAB];
92
86
  #activeTabIndex: number = 0;
@@ -95,7 +89,7 @@ export class ModelSelectorComponent extends Container {
95
89
  #isMenuOpen: boolean = false;
96
90
  #menuSelectedIndex: number = 0;
97
91
  #menuStep: "role" | "thinking" = "role";
98
- #menuSelectedRole: ModelRole | null = null;
92
+ #menuSelectedRole: string | null = null;
99
93
 
100
94
  constructor(
101
95
  tui: TUI,
@@ -103,7 +97,7 @@ export class ModelSelectorComponent extends Container {
103
97
  settings: Settings,
104
98
  modelRegistry: ModelRegistry,
105
99
  scopedModels: ReadonlyArray<ScopedModelItem>,
106
- onSelect: (model: Model, role: ModelRole | null, thinkingLevel?: ThinkingLevel) => void,
100
+ onSelect: (model: Model, role: string | null, thinkingLevel?: ThinkingLevel) => void,
107
101
  onCancel: () => void,
108
102
  options?: { temporaryOnly?: boolean; initialSearchInput?: string },
109
103
  ) {
@@ -118,6 +112,9 @@ export class ModelSelectorComponent extends Container {
118
112
  this.#temporaryOnly = options?.temporaryOnly ?? false;
119
113
  const initialSearchInput = options?.initialSearchInput;
120
114
 
115
+ // Initialize menu role actions (built-in + custom from settings)
116
+ this.#buildMenuRoleActions();
117
+
121
118
  // Load current role assignments from settings
122
119
  this.#loadRoleModels();
123
120
 
@@ -184,22 +181,35 @@ export class ModelSelectorComponent extends Container {
184
181
  });
185
182
  }
186
183
 
184
+ #buildMenuRoleActions(): void {
185
+ this.#menuRoleActions = getKnownRoleIds(this.#settings).map(role => {
186
+ const roleInfo = getRoleInfo(role, this.#settings);
187
+ const roleLabel = roleInfo.tag ? `${roleInfo.tag} (${roleInfo.name})` : roleInfo.name;
188
+ return {
189
+ label: `Set as ${roleLabel}`,
190
+ role,
191
+ };
192
+ });
193
+ }
194
+
187
195
  #loadRoleModels(): void {
188
196
  const allModels = this.#modelRegistry.getAll();
189
197
  const matchPreferences = { usageOrder: this.#settings.getStorage()?.getModelUsageOrder() };
190
- for (const role of MODEL_ROLE_IDS) {
198
+ for (const role of getKnownRoleIds(this.#settings)) {
191
199
  const roleValue = this.#settings.getModelRole(role);
192
200
  if (!roleValue) continue;
193
201
 
194
- const { model, thinkingLevel, explicitThinkingLevel } = resolveModelRoleValue(roleValue, allModels, {
202
+ const resolved = resolveModelRoleValue(roleValue, allModels, {
195
203
  settings: this.#settings,
196
204
  matchPreferences,
197
205
  });
198
- if (model) {
206
+ if (resolved.model) {
199
207
  this.#roles[role] = {
200
- model,
208
+ model: resolved.model,
201
209
  thinkingLevel:
202
- explicitThinkingLevel && thinkingLevel !== undefined ? thinkingLevel : ThinkingLevel.Inherit,
210
+ resolved.explicitThinkingLevel && resolved.thinkingLevel !== undefined
211
+ ? resolved.thinkingLevel
212
+ : ThinkingLevel.Inherit,
203
213
  };
204
214
  }
205
215
  }
@@ -470,7 +480,7 @@ export class ModelSelectorComponent extends Container {
470
480
  // Build role badges (inverted: color as background, black text)
471
481
  const roleBadgeTokens: string[] = [];
472
482
  for (const role of MODEL_ROLE_IDS) {
473
- const { tag, color } = MODEL_ROLES[role];
483
+ const { tag, color } = getRoleInfo(role, this.#settings);
474
484
  const assigned = this.#roles[role];
475
485
  if (!tag || !assigned || !modelsAreEqual(assigned.model, item.model)) continue;
476
486
 
@@ -478,6 +488,15 @@ export class ModelSelectorComponent extends Container {
478
488
  const thinkingLabel = getThinkingLevelMetadata(assigned.thinkingLevel).label;
479
489
  roleBadgeTokens.push(`${badge} ${theme.fg("dim", `(${thinkingLabel})`)}`);
480
490
  }
491
+ // Custom role badges
492
+ for (const [role, assigned] of Object.entries(this.#roles)) {
493
+ if (role in MODEL_ROLES || !assigned || !modelsAreEqual(assigned.model, item.model)) continue;
494
+ const roleInfo = getRoleInfo(role, this.#settings);
495
+ const badgeLabel = roleInfo.tag ?? roleInfo.name;
496
+ const badge = makeInvertedBadge(badgeLabel, roleInfo.color ?? "muted");
497
+ const thinkingLabel = getThinkingLevelMetadata(assigned.thinkingLevel).label;
498
+ roleBadgeTokens.push(`${badge} ${theme.fg("dim", `(${thinkingLabel})`)}`);
499
+ }
481
500
  const badgeText = roleBadgeTokens.length > 0 ? ` ${roleBadgeTokens.join(" ")}` : "";
482
501
 
483
502
  let line = "";
@@ -527,11 +546,11 @@ export class ModelSelectorComponent extends Container {
527
546
  return [ThinkingLevel.Inherit, ThinkingLevel.Off, ...getSupportedEfforts(model)];
528
547
  }
529
548
 
530
- #getCurrentRoleThinkingLevel(role: ModelRole): ThinkingLevel {
549
+ #getCurrentRoleThinkingLevel(role: string): ThinkingLevel {
531
550
  return this.#roles[role]?.thinkingLevel ?? ThinkingLevel.Inherit;
532
551
  }
533
552
 
534
- #getThinkingPreselectIndex(role: ModelRole, model: Model): number {
553
+ #getThinkingPreselectIndex(role: string, model: Model): number {
535
554
  const options = this.#getThinkingLevelsForModel(model);
536
555
  const currentLevel = this.#getCurrentRoleThinkingLevel(role);
537
556
  const foundIndex = options.indexOf(currentLevel);
@@ -569,12 +588,12 @@ export class ModelSelectorComponent extends Container {
569
588
  const label = getThinkingLevelMetadata(thinkingLevel).label;
570
589
  return `${prefix}${label}`;
571
590
  })
572
- : MENU_ROLE_ACTIONS.map((action, index) => {
591
+ : this.#menuRoleActions.map((action, index) => {
573
592
  const prefix = index === this.#menuSelectedIndex ? ` ${theme.nav.cursor} ` : " ";
574
593
  return `${prefix}${action.label}`;
575
594
  });
576
595
 
577
- const selectedRoleName = this.#menuSelectedRole ? MODEL_ROLES[this.#menuSelectedRole].name : "";
596
+ const selectedRoleName = this.#menuSelectedRole ? getRoleInfo(this.#menuSelectedRole, this.#settings).name : "";
578
597
  const headerText =
579
598
  showingThinking && this.#menuSelectedRole
580
599
  ? ` Thinking for: ${selectedRoleName} (${selectedModel.id})`
@@ -674,7 +693,7 @@ export class ModelSelectorComponent extends Container {
674
693
  const optionCount =
675
694
  this.#menuStep === "thinking" && this.#menuSelectedRole !== null
676
695
  ? this.#getThinkingLevelsForModel(selectedModel.model).length
677
- : MENU_ROLE_ACTIONS.length;
696
+ : this.#menuRoleActions.length;
678
697
  if (optionCount === 0) return;
679
698
 
680
699
  if (matchesKey(keyData, "up")) {
@@ -691,7 +710,7 @@ export class ModelSelectorComponent extends Container {
691
710
 
692
711
  if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
693
712
  if (this.#menuStep === "role") {
694
- const action = MENU_ROLE_ACTIONS[this.#menuSelectedIndex];
713
+ const action = this.#menuRoleActions[this.#menuSelectedIndex];
695
714
  if (!action) return;
696
715
  this.#menuSelectedRole = action.role;
697
716
  this.#menuStep = "thinking";
@@ -712,7 +731,7 @@ export class ModelSelectorComponent extends Container {
712
731
  if (getKeybindings().matches(keyData, "tui.select.cancel")) {
713
732
  if (this.#menuStep === "thinking" && this.#menuSelectedRole !== null) {
714
733
  this.#menuStep = "role";
715
- const roleIndex = MENU_ROLE_ACTIONS.findIndex(action => action.role === this.#menuSelectedRole);
734
+ const roleIndex = this.#menuRoleActions.findIndex(action => action.role === this.#menuSelectedRole);
716
735
  this.#menuSelectedRole = null;
717
736
  this.#menuSelectedIndex = roleIndex >= 0 ? roleIndex : 0;
718
737
  this.#updateMenu();
@@ -728,7 +747,7 @@ export class ModelSelectorComponent extends Container {
728
747
  if (thinkingLevel === ThinkingLevel.Inherit) return modelKey;
729
748
  return `${modelKey}:${thinkingLevel}`;
730
749
  }
731
- #handleSelect(model: Model, role: ModelRole | null, thinkingLevel?: ThinkingLevel): void {
750
+ #handleSelect(model: Model, role: string | null, thinkingLevel?: ThinkingLevel): void {
732
751
  // For temporary role, don't save to settings - just notify caller
733
752
  if (role === null) {
734
753
  this.#onSelectCallback(model, null);
@@ -51,7 +51,11 @@ export interface SubmenuSettingDef extends BaseSettingDef {
51
51
  onPreviewCancel?: (originalValue: string) => void;
52
52
  }
53
53
 
54
- export type SettingDef = BooleanSettingDef | EnumSettingDef | SubmenuSettingDef;
54
+ export interface TextInputSettingDef extends BaseSettingDef {
55
+ type: "text";
56
+ }
57
+
58
+ export type SettingDef = BooleanSettingDef | EnumSettingDef | SubmenuSettingDef | TextInputSettingDef;
55
59
 
56
60
  // ═══════════════════════════════════════════════════════════════════════════
57
61
  // Condition Functions
@@ -465,6 +469,11 @@ function pathToSettingDef(path: SettingPath): SettingDef | null {
465
469
  return createSubmenuSettingDef(base, []);
466
470
  }
467
471
 
472
+ // Plain string setting — free-text input field
473
+ if (schemaType === "string") {
474
+ return { ...base, type: "text" };
475
+ }
476
+
468
477
  return null;
469
478
  }
470
479
 
@@ -2,6 +2,7 @@ import type { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
2
2
  import type { Effort } from "@oh-my-pi/pi-ai";
3
3
  import {
4
4
  Container,
5
+ Input,
5
6
  matchesKey,
6
7
  type SelectItem,
7
8
  SelectList,
@@ -31,6 +32,52 @@ import { getPreset } from "./status-line/presets";
31
32
  /**
32
33
  * A submenu component for selecting from a list of options.
33
34
  */
35
+ /**
36
+ * Submenu component for free-text string settings.
37
+ * Mirrors the ConfigInputSubmenu pattern from plugin-settings.ts.
38
+ */
39
+ class TextInputSubmenu extends Container {
40
+ #input: Input;
41
+
42
+ constructor(
43
+ label: string,
44
+ description: string,
45
+ currentValue: string,
46
+ private readonly onSubmit: (value: string) => void,
47
+ private readonly onCancel: () => void,
48
+ ) {
49
+ super();
50
+
51
+ this.addChild(new Text(theme.bold(theme.fg("accent", label)), 0, 0));
52
+ if (description) {
53
+ this.addChild(new Spacer(1));
54
+ this.addChild(new Text(theme.fg("muted", description), 0, 0));
55
+ }
56
+ this.addChild(new Spacer(1));
57
+
58
+ this.#input = new Input();
59
+ if (currentValue) {
60
+ this.#input.setValue(currentValue);
61
+ // Move cursor to end of pre-filled value (ctrl+e = cursorLineEnd).
62
+ this.#input.handleInput("\x05");
63
+ }
64
+ this.#input.onSubmit = value => {
65
+ this.onSubmit(value); // empty string clears the setting
66
+ };
67
+ this.addChild(this.#input);
68
+ this.addChild(new Spacer(1));
69
+ this.addChild(new Text(theme.fg("dim", " Enter to save · Esc to cancel · Clear field to unset"), 0, 0));
70
+ }
71
+
72
+ handleInput(data: string): void {
73
+ if (data === "\x1b" || data === "\x1b\x1b") {
74
+ this.onCancel();
75
+ return;
76
+ }
77
+ this.#input.handleInput(data);
78
+ }
79
+ }
80
+
34
81
  class SelectSubmenu extends Container {
35
82
  #selectList: SelectList;
36
83
  #previewText: Text | null = null;
@@ -180,6 +227,7 @@ export class SettingsSelectorComponent extends Container {
180
227
  #statusPreviewContainer: Container | null = null;
181
228
  #statusPreviewText: Text | null = null;
182
229
  #currentTabId: SettingTab | "plugins" = "appearance";
230
+ #textInputActive = false;
183
231
 
184
232
  constructor(
185
233
  private readonly context: SettingsRuntimeContext,
@@ -277,6 +325,15 @@ export class SettingsSelectorComponent extends Container {
277
325
  currentValue: this.#getSubmenuCurrentValue(def.path, currentValue),
278
326
  submenu: (cv, done) => this.#createSubmenu(def, cv, done),
279
327
  };
328
+
329
+ case "text":
330
+ return {
331
+ id: def.path,
332
+ label: def.label,
333
+ description: def.description,
334
+ currentValue: (currentValue as string) ?? "",
335
+ submenu: (cv, done) => this.#createTextInput(def, cv, done),
336
+ };
280
337
  }
281
338
  }
282
339
 
@@ -389,6 +446,34 @@ export class SettingsSelectorComponent extends Container {
389
446
  );
390
447
  }
391
448
 
449
+ /**
450
+ * Create a text input submenu for a plain string setting.
451
+ */
452
+ #createTextInput(
453
+ def: SettingDef & { type: "text" },
454
+ currentValue: string,
455
+ done: (value?: string) => void,
456
+ ): Container {
457
+ this.#textInputActive = true;
458
+ const wrappedDone = (value?: string) => {
459
+ this.#textInputActive = false;
460
+ done(value);
461
+ };
462
+ return new TextInputSubmenu(
463
+ def.label,
464
+ def.description,
465
+ currentValue,
466
+ value => {
467
+ // Empty string clears the setting; undefined-typed string settings
468
+ // store "" which the browser.ts expandPath ignores (no-op fallback).
469
+ this.#setSettingValue(def.path, value);
470
+ this.callbacks.onChange(def.path, value);
471
+ wrappedDone(value);
472
+ },
473
+ () => wrappedDone(),
474
+ );
475
+ }
476
+
392
477
  /**
393
478
  * Set a setting value, handling type conversion.
394
479
  */
@@ -510,12 +595,14 @@ export class SettingsSelectorComponent extends Container {
510
595
  }
511
596
 
512
597
  handleInput(data: string): void {
513
- // Handle tab switching first (tab, shift+tab, or left/right arrows)
598
+ // Handle tab switching but NOT when a text input is active, since
599
+ // arrow keys must reach the cursor and Tab must not switch tabs.
514
600
  if (
515
- matchesKey(data, "tab") ||
516
- matchesKey(data, "shift+tab") ||
517
- matchesKey(data, "left") ||
518
- matchesKey(data, "right")
601
+ !this.#textInputActive &&
602
+ (matchesKey(data, "tab") ||
603
+ matchesKey(data, "shift+tab") ||
604
+ matchesKey(data, "left") ||
605
+ matchesKey(data, "right"))
519
606
  ) {
520
607
  this.#tabBar.handleInput(data);
521
608
  return;
@@ -50,7 +50,8 @@ export class ExtensionUiController {
50
50
  this.ctx.editor.handleInput(`\x1b[200~${text}\x1b[201~`);
51
51
  },
52
52
  getEditorText: () => this.ctx.editor.getText(),
53
- editor: (title, prefill) => this.showHookEditor(title, prefill),
53
+ editor: (title, prefill, dialogOptions, editorOptions) =>
54
+ this.showHookEditor(title, prefill, dialogOptions, editorOptions),
54
55
  get theme() {
55
56
  return theme;
56
57
  },
@@ -122,6 +123,7 @@ export class ExtensionUiController {
122
123
  };
123
124
  const contextActions: ExtensionContextActions = {
124
125
  getModel: () => this.ctx.session.model,
126
+ getSearchDb: () => this.ctx.session.searchDb,
125
127
  isIdle: () => !this.ctx.session.isStreaming,
126
128
  abort: () => this.ctx.session.abort(),
127
129
  hasPendingMessages: () => this.ctx.session.queuedMessageCount > 0,
@@ -383,6 +385,7 @@ export class ExtensionUiController {
383
385
  };
384
386
  const contextActions: ExtensionContextActions = {
385
387
  getModel: () => this.ctx.session.model,
388
+ getSearchDb: () => this.ctx.session.searchDb,
386
389
  isIdle: () => !this.ctx.session.isStreaming,
387
390
  abort: () => this.ctx.session.abort(),
388
391
  hasPendingMessages: () => this.ctx.session.queuedMessageCount > 0,
@@ -580,6 +583,7 @@ export class ExtensionUiController {
580
583
  sessionManager: this.ctx.session.sessionManager,
581
584
  modelRegistry: this.ctx.session.modelRegistry,
582
585
  model: this.ctx.session.model,
586
+ searchDb: this.ctx.session.searchDb,
583
587
  isIdle: () => !this.ctx.session.isStreaming,
584
588
  hasPendingMessages: () => this.ctx.session.queuedMessageCount > 0,
585
589
  hasQueuedMessages: () => this.ctx.session.queuedMessageCount > 0,
@@ -783,26 +787,53 @@ export class ExtensionUiController {
783
787
  /**
784
788
  * Show a multi-line editor for hooks (with Ctrl+G support).
785
789
  */
786
- showHookEditor(title: string, prefill?: string): Promise<string | undefined> {
790
+ showHookEditor(
791
+ title: string,
792
+ prefill?: string,
793
+ dialogOptions?: ExtensionUIDialogOptions,
794
+ editorOptions?: { promptStyle?: boolean },
795
+ ): Promise<string | undefined> {
787
796
  const { promise, resolve } = Promise.withResolvers<string | undefined>();
797
+ let settled = false;
798
+ const onAbort = () => {
799
+ this.hideHookEditor();
800
+ if (!settled) {
801
+ settled = true;
802
+ resolve(undefined);
803
+ }
804
+ };
805
+ const finish = (value: string | undefined) => {
806
+ if (settled) return;
807
+ settled = true;
808
+ dialogOptions?.signal?.removeEventListener("abort", onAbort);
809
+ resolve(value);
810
+ };
788
811
  this.ctx.hookEditor = new HookEditorComponent(
789
812
  this.ctx.ui,
790
813
  title,
791
814
  prefill,
792
815
  value => {
793
816
  this.hideHookEditor();
794
- resolve(value);
817
+ finish(value);
795
818
  },
796
819
  () => {
797
820
  this.hideHookEditor();
798
- resolve(undefined);
821
+ finish(undefined);
799
822
  },
823
+ editorOptions,
800
824
  );
801
825
 
802
826
  this.ctx.editorContainer.clear();
803
827
  this.ctx.editorContainer.addChild(this.ctx.hookEditor);
804
828
  this.ctx.ui.setFocus(this.ctx.hookEditor);
805
829
  this.ctx.ui.requestRender();
830
+ if (dialogOptions?.signal) {
831
+ if (dialogOptions.signal.aborted) {
832
+ onAbort();
833
+ } else {
834
+ dialogOptions.signal.addEventListener("abort", onAbort, { once: true });
835
+ }
836
+ }
806
837
  return promise;
807
838
  }
808
839
 
@@ -550,6 +550,7 @@ export class InputController {
550
550
  return createPromptActionAutocompleteProvider({
551
551
  commands,
552
552
  basePath,
553
+ searchDb: this.ctx.session.searchDb,
553
554
  keybindings: this.ctx.keybindings,
554
555
  copyCurrentLine: () => this.handleCopyCurrentLine(),
555
556
  copyPrompt: () => this.handleCopyPrompt(),
@@ -608,8 +609,8 @@ export class InputController {
608
609
 
609
610
  async cycleRoleModel(options?: { temporary?: boolean }): Promise<void> {
610
611
  try {
611
- const roleOrder = ["smol", "default", "slow"] as const;
612
- const result = await this.ctx.session.cycleRoleModels(roleOrder, options);
612
+ const cycleOrder = settings.get("cycleOrder");
613
+ const result = await this.ctx.session.cycleRoleModels(cycleOrder, options);
613
614
  if (!result) {
614
615
  this.ctx.showStatus("Only one role model available");
615
616
  return;
@@ -625,7 +626,7 @@ export class InputController {
625
626
  : "";
626
627
  const tempLabel = options?.temporary ? " (temporary)" : "";
627
628
  const cycleSeparator = theme.fg("dim", " > ");
628
- const cycleLabel = roleOrder
629
+ const cycleLabel = cycleOrder
629
630
  .map(role => {
630
631
  if (role === result.role) {
631
632
  return theme.bold(theme.fg("accent", role));
@@ -3,7 +3,7 @@ import { getOAuthProviders, type OAuthProvider } from "@oh-my-pi/pi-ai";
3
3
  import type { Component } from "@oh-my-pi/pi-tui";
4
4
  import { Input, Loader, Spacer, Text } from "@oh-my-pi/pi-tui";
5
5
  import { getAgentDbPath, getProjectDir } from "@oh-my-pi/pi-utils";
6
- import { MODEL_ROLES } from "../../config/model-registry";
6
+ import { getRoleInfo } from "../../config/model-registry";
7
7
  import { settings } from "../../config/settings";
8
8
  import { DebugSelectorComponent } from "../../debug";
9
9
  import { disableProvider, enableProvider } from "../../discovery";
@@ -406,7 +406,7 @@ export class SelectorController {
406
406
  // Don't call done() - selector stays open for role assignment
407
407
  } else {
408
408
  // Other roles (smol, slow): just update settings, not current model
409
- const roleInfo = MODEL_ROLES[role];
409
+ const roleInfo = getRoleInfo(role, settings);
410
410
  const roleLabel = roleInfo?.name ?? role;
411
411
  this.ctx.showStatus(`${roleLabel} model: ${model.id}`);
412
412
  // Don't call done() - selector stays open
@@ -1399,8 +1399,13 @@ export class InteractiveMode implements InteractiveModeContext {
1399
1399
  this.#extensionUiController.hideHookInput();
1400
1400
  }
1401
1401
 
1402
- showHookEditor(title: string, prefill?: string): Promise<string | undefined> {
1403
- return this.#extensionUiController.showHookEditor(title, prefill);
1402
+ showHookEditor(
1403
+ title: string,
1404
+ prefill?: string,
1405
+ dialogOptions?: ExtensionUIDialogOptions,
1406
+ editorOptions?: { promptStyle?: boolean },
1407
+ ): Promise<string | undefined> {
1408
+ return this.#extensionUiController.showHookEditor(title, prefill, dialogOptions, editorOptions);
1404
1409
  }
1405
1410
 
1406
1411
  hideHookEditor(): void {
@@ -76,6 +76,7 @@ export async function runPrintMode(session: AgentSession, options: PrintModeOpti
76
76
  // ExtensionContextActions
77
77
  {
78
78
  getModel: () => session.model,
79
+ getSearchDb: () => session.searchDb,
79
80
  isIdle: () => !session.isStreaming,
80
81
  abort: () => session.abort(),
81
82
  hasPendingMessages: () => session.queuedMessageCount > 0,
@@ -1,3 +1,4 @@
1
+ import type { SearchDb } from "@oh-my-pi/pi-natives";
1
2
  import {
2
3
  type AutocompleteItem,
3
4
  type AutocompleteProvider,
@@ -23,6 +24,7 @@ interface PromptActionAutocompleteItem extends AutocompleteItem {
23
24
  interface PromptActionAutocompleteOptions {
24
25
  commands: SlashCommand[];
25
26
  basePath: string;
27
+ searchDb?: SearchDb;
26
28
  keybindings: KeybindingsManager;
27
29
  copyCurrentLine: () => void;
28
30
  copyPrompt: () => void;
@@ -90,8 +92,8 @@ export class PromptActionAutocompleteProvider implements AutocompleteProvider {
90
92
  #baseProvider: CombinedAutocompleteProvider;
91
93
  #actions: PromptActionDefinition[];
92
94
 
93
- constructor(commands: SlashCommand[], basePath: string, actions: PromptActionDefinition[]) {
94
- this.#baseProvider = new CombinedAutocompleteProvider(commands, basePath);
95
+ constructor(commands: SlashCommand[], basePath: string, actions: PromptActionDefinition[], searchDb?: SearchDb) {
96
+ this.#baseProvider = new CombinedAutocompleteProvider(commands, basePath, searchDb);
95
97
  this.#actions = actions;
96
98
  }
97
99
 
@@ -227,5 +229,5 @@ export function createPromptActionAutocompleteProvider(
227
229
  },
228
230
  ];
229
231
 
230
- return new PromptActionAutocompleteProvider(options.commands, options.basePath, actions);
232
+ return new PromptActionAutocompleteProvider(options.commands, options.basePath, actions, options.searchDb);
231
233
  }