@oh-my-pi/pi-coding-agent 15.7.1 → 15.7.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 (177) hide show
  1. package/CHANGELOG.md +95 -6
  2. package/dist/types/auto-thinking/classifier.d.ts +35 -0
  3. package/dist/types/cli/args.d.ts +1 -1
  4. package/dist/types/cli/extension-flags.d.ts +36 -0
  5. package/dist/types/config/config-file.d.ts +4 -0
  6. package/dist/types/config/file-lock.d.ts +23 -0
  7. package/dist/types/config/keybindings.d.ts +2 -1
  8. package/dist/types/config/model-registry.d.ts +6 -0
  9. package/dist/types/config/settings-schema.d.ts +112 -69
  10. package/dist/types/edit/hashline/diff.d.ts +9 -3
  11. package/dist/types/eval/__tests__/agent-bridge.test.d.ts +1 -0
  12. package/dist/types/eval/__tests__/budget-bridge.test.d.ts +1 -0
  13. package/dist/types/eval/__tests__/idle-timeout.test.d.ts +1 -0
  14. package/dist/types/eval/agent-bridge.d.ts +25 -0
  15. package/dist/types/eval/backend.d.ts +17 -2
  16. package/dist/types/eval/budget-bridge.d.ts +29 -0
  17. package/dist/types/eval/idle-timeout.d.ts +28 -0
  18. package/dist/types/eval/js/executor.d.ts +8 -0
  19. package/dist/types/eval/js/tool-bridge.d.ts +2 -1
  20. package/dist/types/eval/py/executor.d.ts +13 -0
  21. package/dist/types/exec/bash-executor.d.ts +1 -0
  22. package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
  23. package/dist/types/extensibility/extensions/runner.d.ts +7 -0
  24. package/dist/types/extensibility/plugins/git-url.d.ts +11 -1
  25. package/dist/types/extensibility/plugins/manager.d.ts +12 -1
  26. package/dist/types/extensibility/shared-events.d.ts +2 -2
  27. package/dist/types/memory-backend/index.d.ts +1 -1
  28. package/dist/types/memory-backend/resolve.d.ts +1 -1
  29. package/dist/types/memory-backend/types.d.ts +3 -3
  30. package/dist/types/mnemopi/backend.d.ts +4 -0
  31. package/dist/types/mnemopi/config.d.ts +29 -0
  32. package/dist/types/mnemopi/state.d.ts +72 -0
  33. package/dist/types/modes/components/custom-editor.d.ts +2 -2
  34. package/dist/types/modes/components/model-selector.d.ts +3 -2
  35. package/dist/types/modes/components/omfg-panel.d.ts +19 -0
  36. package/dist/types/modes/controllers/command-controller.d.ts +7 -0
  37. package/dist/types/modes/controllers/input-controller.d.ts +1 -3
  38. package/dist/types/modes/controllers/omfg-controller.d.ts +10 -0
  39. package/dist/types/modes/controllers/omfg-rule.d.ts +26 -0
  40. package/dist/types/modes/gradient-highlight.d.ts +5 -1
  41. package/dist/types/modes/interactive-mode.d.ts +7 -3
  42. package/dist/types/modes/magic-keywords.d.ts +14 -0
  43. package/dist/types/modes/markdown-prose.d.ts +27 -0
  44. package/dist/types/modes/orchestrate.d.ts +7 -2
  45. package/dist/types/modes/shared.d.ts +1 -1
  46. package/dist/types/modes/theme/theme.d.ts +2 -1
  47. package/dist/types/modes/turn-budget.d.ts +18 -0
  48. package/dist/types/modes/types.d.ts +7 -3
  49. package/dist/types/modes/ultrathink.d.ts +7 -2
  50. package/dist/types/modes/workflow.d.ts +15 -0
  51. package/dist/types/sdk.d.ts +15 -4
  52. package/dist/types/session/agent-session.d.ts +59 -23
  53. package/dist/types/session/session-manager.d.ts +18 -0
  54. package/dist/types/session/session-storage.d.ts +6 -0
  55. package/dist/types/session/shake-types.d.ts +24 -0
  56. package/dist/types/task/executor.d.ts +2 -2
  57. package/dist/types/thinking.d.ts +39 -1
  58. package/dist/types/tiny/device.d.ts +3 -3
  59. package/dist/types/tiny/models.d.ts +34 -1
  60. package/dist/types/tiny/title-protocol.d.ts +4 -0
  61. package/dist/types/tools/index.d.ts +19 -3
  62. package/dist/types/tools/memory-edit.d.ts +1 -1
  63. package/examples/sdk/09-api-keys-and-oauth.ts +2 -2
  64. package/package.json +10 -10
  65. package/src/auto-thinking/classifier.ts +180 -0
  66. package/src/autoresearch/tools/run-experiment.ts +45 -113
  67. package/src/cli/args.ts +39 -16
  68. package/src/cli/extension-flags.ts +48 -0
  69. package/src/cli/plugin-cli.ts +11 -2
  70. package/src/config/config-file.ts +98 -13
  71. package/src/config/file-lock.ts +60 -17
  72. package/src/config/keybindings.ts +78 -27
  73. package/src/config/model-registry.ts +7 -1
  74. package/src/config/settings-schema.ts +118 -71
  75. package/src/config/settings.ts +12 -0
  76. package/src/edit/hashline/diff.ts +87 -22
  77. package/src/edit/renderer.ts +16 -12
  78. package/src/edit/streaming.ts +17 -6
  79. package/src/eval/__tests__/agent-bridge.test.ts +433 -0
  80. package/src/eval/__tests__/budget-bridge.test.ts +69 -0
  81. package/src/eval/__tests__/idle-timeout.test.ts +66 -0
  82. package/src/eval/__tests__/shared-executors.test.ts +53 -0
  83. package/src/eval/agent-bridge.ts +295 -0
  84. package/src/eval/backend.ts +17 -2
  85. package/src/eval/budget-bridge.ts +48 -0
  86. package/src/eval/idle-timeout.ts +80 -0
  87. package/src/eval/js/executor.ts +35 -7
  88. package/src/eval/js/index.ts +2 -1
  89. package/src/eval/js/shared/local-module-loader.ts +75 -10
  90. package/src/eval/js/shared/prelude.txt +85 -1
  91. package/src/eval/js/tool-bridge.ts +9 -0
  92. package/src/eval/py/executor.ts +41 -14
  93. package/src/eval/py/index.ts +2 -1
  94. package/src/eval/py/prelude.py +132 -1
  95. package/src/exec/bash-executor.ts +2 -3
  96. package/src/extensibility/custom-tools/types.ts +2 -2
  97. package/src/extensibility/extensions/runner.ts +12 -2
  98. package/src/extensibility/plugins/git-url.ts +90 -4
  99. package/src/extensibility/plugins/manager.ts +103 -7
  100. package/src/extensibility/shared-events.ts +2 -2
  101. package/src/internal-urls/docs-index.generated.ts +88 -88
  102. package/src/main.ts +50 -56
  103. package/src/memory-backend/index.ts +1 -1
  104. package/src/memory-backend/resolve.ts +3 -3
  105. package/src/memory-backend/types.ts +3 -3
  106. package/src/{mnemosyne → mnemopi}/backend.ts +70 -70
  107. package/src/{mnemosyne → mnemopi}/config.ts +36 -36
  108. package/src/{mnemosyne → mnemopi}/state.ts +67 -67
  109. package/src/modes/acp/acp-agent.ts +13 -3
  110. package/src/modes/components/agent-dashboard.ts +6 -6
  111. package/src/modes/components/custom-editor.ts +4 -11
  112. package/src/modes/components/extensions/state-manager.ts +3 -4
  113. package/src/modes/components/footer.ts +18 -12
  114. package/src/modes/components/hook-selector.ts +86 -20
  115. package/src/modes/components/model-selector.ts +20 -11
  116. package/src/modes/components/oauth-selector.ts +93 -21
  117. package/src/modes/components/omfg-panel.ts +141 -0
  118. package/src/modes/components/settings-defs.ts +9 -2
  119. package/src/modes/components/settings-selector.ts +4 -1
  120. package/src/modes/components/status-line/segments.ts +13 -5
  121. package/src/modes/components/tips.txt +2 -1
  122. package/src/modes/components/tool-execution.ts +38 -19
  123. package/src/modes/components/tree-selector.ts +4 -3
  124. package/src/modes/components/user-message-selector.ts +94 -19
  125. package/src/modes/components/user-message.ts +8 -1
  126. package/src/modes/controllers/command-controller.ts +57 -0
  127. package/src/modes/controllers/event-controller.ts +65 -3
  128. package/src/modes/controllers/input-controller.ts +14 -11
  129. package/src/modes/controllers/omfg-controller.ts +283 -0
  130. package/src/modes/controllers/omfg-rule.ts +647 -0
  131. package/src/modes/controllers/selector-controller.ts +21 -6
  132. package/src/modes/gradient-highlight.ts +23 -6
  133. package/src/modes/interactive-mode.ts +41 -7
  134. package/src/modes/magic-keywords.ts +20 -0
  135. package/src/modes/markdown-prose.ts +247 -0
  136. package/src/modes/orchestrate.ts +17 -11
  137. package/src/modes/shared.ts +3 -11
  138. package/src/modes/theme/theme.ts +6 -0
  139. package/src/modes/turn-budget.ts +31 -0
  140. package/src/modes/types.ts +7 -1
  141. package/src/modes/ultrathink.ts +16 -10
  142. package/src/modes/utils/hotkeys-markdown.ts +1 -1
  143. package/src/modes/workflow.ts +42 -0
  144. package/src/prompts/system/auto-thinking-difficulty-local.md +14 -0
  145. package/src/prompts/system/auto-thinking-difficulty.md +12 -0
  146. package/src/prompts/system/omfg-user.md +51 -0
  147. package/src/prompts/system/system-prompt.md +1 -0
  148. package/src/prompts/system/workflow-notice.md +70 -0
  149. package/src/prompts/tools/eval.md +13 -1
  150. package/src/prompts/tools/memory-edit.md +1 -1
  151. package/src/sdk.ts +86 -38
  152. package/src/session/agent-session.ts +558 -80
  153. package/src/session/session-manager.ts +32 -0
  154. package/src/session/session-storage.ts +68 -8
  155. package/src/session/shake-types.ts +44 -0
  156. package/src/slash-commands/builtin-registry.ts +41 -16
  157. package/src/task/executor.ts +3 -3
  158. package/src/task/index.ts +6 -6
  159. package/src/thinking.ts +73 -1
  160. package/src/tiny/device.ts +4 -10
  161. package/src/tiny/models.ts +54 -2
  162. package/src/tiny/title-protocol.ts +11 -1
  163. package/src/tiny/worker.ts +19 -7
  164. package/src/tools/eval.ts +202 -26
  165. package/src/tools/grouped-file-output.ts +9 -2
  166. package/src/tools/index.ts +17 -5
  167. package/src/tools/memory-edit.ts +4 -4
  168. package/src/tools/memory-recall.ts +5 -5
  169. package/src/tools/memory-reflect.ts +5 -5
  170. package/src/tools/memory-retain.ts +4 -4
  171. package/src/tools/render-utils.ts +2 -1
  172. package/src/tools/search.ts +480 -76
  173. package/dist/types/mnemosyne/backend.d.ts +0 -4
  174. package/dist/types/mnemosyne/config.d.ts +0 -29
  175. package/dist/types/mnemosyne/state.d.ts +0 -72
  176. /package/dist/types/{mnemosyne → mnemopi}/index.d.ts +0 -0
  177. /package/src/{mnemosyne → mnemopi}/index.ts +0 -0
@@ -4,6 +4,8 @@
4
4
  */
5
5
  import {
6
6
  Container,
7
+ extractPrintableText,
8
+ fuzzyFilter,
7
9
  Markdown,
8
10
  matchesKey,
9
11
  padding,
@@ -91,6 +93,8 @@ class OutlinedList extends Container {
91
93
 
92
94
  export class HookSelectorComponent extends Container {
93
95
  #options: string[];
96
+ #filteredOptions: string[];
97
+ #searchQuery = "";
94
98
  #selectedIndex: number;
95
99
  #maxVisible: number;
96
100
  #listContainer: Container | undefined;
@@ -116,7 +120,8 @@ export class HookSelectorComponent extends Container {
116
120
  super();
117
121
 
118
122
  this.#options = options;
119
- this.#selectedIndex = Math.min(opts?.initialIndex ?? 0, options.length - 1);
123
+ this.#filteredOptions = options;
124
+ this.#selectedIndex = Math.min(opts?.initialIndex ?? 0, this.#filteredOptions.length - 1);
120
125
  this.#maxVisible = Math.max(3, opts?.maxVisible ?? 12);
121
126
  this.#onSelectCallback = onSelect;
122
127
  this.#onCancelCallback = onCancel;
@@ -149,8 +154,7 @@ export class HookSelectorComponent extends Container {
149
154
  s => this.#titleComponent.setText(`${this.#baseTitle} (${s}s)`),
150
155
  () => {
151
156
  opts?.onTimeout?.();
152
- // Auto-select current option on timeout (typically the first/recommended option)
153
- const selected = this.#options[this.#selectedIndex];
157
+ const selected = this.#filteredOptions[this.#selectedIndex];
154
158
  if (selected) {
155
159
  this.#onSelectCallback(selected);
156
160
  } else {
@@ -178,24 +182,31 @@ export class HookSelectorComponent extends Container {
178
182
 
179
183
  #updateList(): void {
180
184
  const lines: string[] = [];
185
+ const total = this.#filteredOptions.length;
181
186
  const startIndex = Math.max(
182
187
  0,
183
- Math.min(this.#selectedIndex - Math.floor(this.#maxVisible / 2), this.#options.length - this.#maxVisible),
188
+ Math.min(this.#selectedIndex - Math.floor(this.#maxVisible / 2), total - this.#maxVisible),
184
189
  );
185
- const endIndex = Math.min(startIndex + this.#maxVisible, this.#options.length);
190
+ const endIndex = Math.min(startIndex + this.#maxVisible, total);
186
191
 
187
192
  const mdTheme = getMarkdownTheme();
188
193
  for (let i = startIndex; i < endIndex; i++) {
194
+ const option = this.#filteredOptions[i];
195
+ if (option === undefined) continue;
189
196
  const isSelected = i === this.#selectedIndex;
190
197
  const label = isSelected
191
- ? renderInlineMarkdown(this.#options[i], mdTheme, t => theme.fg("accent", t))
192
- : renderInlineMarkdown(this.#options[i], mdTheme, t => theme.fg("text", t));
198
+ ? renderInlineMarkdown(option, mdTheme, t => theme.fg("accent", t))
199
+ : renderInlineMarkdown(option, mdTheme, t => theme.fg("text", t));
193
200
  const prefix = isSelected ? theme.fg("accent", `${theme.nav.cursor} `) : " ";
194
201
  lines.push(prefix + label);
195
202
  }
196
203
 
197
- if (startIndex > 0 || endIndex < this.#options.length) {
198
- lines.push(theme.fg("dim", ` (${this.#selectedIndex + 1}/${this.#options.length})`));
204
+ if (total === 0) {
205
+ lines.push(theme.fg("dim", " No matching options"));
206
+ }
207
+
208
+ if (startIndex > 0 || endIndex < total || this.#shouldRenderSearchStatus()) {
209
+ lines.push(this.#renderStatusLine(total));
199
210
  }
200
211
  if (this.#outlinedList) {
201
212
  this.#outlinedList.setLines(lines);
@@ -242,29 +253,84 @@ export class HookSelectorComponent extends Container {
242
253
  slider.onChange?.(next);
243
254
  }
244
255
 
256
+ #isSearchEnabled(): boolean {
257
+ return this.#options.length > this.#maxVisible;
258
+ }
259
+
260
+ #shouldRenderSearchStatus(): boolean {
261
+ return this.#isSearchEnabled() || this.#searchQuery.length > 0;
262
+ }
263
+
264
+ #renderStatusLine(total: number): string {
265
+ const selectedCount = total === 0 ? 0 : this.#selectedIndex + 1;
266
+ const count =
267
+ this.#searchQuery.trim() && total !== this.#options.length
268
+ ? `${selectedCount}/${total} of ${this.#options.length}`
269
+ : `${selectedCount}/${total}`;
270
+ const suffix = this.#searchQuery.trim() ? ` Search: ${this.#searchQuery}` : " Type to search";
271
+ return theme.fg("dim", ` (${count})${suffix}`);
272
+ }
273
+
274
+ #setSearchQuery(query: string): void {
275
+ this.#searchQuery = query;
276
+ this.#filteredOptions = query.trim() ? fuzzyFilter(this.#options, query, option => option) : this.#options;
277
+ this.#selectedIndex = 0;
278
+ this.#updateList();
279
+ }
280
+
281
+ #handleSearchInput(keyData: string): boolean {
282
+ if (!this.#isSearchEnabled()) return false;
283
+
284
+ if (matchesKey(keyData, "backspace")) {
285
+ if (this.#searchQuery.length === 0) return false;
286
+ const chars = [...this.#searchQuery];
287
+ chars.pop();
288
+ this.#setSearchQuery(chars.join(""));
289
+ return true;
290
+ }
291
+
292
+ const printableText = extractPrintableText(keyData);
293
+ if (printableText === undefined) return false;
294
+ if (this.#searchQuery.length === 0 && printableText.trim().length === 0) return false;
295
+
296
+ this.#setSearchQuery(this.#searchQuery + printableText);
297
+ return true;
298
+ }
299
+
245
300
  handleInput(keyData: string): void {
246
301
  // Reset countdown on any interaction
247
302
  this.#countdown?.reset();
248
303
 
249
- if (matchesSelectUp(keyData) || keyData === "k") {
250
- this.#selectedIndex = Math.max(0, this.#selectedIndex - 1);
251
- this.#updateList();
252
- } else if (matchesSelectDown(keyData) || keyData === "j") {
253
- this.#selectedIndex = Math.min(this.#options.length - 1, this.#selectedIndex + 1);
254
- this.#updateList();
304
+ if (matchesSelectCancel(keyData)) {
305
+ this.#onCancelCallback();
306
+ return;
307
+ }
308
+
309
+ if (this.#handleSearchInput(keyData)) {
310
+ return;
311
+ }
312
+
313
+ if (matchesSelectUp(keyData) || (!this.#isSearchEnabled() && keyData === "k")) {
314
+ if (this.#filteredOptions.length > 0) {
315
+ this.#selectedIndex = Math.max(0, this.#selectedIndex - 1);
316
+ this.#updateList();
317
+ }
318
+ } else if (matchesSelectDown(keyData) || (!this.#isSearchEnabled() && keyData === "j")) {
319
+ if (this.#filteredOptions.length > 0) {
320
+ this.#selectedIndex = Math.min(this.#filteredOptions.length - 1, this.#selectedIndex + 1);
321
+ this.#updateList();
322
+ }
255
323
  } else if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
256
- const selected = this.#options[this.#selectedIndex];
324
+ const selected = this.#filteredOptions[this.#selectedIndex];
257
325
  if (selected) this.#onSelectCallback(selected);
258
- } else if (matchesKey(keyData, "left") || (this.#slider && keyData === "h")) {
326
+ } else if (matchesKey(keyData, "left") || (this.#slider && !this.#isSearchEnabled() && keyData === "h")) {
259
327
  if (this.#slider) this.#moveSlider(-1);
260
328
  else this.#onLeftCallback?.();
261
- } else if (matchesKey(keyData, "right") || (this.#slider && keyData === "l")) {
329
+ } else if (matchesKey(keyData, "right") || (this.#slider && !this.#isSearchEnabled() && keyData === "l")) {
262
330
  if (this.#slider) this.#moveSlider(1);
263
331
  else this.#onRightCallback?.();
264
332
  } else if (this.#onExternalEditorCallback && matchesAppExternalEditor(keyData)) {
265
333
  this.#onExternalEditorCallback();
266
- } else if (matchesSelectCancel(keyData)) {
267
- this.#onCancelCallback();
268
334
  }
269
335
  }
270
336
 
@@ -19,7 +19,7 @@ import { resolveModelRoleValue } from "../../config/model-resolver";
19
19
  import type { Settings } from "../../config/settings";
20
20
  import { type ThemeColor, theme } from "../../modes/theme/theme";
21
21
  import { matchesSelectDown, matchesSelectUp } from "../../modes/utils/keybinding-matchers";
22
- import { getThinkingLevelMetadata } from "../../thinking";
22
+ import { AUTO_THINKING, type ConfiguredThinkingLevel, getConfiguredThinkingLevelMetadata } from "../../thinking";
23
23
  import { getTabBarTheme } from "../shared";
24
24
  import { DynamicBorder } from "./dynamic-border";
25
25
 
@@ -83,10 +83,15 @@ interface ScopedModelItem {
83
83
 
84
84
  interface RoleAssignment {
85
85
  model: Model;
86
- thinkingLevel: ThinkingLevel;
86
+ thinkingLevel: ConfiguredThinkingLevel;
87
87
  }
88
88
 
89
- type RoleSelectCallback = (model: Model, role: string | null, thinkingLevel?: ThinkingLevel, selector?: string) => void;
89
+ type RoleSelectCallback = (
90
+ model: Model,
91
+ role: string | null,
92
+ thinkingLevel?: ConfiguredThinkingLevel,
93
+ selector?: string,
94
+ ) => void;
90
95
  type CancelCallback = () => void;
91
96
  interface MenuRoleAction {
92
97
  label: string;
@@ -165,7 +170,7 @@ export class ModelSelectorComponent extends Container {
165
170
  settings: Settings,
166
171
  modelRegistry: ModelRegistry,
167
172
  scopedModels: ReadonlyArray<ScopedModelItem>,
168
- onSelect: (model: Model, role: string | null, thinkingLevel?: ThinkingLevel, selector?: string) => void,
173
+ onSelect: RoleSelectCallback,
169
174
  onCancel: () => void,
170
175
  options?: { temporaryOnly?: boolean; initialSearchInput?: string },
171
176
  ) {
@@ -790,7 +795,7 @@ export class ModelSelectorComponent extends Container {
790
795
  if (!tag || !assigned || !modelsAreEqual(assigned.model, item.model)) continue;
791
796
 
792
797
  const badge = makeInvertedBadge(tag, color ?? "success");
793
- const thinkingLabel = getThinkingLevelMetadata(assigned.thinkingLevel).label;
798
+ const thinkingLabel = getConfiguredThinkingLevelMetadata(assigned.thinkingLevel).label;
794
799
  roleBadgeTokens.push(`${badge} ${theme.fg("dim", `(${thinkingLabel})`)}`);
795
800
  }
796
801
  // Custom role badges
@@ -799,7 +804,7 @@ export class ModelSelectorComponent extends Container {
799
804
  const roleInfo = getRoleInfo(role, this.#settings);
800
805
  const badgeLabel = roleInfo.tag ?? roleInfo.name;
801
806
  const badge = makeInvertedBadge(badgeLabel, roleInfo.color ?? "muted");
802
- const thinkingLabel = getThinkingLevelMetadata(assigned.thinkingLevel).label;
807
+ const thinkingLabel = getConfiguredThinkingLevelMetadata(assigned.thinkingLevel).label;
803
808
  roleBadgeTokens.push(`${badge} ${theme.fg("dim", `(${thinkingLabel})`)}`);
804
809
  }
805
810
  const badgeText = roleBadgeTokens.length > 0 ? ` ${roleBadgeTokens.join(" ")}` : "";
@@ -863,11 +868,11 @@ export class ModelSelectorComponent extends Container {
863
868
  );
864
869
  }
865
870
  }
866
- #getThinkingLevelsForModel(model: Model): ReadonlyArray<ThinkingLevel> {
867
- return [ThinkingLevel.Inherit, ThinkingLevel.Off, ...getSupportedEfforts(model)];
871
+ #getThinkingLevelsForModel(model: Model): ReadonlyArray<ConfiguredThinkingLevel> {
872
+ return [ThinkingLevel.Inherit, ThinkingLevel.Off, AUTO_THINKING, ...getSupportedEfforts(model)];
868
873
  }
869
874
 
870
- #getCurrentRoleThinkingLevel(role: string): ThinkingLevel {
875
+ #getCurrentRoleThinkingLevel(role: string): ConfiguredThinkingLevel {
871
876
  return this.#roles[role]?.thinkingLevel ?? ThinkingLevel.Inherit;
872
877
  }
873
878
 
@@ -912,7 +917,7 @@ export class ModelSelectorComponent extends Container {
912
917
  const optionLines = showingThinking
913
918
  ? thinkingOptions.map((thinkingLevel, index) => {
914
919
  const prefix = index === this.#menuSelectedIndex ? ` ${theme.nav.cursor} ` : " ";
915
- const label = getThinkingLevelMetadata(thinkingLevel).label;
920
+ const label = getConfiguredThinkingLevelMetadata(thinkingLevel).label;
916
921
  return `${prefix}${label}`;
917
922
  })
918
923
  : this.#menuRoleActions.map((action, index) => {
@@ -1069,7 +1074,11 @@ export class ModelSelectorComponent extends Container {
1069
1074
  }
1070
1075
  }
1071
1076
 
1072
- #handleSelect(item: ModelItem | CanonicalModelItem, role: string | null, thinkingLevel?: ThinkingLevel): void {
1077
+ #handleSelect(
1078
+ item: ModelItem | CanonicalModelItem,
1079
+ role: string | null,
1080
+ thinkingLevel?: ConfiguredThinkingLevel,
1081
+ ): void {
1073
1082
  // For temporary role, don't save to settings - just notify caller
1074
1083
  if (role === null) {
1075
1084
  this.#onSelectCallback(item.model, null, undefined, item.selector);
@@ -1,6 +1,6 @@
1
1
  import { getOAuthProviders } from "@oh-my-pi/pi-ai/utils/oauth";
2
2
  import type { OAuthProviderInfo } from "@oh-my-pi/pi-ai/utils/oauth/types";
3
- import { Container, matchesKey, Spacer, TruncatedText } from "@oh-my-pi/pi-tui";
3
+ import { Container, extractPrintableText, fuzzyFilter, matchesKey, Spacer, TruncatedText } from "@oh-my-pi/pi-tui";
4
4
  import { theme } from "../../modes/theme/theme";
5
5
  import { matchesSelectCancel, matchesSelectDown, matchesSelectUp } from "../../modes/utils/keybinding-matchers";
6
6
  import type { AuthStorage } from "../../session/auth-storage";
@@ -13,6 +13,8 @@ const OAUTH_SELECTOR_MAX_VISIBLE = 10;
13
13
  export class OAuthSelectorComponent extends Container {
14
14
  #listContainer: Container;
15
15
  #allProviders: OAuthProviderInfo[] = [];
16
+ #filteredProviders: OAuthProviderInfo[] = [];
17
+ #searchQuery = "";
16
18
  #selectedIndex: number = 0;
17
19
  #mode: "login" | "logout";
18
20
  #authStorage: AuthStorage;
@@ -67,6 +69,7 @@ export class OAuthSelectorComponent extends Container {
67
69
  }
68
70
  #loadProviders(): void {
69
71
  this.#allProviders = getOAuthProviders();
72
+ this.#filteredProviders = this.#allProviders;
70
73
  }
71
74
 
72
75
  #startValidation(): void {
@@ -144,10 +147,68 @@ export class OAuthSelectorComponent extends Container {
144
147
  }
145
148
  return this.#authStorage.hasAuth(providerId) ? theme.fg("success", ` ${theme.status.success} logged in`) : "";
146
149
  }
150
+ #isSearchEnabled(): boolean {
151
+ return this.#allProviders.length > OAUTH_SELECTOR_MAX_VISIBLE;
152
+ }
153
+
154
+ #shouldRenderSearchStatus(): boolean {
155
+ return this.#isSearchEnabled() || this.#searchQuery.length > 0;
156
+ }
157
+
158
+ #renderStatusLine(total: number): string {
159
+ const selectedCount = total === 0 ? 0 : this.#selectedIndex + 1;
160
+ const count =
161
+ this.#searchQuery.trim() && total !== this.#allProviders.length
162
+ ? `${selectedCount}/${total} of ${this.#allProviders.length}`
163
+ : `${selectedCount}/${total}`;
164
+ const suffix = this.#searchQuery.trim() ? ` Search: ${this.#searchQuery}` : " Type to search";
165
+ return theme.fg("muted", ` (${count})${suffix}`);
166
+ }
167
+
168
+ #getProviderSearchText(provider: OAuthProviderInfo): string {
169
+ let text = `${provider.name} ${provider.id}`;
170
+ if (this.#authStorage.hasAuth(provider.id)) {
171
+ text += " logged in authenticated";
172
+ }
173
+ if (!provider.available) {
174
+ text += " unavailable";
175
+ }
176
+ return text;
177
+ }
178
+
179
+ #setSearchQuery(query: string): void {
180
+ this.#searchQuery = query;
181
+ this.#filteredProviders = query.trim()
182
+ ? fuzzyFilter(this.#allProviders, query, provider => this.#getProviderSearchText(provider))
183
+ : this.#allProviders;
184
+ this.#selectedIndex = 0;
185
+ this.#statusMessage = undefined;
186
+ this.#updateList();
187
+ }
188
+
189
+ #handleSearchInput(keyData: string): boolean {
190
+ if (!this.#isSearchEnabled()) return false;
191
+
192
+ if (matchesKey(keyData, "backspace")) {
193
+ if (this.#searchQuery.length === 0) return false;
194
+ const chars = [...this.#searchQuery];
195
+ chars.pop();
196
+ this.#setSearchQuery(chars.join(""));
197
+ return true;
198
+ }
199
+
200
+ const printableText = extractPrintableText(keyData);
201
+ if (printableText === undefined) return false;
202
+ if (this.#searchQuery.length === 0 && printableText.trim().length === 0) return false;
203
+
204
+ this.#setSearchQuery(this.#searchQuery + printableText);
205
+ return true;
206
+ }
207
+
147
208
  #updateList(): void {
148
209
  this.#listContainer.clear();
149
210
 
150
- const total = this.#allProviders.length;
211
+ const total = this.#filteredProviders.length;
151
212
  const maxVisible = OAUTH_SELECTOR_MAX_VISIBLE;
152
213
  const startIndex =
153
214
  total <= maxVisible
@@ -156,7 +217,7 @@ export class OAuthSelectorComponent extends Container {
156
217
  const endIndex = Math.min(startIndex + maxVisible, total);
157
218
 
158
219
  for (let i = startIndex; i < endIndex; i++) {
159
- const provider = this.#allProviders[i];
220
+ const provider = this.#filteredProviders[i];
160
221
  if (!provider) continue;
161
222
  const isSelected = i === this.#selectedIndex;
162
223
  const isAvailable = provider.available;
@@ -174,16 +235,19 @@ export class OAuthSelectorComponent extends Container {
174
235
  this.#listContainer.addChild(new TruncatedText(line, 0, 0));
175
236
  }
176
237
 
177
- // Scroll indicator when list is windowed
178
- if (startIndex > 0 || endIndex < total) {
179
- const scrollInfo = theme.fg("muted", ` (${this.#selectedIndex + 1}/${total})`);
180
- this.#listContainer.addChild(new TruncatedText(scrollInfo, 0, 0));
238
+ // Scroll/search indicator when list is windowed or searchable
239
+ if (startIndex > 0 || endIndex < total || this.#shouldRenderSearchStatus()) {
240
+ this.#listContainer.addChild(new TruncatedText(this.#renderStatusLine(total), 0, 0));
181
241
  }
182
242
 
183
243
  // Show "no providers" if empty
184
244
  if (total === 0) {
185
245
  const message =
186
- this.#mode === "login" ? "No OAuth providers available" : "No OAuth providers logged in. Use /login first.";
246
+ this.#allProviders.length === 0
247
+ ? this.#mode === "login"
248
+ ? "No OAuth providers available"
249
+ : "No OAuth providers logged in. Use /login first."
250
+ : "No matching providers";
187
251
  this.#listContainer.addChild(new TruncatedText(theme.fg("muted", ` ${message}`), 0, 0));
188
252
  }
189
253
  if (this.#statusMessage) {
@@ -192,25 +256,38 @@ export class OAuthSelectorComponent extends Container {
192
256
  }
193
257
  }
194
258
  handleInput(keyData: string): void {
259
+ // Escape or Ctrl+C
260
+ if (matchesSelectCancel(keyData)) {
261
+ this.stopValidation();
262
+ this.#onCancelCallback();
263
+ return;
264
+ }
265
+
266
+ if (this.#handleSearchInput(keyData)) {
267
+ return;
268
+ }
269
+
195
270
  // Up arrow
196
271
  if (matchesSelectUp(keyData)) {
197
- if (this.#allProviders.length > 0) {
198
- this.#selectedIndex = this.#selectedIndex === 0 ? this.#allProviders.length - 1 : this.#selectedIndex - 1;
272
+ if (this.#filteredProviders.length > 0) {
273
+ this.#selectedIndex =
274
+ this.#selectedIndex === 0 ? this.#filteredProviders.length - 1 : this.#selectedIndex - 1;
199
275
  }
200
276
  this.#statusMessage = undefined;
201
277
  this.#updateList();
202
278
  }
203
279
  // Down arrow
204
280
  else if (matchesSelectDown(keyData)) {
205
- if (this.#allProviders.length > 0) {
206
- this.#selectedIndex = this.#selectedIndex === this.#allProviders.length - 1 ? 0 : this.#selectedIndex + 1;
281
+ if (this.#filteredProviders.length > 0) {
282
+ this.#selectedIndex =
283
+ this.#selectedIndex === this.#filteredProviders.length - 1 ? 0 : this.#selectedIndex + 1;
207
284
  }
208
285
  this.#statusMessage = undefined;
209
286
  this.#updateList();
210
287
  }
211
288
  // Page up - jump up by one visible page
212
289
  else if (matchesKey(keyData, "pageUp")) {
213
- if (this.#allProviders.length > 0) {
290
+ if (this.#filteredProviders.length > 0) {
214
291
  this.#selectedIndex = Math.max(0, this.#selectedIndex - OAUTH_SELECTOR_MAX_VISIBLE);
215
292
  }
216
293
  this.#statusMessage = undefined;
@@ -218,9 +295,9 @@ export class OAuthSelectorComponent extends Container {
218
295
  }
219
296
  // Page down - jump down by one visible page
220
297
  else if (matchesKey(keyData, "pageDown")) {
221
- if (this.#allProviders.length > 0) {
298
+ if (this.#filteredProviders.length > 0) {
222
299
  this.#selectedIndex = Math.min(
223
- this.#allProviders.length - 1,
300
+ this.#filteredProviders.length - 1,
224
301
  this.#selectedIndex + OAUTH_SELECTOR_MAX_VISIBLE,
225
302
  );
226
303
  }
@@ -229,7 +306,7 @@ export class OAuthSelectorComponent extends Container {
229
306
  }
230
307
  // Enter
231
308
  else if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
232
- const selectedProvider = this.#allProviders[this.#selectedIndex];
309
+ const selectedProvider = this.#filteredProviders[this.#selectedIndex];
233
310
  if (selectedProvider?.available) {
234
311
  this.#statusMessage = undefined;
235
312
  this.stopValidation();
@@ -239,10 +316,5 @@ export class OAuthSelectorComponent extends Container {
239
316
  this.#updateList();
240
317
  }
241
318
  }
242
- // Escape or Ctrl+C
243
- else if (matchesSelectCancel(keyData)) {
244
- this.stopValidation();
245
- this.#onCancelCallback();
246
- }
247
319
  }
248
320
  }
@@ -0,0 +1,141 @@
1
+ import { type Component, Container, Markdown, Spacer, Text, type TUI } from "@oh-my-pi/pi-tui";
2
+ import { replaceTabs } from "../../tools/render-utils";
3
+ import { getMarkdownTheme, theme } from "../theme/theme";
4
+ import { DynamicBorder } from "./dynamic-border";
5
+
6
+ export type OmfgPanelState =
7
+ | "generating"
8
+ | "validating"
9
+ | "confirming"
10
+ | "saving"
11
+ | "saved"
12
+ | "rejected"
13
+ | "aborted"
14
+ | "error";
15
+
16
+ interface OmfgPanelComponentOptions {
17
+ complaint: string;
18
+ tui: TUI;
19
+ }
20
+
21
+ export class OmfgPanelComponent extends Container {
22
+ #complaint: string;
23
+ #tui: TUI;
24
+ #state: OmfgPanelState = "generating";
25
+ #status = "Generating TTSR rule…";
26
+ #preview = "";
27
+ #savedPath: string | undefined;
28
+ #errorMessage: string | undefined;
29
+ #closed = false;
30
+
31
+ constructor(options: OmfgPanelComponentOptions) {
32
+ super();
33
+ this.#complaint = options.complaint;
34
+ this.#tui = options.tui;
35
+ this.#rebuild();
36
+ }
37
+
38
+ appendDraft(delta: string): void {
39
+ if (!delta || this.#closed) return;
40
+ this.#preview += delta;
41
+ this.#rebuild();
42
+ }
43
+
44
+ setRule(text: string): void {
45
+ if (this.#closed) return;
46
+ this.#preview = text;
47
+ this.#rebuild();
48
+ }
49
+
50
+ setStatus(state: OmfgPanelState, status: string): void {
51
+ if (this.#closed) return;
52
+ this.#state = state;
53
+ this.#status = status;
54
+ this.#errorMessage = undefined;
55
+ this.#rebuild();
56
+ }
57
+
58
+ markSaved(path: string): void {
59
+ if (this.#closed) return;
60
+ this.#state = "saved";
61
+ this.#savedPath = path;
62
+ this.#status = `Saved ${path}`;
63
+ this.#errorMessage = undefined;
64
+ this.#rebuild();
65
+ }
66
+
67
+ markRejected(): void {
68
+ if (this.#closed) return;
69
+ this.#state = "rejected";
70
+ this.#status = "Rule was not saved.";
71
+ this.#errorMessage = undefined;
72
+ this.#rebuild();
73
+ }
74
+
75
+ markAborted(): void {
76
+ if (this.#closed) return;
77
+ this.#state = "aborted";
78
+ this.#status = "Cancelled.";
79
+ this.#errorMessage = undefined;
80
+ this.#rebuild();
81
+ }
82
+
83
+ markError(message: string): void {
84
+ if (this.#closed) return;
85
+ this.#state = "error";
86
+ this.#status = "Could not create rule.";
87
+ this.#errorMessage = message;
88
+ this.#rebuild();
89
+ }
90
+
91
+ close(): void {
92
+ this.#closed = true;
93
+ }
94
+
95
+ #rebuild(): void {
96
+ this.clear();
97
+ this.addChild(new DynamicBorder(str => theme.fg("dim", str)));
98
+ this.addChild(new Spacer(1));
99
+ this.addChild(new Text(theme.fg("accent", replaceTabs(`/omfg ${this.#complaint}`)), 1, 0));
100
+ this.addChild(new Text(theme.fg("muted", replaceTabs(this.#status)), 1, 0));
101
+ this.addChild(new Spacer(1));
102
+ this.addChild(this.#contentComponent());
103
+ this.addChild(new Spacer(1));
104
+ this.addChild(new Text(this.#footerLine(), 1, 0));
105
+ this.addChild(new Spacer(1));
106
+ this.addChild(new DynamicBorder(str => theme.fg("dim", str)));
107
+ this.#tui.requestRender();
108
+ }
109
+
110
+ #footerLine(): string {
111
+ switch (this.#state) {
112
+ case "generating":
113
+ case "validating":
114
+ case "confirming":
115
+ case "saving":
116
+ return theme.fg("muted", "Esc cancel /omfg");
117
+ case "saved":
118
+ return theme.fg(
119
+ "success",
120
+ `${theme.status.success} Registered live · ${replaceTabs(this.#savedPath ?? "saved")}`,
121
+ );
122
+ case "rejected":
123
+ return theme.fg("warning", `${theme.status.warning} Not saved · Esc dismiss`);
124
+ case "aborted":
125
+ return theme.fg("warning", `${theme.status.warning} Cancelled · Esc dismiss`);
126
+ case "error":
127
+ return theme.fg("error", `${theme.status.error} Error · Esc dismiss`);
128
+ }
129
+ }
130
+
131
+ #contentComponent(): Component {
132
+ if (this.#state === "error") {
133
+ return new Text(theme.fg("error", replaceTabs(this.#errorMessage ?? "Unknown error")), 1, 0);
134
+ }
135
+ const text = replaceTabs(this.#preview).trim();
136
+ if (!text) {
137
+ return new Text(theme.fg("dim", `${theme.status.pending} Waiting for candidate rule…`), 1, 0);
138
+ }
139
+ return new Markdown(text, 1, 0, getMarkdownTheme());
140
+ }
141
+ }
@@ -79,9 +79,16 @@ const CONDITIONS: Record<string, () => boolean> = {
79
79
  return false;
80
80
  }
81
81
  },
82
- mnemosyneActive: () => {
82
+ mnemopiActive: () => {
83
83
  try {
84
- return Settings.instance.get("memory.backend") === "mnemosyne";
84
+ return Settings.instance.get("memory.backend") === "mnemopi";
85
+ } catch {
86
+ return false;
87
+ }
88
+ },
89
+ autoThinkingActive: () => {
90
+ try {
91
+ return Settings.instance.get("defaultThinkingLevel") === "auto";
85
92
  } catch {
86
93
  return false;
87
94
  }
@@ -23,6 +23,7 @@ import type {
23
23
  import { SETTING_TABS, TAB_METADATA } from "../../config/settings-schema";
24
24
  import { getCurrentThemeName, getSelectListTheme, getSettingsListTheme, theme } from "../../modes/theme/theme";
25
25
  import { matchesAppInterrupt } from "../../modes/utils/keybinding-matchers";
26
+ import { AUTO_THINKING, type ConfiguredThinkingLevel } from "../../thinking";
26
27
  import { getTabBarTheme } from "../shared";
27
28
  import { DynamicBorder } from "./dynamic-border";
28
29
  import { handleInputOrEscape, PluginSettingsComponent } from "./plugin-settings";
@@ -373,7 +374,9 @@ export class SettingsSelectorComponent extends Container {
373
374
 
374
375
  // Special case: inject runtime options for thinking level
375
376
  if (def.path === "defaultThinkingLevel") {
376
- options = this.context.availableThinkingLevels.map(level => {
377
+ // Prepend `auto`; the rest are the model's runtime-supported efforts.
378
+ const levels: ConfiguredThinkingLevel[] = [AUTO_THINKING, ...this.context.availableThinkingLevels];
379
+ options = levels.map(level => {
377
380
  const baseOpt = options.find(o => o.value === level);
378
381
  return baseOpt || { value: level, label: level };
379
382
  });
@@ -90,11 +90,19 @@ const modelSegment: StatusLineSegment = {
90
90
 
91
91
  // Add thinking level with dot separator
92
92
  if (opts.showThinkingLevel !== false && state.model?.thinking) {
93
- const level = state.thinkingLevel ?? ThinkingLevel.Off;
94
- if (level !== ThinkingLevel.Off) {
95
- const thinkingText = theme.thinking[level as keyof typeof theme.thinking];
96
- if (thinkingText) {
97
- content += `${theme.sep.dot}${thinkingText}`;
93
+ if (ctx.session.isAutoThinking) {
94
+ // Pending (no turn classified yet / classifying) shows a symbol-theme
95
+ // question-box marker; once resolved it shows `<level>`.
96
+ const resolved = ctx.session.autoResolvedThinkingLevel();
97
+ const resolvedText = resolved ? (theme.thinking[resolved as keyof typeof theme.thinking] ?? resolved) : "";
98
+ content += `${theme.sep.dot}${resolved ? resolvedText : `${theme.thinking.autoPending} auto`}`;
99
+ } else {
100
+ const level = state.thinkingLevel ?? ThinkingLevel.Off;
101
+ if (level !== ThinkingLevel.Off) {
102
+ const thinkingText = theme.thinking[level as keyof typeof theme.thinking];
103
+ if (thinkingText) {
104
+ content += `${theme.sep.dot}${thinkingText}`;
105
+ }
98
106
  }
99
107
  }
100
108
  }
@@ -4,10 +4,11 @@ Ctrl+D can be used to exit, but with your draft saved!
4
4
  Find out which model you emotionally abuse the most with `omp stats`
5
5
  Try task isolation to create CoW worktrees
6
6
  Your LLM can call an LLM using `llm(x...)`. Have a big batch of tasks? Ask clanker to use it!
7
- Next time you see spaghet try: "omp, create a TTSR rule that will prevent this pattern, use omp://"
7
+ Spaghetti code? Try complaining with /omfg
8
8
  Did you know? Each kitty/tmux split keeps its own session — `omp -c` resumes the right one
9
9
  Drop the word `ultrathink` in your message for harder multi-step reasoning — watch it glow rainbow as you type
10
10
  Say `orchestrate` in your message to drive a multi-phase task with parallel subagents — watch it glow as you type
11
+ Say `workflow` in your message to drive the task with parallel subagents in eval — watch it glow as you type
11
12
  Log in to several accounts of the same provider — `/login` again — and omp load-balances across them automatically
12
13
  Run `omp auth-broker serve` once and every machine pulls live tokens over the wire — refresh keys never leave the host; `omp auth-gateway` fronts it as a drop-in proxy any OpenAI-compatible client can hit
13
14
  Press alt+p (or /switch) to switch provider, and ctrl+p to cycle role models smol -> slow -> etc