@oh-my-pi/pi-coding-agent 15.7.2 → 15.7.5

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 (188) hide show
  1. package/CHANGELOG.md +114 -6
  2. package/dist/types/cli/args.d.ts +1 -1
  3. package/dist/types/cli/extension-flags.d.ts +36 -0
  4. package/dist/types/config/config-file.d.ts +4 -0
  5. package/dist/types/config/file-lock.d.ts +23 -0
  6. package/dist/types/config/keybindings.d.ts +2 -1
  7. package/dist/types/config/model-registry.d.ts +6 -0
  8. package/dist/types/config/settings-schema.d.ts +69 -65
  9. package/dist/types/edit/hashline/diff.d.ts +3 -3
  10. package/dist/types/eval/__tests__/agent-bridge.test.d.ts +1 -0
  11. package/dist/types/eval/__tests__/budget-bridge.test.d.ts +1 -0
  12. package/dist/types/eval/__tests__/idle-timeout.test.d.ts +1 -0
  13. package/dist/types/eval/agent-bridge.d.ts +25 -0
  14. package/dist/types/eval/backend.d.ts +17 -2
  15. package/dist/types/eval/budget-bridge.d.ts +29 -0
  16. package/dist/types/eval/idle-timeout.d.ts +28 -0
  17. package/dist/types/eval/js/executor.d.ts +8 -0
  18. package/dist/types/eval/js/tool-bridge.d.ts +2 -1
  19. package/dist/types/eval/py/executor.d.ts +13 -0
  20. package/dist/types/exec/bash-executor.d.ts +1 -0
  21. package/dist/types/extensibility/custom-tools/types.d.ts +2 -2
  22. package/dist/types/extensibility/extensions/runner.d.ts +7 -0
  23. package/dist/types/extensibility/plugins/git-url.d.ts +11 -1
  24. package/dist/types/extensibility/plugins/manager.d.ts +12 -1
  25. package/dist/types/extensibility/shared-events.d.ts +2 -2
  26. package/dist/types/internal-urls/local-protocol.d.ts +19 -9
  27. package/dist/types/internal-urls/types.d.ts +14 -0
  28. package/dist/types/lsp/client.d.ts +3 -0
  29. package/dist/types/mcp/manager.d.ts +14 -5
  30. package/dist/types/memory-backend/index.d.ts +1 -1
  31. package/dist/types/memory-backend/resolve.d.ts +1 -1
  32. package/dist/types/memory-backend/types.d.ts +3 -3
  33. package/dist/types/mnemopi/backend.d.ts +4 -0
  34. package/dist/types/mnemopi/config.d.ts +29 -0
  35. package/dist/types/mnemopi/state.d.ts +72 -0
  36. package/dist/types/modes/components/custom-editor.d.ts +2 -2
  37. package/dist/types/modes/components/omfg-panel.d.ts +19 -0
  38. package/dist/types/modes/controllers/command-controller.d.ts +6 -0
  39. package/dist/types/modes/controllers/input-controller.d.ts +1 -3
  40. package/dist/types/modes/controllers/omfg-controller.d.ts +10 -0
  41. package/dist/types/modes/controllers/omfg-rule.d.ts +26 -0
  42. package/dist/types/modes/gradient-highlight.d.ts +5 -1
  43. package/dist/types/modes/interactive-mode.d.ts +7 -3
  44. package/dist/types/modes/magic-keywords.d.ts +14 -0
  45. package/dist/types/modes/markdown-prose.d.ts +27 -0
  46. package/dist/types/modes/orchestrate.d.ts +7 -2
  47. package/dist/types/modes/shared.d.ts +1 -1
  48. package/dist/types/modes/turn-budget.d.ts +18 -0
  49. package/dist/types/modes/types.d.ts +7 -3
  50. package/dist/types/modes/ultrathink.d.ts +7 -2
  51. package/dist/types/modes/workflow.d.ts +15 -0
  52. package/dist/types/sdk.d.ts +13 -3
  53. package/dist/types/session/agent-session.d.ts +36 -17
  54. package/dist/types/session/session-manager.d.ts +18 -0
  55. package/dist/types/session/session-storage.d.ts +6 -0
  56. package/dist/types/session/shake-types.d.ts +24 -0
  57. package/dist/types/task/executor.d.ts +2 -2
  58. package/dist/types/task/repair-args.d.ts +52 -0
  59. package/dist/types/tiny/models.d.ts +1 -1
  60. package/dist/types/tiny/title-client.d.ts +28 -2
  61. package/dist/types/tiny/title-protocol.d.ts +8 -5
  62. package/dist/types/tools/find.d.ts +1 -1
  63. package/dist/types/tools/index.d.ts +19 -3
  64. package/dist/types/tools/memory-edit.d.ts +1 -1
  65. package/dist/types/tools/path-utils.d.ts +7 -0
  66. package/dist/types/tui/output-block.d.ts +7 -7
  67. package/examples/sdk/09-api-keys-and-oauth.ts +2 -2
  68. package/package.json +10 -10
  69. package/scripts/build-binary.ts +0 -1
  70. package/src/autoresearch/tools/run-experiment.ts +45 -113
  71. package/src/cli/args.ts +39 -16
  72. package/src/cli/extension-flags.ts +48 -0
  73. package/src/cli/plugin-cli.ts +11 -2
  74. package/src/cli.ts +59 -0
  75. package/src/config/config-file.ts +98 -13
  76. package/src/config/file-lock.ts +60 -17
  77. package/src/config/keybindings.ts +78 -27
  78. package/src/config/model-registry.ts +7 -1
  79. package/src/config/settings-schema.ts +73 -67
  80. package/src/config/settings.ts +22 -0
  81. package/src/edit/hashline/diff.ts +81 -24
  82. package/src/edit/renderer.ts +16 -12
  83. package/src/eval/__tests__/agent-bridge.test.ts +433 -0
  84. package/src/eval/__tests__/budget-bridge.test.ts +69 -0
  85. package/src/eval/__tests__/idle-timeout.test.ts +66 -0
  86. package/src/eval/__tests__/shared-executors.test.ts +21 -0
  87. package/src/eval/agent-bridge.ts +295 -0
  88. package/src/eval/backend.ts +17 -2
  89. package/src/eval/budget-bridge.ts +48 -0
  90. package/src/eval/idle-timeout.ts +80 -0
  91. package/src/eval/js/executor.ts +35 -7
  92. package/src/eval/js/index.ts +2 -1
  93. package/src/eval/js/shared/prelude.txt +85 -1
  94. package/src/eval/js/tool-bridge.ts +9 -0
  95. package/src/eval/py/executor.ts +41 -14
  96. package/src/eval/py/index.ts +2 -1
  97. package/src/eval/py/prelude.py +132 -1
  98. package/src/exec/bash-executor.ts +2 -3
  99. package/src/extensibility/custom-tools/types.ts +2 -2
  100. package/src/extensibility/extensions/runner.ts +12 -2
  101. package/src/extensibility/plugins/git-url.ts +90 -4
  102. package/src/extensibility/plugins/manager.ts +103 -7
  103. package/src/extensibility/shared-events.ts +2 -2
  104. package/src/internal-urls/docs-index.generated.ts +88 -88
  105. package/src/internal-urls/local-protocol.ts +23 -11
  106. package/src/internal-urls/types.ts +15 -0
  107. package/src/lsp/client.ts +28 -5
  108. package/src/main.ts +44 -55
  109. package/src/mcp/manager.ts +87 -4
  110. package/src/memory-backend/index.ts +1 -1
  111. package/src/memory-backend/resolve.ts +3 -3
  112. package/src/memory-backend/types.ts +3 -3
  113. package/src/{mnemosyne → mnemopi}/backend.ts +70 -70
  114. package/src/{mnemosyne → mnemopi}/config.ts +36 -36
  115. package/src/{mnemosyne → mnemopi}/state.ts +67 -67
  116. package/src/modes/components/agent-dashboard.ts +6 -6
  117. package/src/modes/components/custom-editor.ts +4 -11
  118. package/src/modes/components/extensions/state-manager.ts +3 -4
  119. package/src/modes/components/footer.ts +8 -9
  120. package/src/modes/components/hook-selector.ts +86 -20
  121. package/src/modes/components/oauth-selector.ts +93 -21
  122. package/src/modes/components/omfg-panel.ts +141 -0
  123. package/src/modes/components/settings-defs.ts +2 -2
  124. package/src/modes/components/tips.txt +2 -1
  125. package/src/modes/components/tool-execution.ts +38 -19
  126. package/src/modes/components/tree-selector.ts +4 -3
  127. package/src/modes/components/user-message-selector.ts +94 -19
  128. package/src/modes/components/user-message.ts +8 -1
  129. package/src/modes/controllers/command-controller.ts +25 -0
  130. package/src/modes/controllers/event-controller.ts +68 -3
  131. package/src/modes/controllers/input-controller.ts +14 -11
  132. package/src/modes/controllers/mcp-command-controller.ts +1 -1
  133. package/src/modes/controllers/omfg-controller.ts +283 -0
  134. package/src/modes/controllers/omfg-rule.ts +647 -0
  135. package/src/modes/controllers/selector-controller.ts +1 -0
  136. package/src/modes/gradient-highlight.ts +23 -6
  137. package/src/modes/interactive-mode.ts +41 -7
  138. package/src/modes/magic-keywords.ts +20 -0
  139. package/src/modes/markdown-prose.ts +247 -0
  140. package/src/modes/orchestrate.ts +17 -11
  141. package/src/modes/shared.ts +3 -11
  142. package/src/modes/turn-budget.ts +31 -0
  143. package/src/modes/types.ts +7 -1
  144. package/src/modes/ultrathink.ts +16 -10
  145. package/src/modes/utils/hotkeys-markdown.ts +1 -1
  146. package/src/modes/workflow.ts +42 -0
  147. package/src/prompts/system/omfg-user.md +51 -0
  148. package/src/prompts/system/project-prompt.md +3 -2
  149. package/src/prompts/system/subagent-system-prompt.md +12 -8
  150. package/src/prompts/system/system-prompt.md +9 -6
  151. package/src/prompts/system/workflow-notice.md +70 -0
  152. package/src/prompts/tools/eval.md +13 -1
  153. package/src/prompts/tools/memory-edit.md +1 -1
  154. package/src/sdk.ts +63 -33
  155. package/src/session/agent-session.ts +290 -55
  156. package/src/session/session-manager.ts +32 -0
  157. package/src/session/session-storage.ts +68 -8
  158. package/src/session/shake-types.ts +43 -0
  159. package/src/slash-commands/builtin-registry.ts +39 -16
  160. package/src/task/executor.ts +17 -7
  161. package/src/task/index.ts +9 -8
  162. package/src/task/repair-args.ts +117 -0
  163. package/src/tiny/models.ts +2 -2
  164. package/src/tiny/title-client.ts +133 -43
  165. package/src/tiny/title-protocol.ts +10 -5
  166. package/src/tiny/worker.ts +3 -46
  167. package/src/tools/ast-edit.ts +3 -0
  168. package/src/tools/ast-grep.ts +3 -0
  169. package/src/tools/eval.ts +202 -26
  170. package/src/tools/find.ts +20 -6
  171. package/src/tools/gh.ts +1 -0
  172. package/src/tools/grouped-file-output.ts +9 -2
  173. package/src/tools/index.ts +17 -5
  174. package/src/tools/memory-edit.ts +4 -4
  175. package/src/tools/memory-recall.ts +5 -5
  176. package/src/tools/memory-reflect.ts +5 -5
  177. package/src/tools/memory-retain.ts +4 -4
  178. package/src/tools/path-utils.ts +13 -2
  179. package/src/tools/read.ts +1 -0
  180. package/src/tools/render-utils.ts +2 -1
  181. package/src/tools/search.ts +491 -76
  182. package/src/tui/output-block.ts +37 -75
  183. package/src/utils/git.ts +9 -3
  184. package/dist/types/mnemosyne/backend.d.ts +0 -4
  185. package/dist/types/mnemosyne/config.d.ts +0 -29
  186. package/dist/types/mnemosyne/state.d.ts +0 -72
  187. /package/dist/types/{mnemosyne → mnemopi}/index.d.ts +0 -0
  188. /package/src/{mnemosyne → mnemopi}/index.ts +0 -0
@@ -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,9 @@ 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
85
  } catch {
86
86
  return false;
87
87
  }
@@ -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
@@ -45,15 +45,6 @@ function ensureInvalidate(component: unknown): Component {
45
45
  return c as Component;
46
46
  }
47
47
 
48
- function cloneToolArgs<T>(args: T): T {
49
- if (args === null || args === undefined) return args;
50
- try {
51
- return structuredClone(args);
52
- } catch {
53
- return args;
54
- }
55
- }
56
-
57
48
  /**
58
49
  * Drop trailing removal/hunk-header lines that appear in a streaming diff
59
50
  * before the matching `+added` lines have arrived. Without this, a partial
@@ -107,6 +98,27 @@ function resolveEditModeForTool(toolName: string, tool: AgentTool | undefined):
107
98
  return (tool as { mode?: EditMode } | undefined)?.mode;
108
99
  }
109
100
 
101
+ function rawTextInputFromPartialJson(partialJson: unknown): string | undefined {
102
+ if (typeof partialJson !== "string") return undefined;
103
+ if (partialJson.length === 0) return undefined;
104
+ const trimmed = partialJson.trimStart();
105
+ if (trimmed.length === 0) return undefined;
106
+ const first = trimmed[0];
107
+ // Function-tool arguments stream as JSON. Custom/free-form tools stream raw
108
+ // text in the same transport field; only the raw form is a valid fallback for
109
+ // the conventional `input` parameter.
110
+ if (first === "{" || first === "[" || first === '"') return undefined;
111
+ return partialJson;
112
+ }
113
+
114
+ function getArgsWithStreamedTextInput(args: unknown): unknown {
115
+ if (args == null || typeof args !== "object") return args;
116
+ const record = args as Record<string, unknown>;
117
+ if (typeof record.input === "string") return args;
118
+ const input = rawTextInputFromPartialJson(record.__partialJson);
119
+ return input === undefined ? args : { ...record, input };
120
+ }
121
+
110
122
  export interface ToolExecutionOptions {
111
123
  snapshots?: SnapshotStore;
112
124
  showImages?: boolean; // default: true (only used if terminal supports images)
@@ -198,7 +210,7 @@ export class ToolExecutionComponent extends Container {
198
210
  this.#tool = tool;
199
211
  this.#ui = ui;
200
212
  this.#cwd = cwd;
201
- this.#args = cloneToolArgs(args);
213
+ this.#args = args;
202
214
 
203
215
  this.addChild(new Spacer(1));
204
216
 
@@ -222,7 +234,12 @@ export class ToolExecutionComponent extends Container {
222
234
  }
223
235
 
224
236
  updateArgs(args: any, _toolCallId?: string): void {
225
- this.#args = cloneToolArgs(args);
237
+ // Reference-equality short-circuit before any further work. Callers
238
+ // always allocate a new arg object on each streamed delta (see
239
+ // event-controller.ts and ui-helpers.ts), so a same-reference assignment
240
+ // signals "nothing meaningful changed" and the renderer can skip.
241
+ if (args === this.#args) return;
242
+ this.#args = args;
226
243
  this.#updateSpinnerAnimation();
227
244
  void this.#runPreviewDiff();
228
245
  this.#updateDisplay();
@@ -247,12 +264,13 @@ export class ToolExecutionComponent extends Container {
247
264
  const args = this.#args;
248
265
  if (args == null || typeof args !== "object") return;
249
266
 
250
- const partialJson = (args as { __partialJson?: string }).__partialJson;
267
+ const previewArgs = getArgsWithStreamedTextInput(args);
268
+ const partialJson = (previewArgs as { __partialJson?: string }).__partialJson;
251
269
  let effectiveArgs: unknown;
252
270
  try {
253
- effectiveArgs = strategy.extractCompleteEdits(args, partialJson);
271
+ effectiveArgs = strategy.extractCompleteEdits(previewArgs, partialJson);
254
272
  } catch {
255
- effectiveArgs = args;
273
+ effectiveArgs = previewArgs;
256
274
  }
257
275
 
258
276
  // Coalesce duplicate computes for identical args. The key pairs the
@@ -717,20 +735,21 @@ export class ToolExecutionComponent extends Container {
717
735
  }
718
736
 
719
737
  #getCallArgsForRender(): any {
738
+ const renderArgs = getArgsWithStreamedTextInput(this.#args);
720
739
  if (!isEditLikeToolName(this.#toolName)) {
721
- return this.#args;
740
+ return renderArgs;
722
741
  }
723
742
  const previews = this.#editDiffPreview;
724
743
  if (!previews || previews.length === 0) {
725
- return this.#args;
744
+ return renderArgs;
726
745
  }
727
746
  // Single-file previews feed the existing `previewDiff` channel consumed
728
747
  // by `formatStreamingDiff` in the renderer.
729
748
  const first = previews[0];
730
749
  if (!first?.diff) {
731
- return this.#args;
750
+ return renderArgs;
732
751
  }
733
- return { ...(this.#args as Record<string, unknown>), previewDiff: first.diff };
752
+ return { ...(renderArgs as Record<string, unknown>), previewDiff: first.diff };
734
753
  }
735
754
 
736
755
  /**
@@ -781,7 +800,7 @@ export class ToolExecutionComponent extends Container {
781
800
  if (!previews?.some(preview => preview.diff)) {
782
801
  const editMode = this.#editMode;
783
802
  const strategy = editMode ? EDIT_MODE_STRATEGIES[editMode] : undefined;
784
- const fallback = strategy?.renderStreamingFallback(this.#args, theme);
803
+ const fallback = strategy?.renderStreamingFallback(getArgsWithStreamedTextInput(this.#args), theme);
785
804
  if (fallback) context.editStreamingFallback = fallback;
786
805
  }
787
806
  context.renderDiff = renderDiff;
@@ -3,6 +3,7 @@ import {
3
3
  type Component,
4
4
  Container,
5
5
  extractPrintableText,
6
+ fuzzyMatch,
6
7
  Input,
7
8
  matchesKey,
8
9
  Spacer,
@@ -325,10 +326,10 @@ class TreeList implements Component {
325
326
 
326
327
  if (!passesFilter) return false;
327
328
 
328
- // Apply search filter
329
+ // Apply fuzzy search filter
329
330
  if (searchTokens.length > 0) {
330
- const nodeText = this.#getSearchableText(flatNode.node).toLowerCase();
331
- return searchTokens.every(token => nodeText.includes(token));
331
+ const nodeText = this.#getSearchableText(flatNode.node);
332
+ return searchTokens.every(token => fuzzyMatch(token, nodeText).matches);
332
333
  }
333
334
 
334
335
  return true;
@@ -1,4 +1,13 @@
1
- import { type Component, Container, matchesKey, Spacer, Text, truncateToWidth } from "@oh-my-pi/pi-tui";
1
+ import {
2
+ type Component,
3
+ Container,
4
+ extractPrintableText,
5
+ fuzzyFilter,
6
+ matchesKey,
7
+ Spacer,
8
+ Text,
9
+ truncateToWidth,
10
+ } from "@oh-my-pi/pi-tui";
2
11
  import { theme } from "../../modes/theme/theme";
3
12
  import { matchesSelectCancel, matchesSelectDown, matchesSelectUp } from "../../modes/utils/keybinding-matchers";
4
13
  import { DynamicBorder } from "./dynamic-border";
@@ -13,6 +22,8 @@ interface UserMessageItem {
13
22
  * Custom user message list component with selection
14
23
  */
15
24
  class UserMessageList implements Component {
25
+ #filteredMessages: UserMessageItem[];
26
+ #searchQuery = "";
16
27
  #selectedIndex: number = 0;
17
28
  onSelect?: (entryId: string) => void;
18
29
  onCancel?: () => void;
@@ -20,14 +31,60 @@ class UserMessageList implements Component {
20
31
 
21
32
  constructor(private readonly messages: UserMessageItem[]) {
22
33
  // Store messages in chronological order (oldest to newest)
34
+ this.#filteredMessages = messages;
23
35
  // Start with the last (most recent) message selected
24
- this.#selectedIndex = Math.max(0, this.messages.length - 1);
36
+ this.#selectedIndex = Math.max(0, this.#filteredMessages.length - 1);
25
37
  }
26
38
 
27
39
  invalidate(): void {
28
40
  // No cached state to invalidate currently
29
41
  }
30
42
 
43
+ #isSearchEnabled(): boolean {
44
+ return this.messages.length > this.#maxVisible;
45
+ }
46
+
47
+ #shouldRenderSearchStatus(): boolean {
48
+ return this.#isSearchEnabled() || this.#searchQuery.length > 0;
49
+ }
50
+
51
+ #renderStatusLine(total: number): string {
52
+ const selectedCount = total === 0 ? 0 : this.#selectedIndex + 1;
53
+ const count =
54
+ this.#searchQuery.trim() && total !== this.messages.length
55
+ ? `${selectedCount}/${total} of ${this.messages.length}`
56
+ : `${selectedCount}/${total}`;
57
+ const suffix = this.#searchQuery.trim() ? ` Search: ${this.#searchQuery}` : " Type to search";
58
+ return theme.fg("muted", ` (${count})${suffix}`);
59
+ }
60
+
61
+ #setSearchQuery(query: string): void {
62
+ this.#searchQuery = query;
63
+ this.#filteredMessages = query.trim()
64
+ ? fuzzyFilter(this.messages, query, message => `${message.text} ${message.timestamp ?? ""}`)
65
+ : this.messages;
66
+ this.#selectedIndex = query.trim() ? 0 : Math.max(0, this.#filteredMessages.length - 1);
67
+ }
68
+
69
+ #handleSearchInput(keyData: string): boolean {
70
+ if (!this.#isSearchEnabled()) return false;
71
+
72
+ if (matchesKey(keyData, "backspace")) {
73
+ if (this.#searchQuery.length === 0) return false;
74
+ const chars = [...this.#searchQuery];
75
+ chars.pop();
76
+ this.#setSearchQuery(chars.join(""));
77
+ return true;
78
+ }
79
+
80
+ const printableText = extractPrintableText(keyData);
81
+ if (printableText === undefined) return false;
82
+ if (this.#searchQuery.length === 0 && printableText.trim().length === 0) return false;
83
+
84
+ this.#setSearchQuery(this.#searchQuery + printableText);
85
+ return true;
86
+ }
87
+
31
88
  render(width: number): string[] {
32
89
  const lines: string[] = [];
33
90
 
@@ -36,16 +93,19 @@ class UserMessageList implements Component {
36
93
  return lines;
37
94
  }
38
95
 
96
+ const total = this.#filteredMessages.length;
97
+
39
98
  // Calculate visible range with scrolling
40
99
  const startIndex = Math.max(
41
100
  0,
42
- Math.min(this.#selectedIndex - Math.floor(this.#maxVisible / 2), this.messages.length - this.#maxVisible),
101
+ Math.min(this.#selectedIndex - Math.floor(this.#maxVisible / 2), total - this.#maxVisible),
43
102
  );
44
- const endIndex = Math.min(startIndex + this.#maxVisible, this.messages.length);
103
+ const endIndex = Math.min(startIndex + this.#maxVisible, total);
45
104
 
46
105
  // Render visible messages (2 lines per message + blank line)
47
106
  for (let i = startIndex; i < endIndex; i++) {
48
- const message = this.messages[i];
107
+ const message = this.#filteredMessages[i];
108
+ if (!message) continue;
49
109
  const isSelected = i === this.#selectedIndex;
50
110
 
51
111
  // Normalize message to single line
@@ -60,44 +120,59 @@ class UserMessageList implements Component {
60
120
  lines.push(messageLine);
61
121
 
62
122
  // Second line: metadata (position in history)
63
- const position = i + 1;
123
+ const position = this.messages.indexOf(message) + 1;
64
124
  const metadata = ` Message ${position} of ${this.messages.length}`;
65
125
  const metadataLine = theme.fg("muted", metadata);
66
126
  lines.push(metadataLine);
67
127
  lines.push(""); // Blank line between messages
68
128
  }
69
129
 
70
- // Add scroll indicator if needed
71
- if (startIndex > 0 || endIndex < this.messages.length) {
72
- const scrollInfo = theme.fg("muted", ` (${this.#selectedIndex + 1}/${this.messages.length})`);
73
- lines.push(scrollInfo);
130
+ if (total === 0) {
131
+ lines.push(theme.fg("muted", " No matching messages"));
132
+ }
133
+
134
+ // Add scroll/search indicator if needed
135
+ if (startIndex > 0 || endIndex < total || this.#shouldRenderSearchStatus()) {
136
+ lines.push(this.#renderStatusLine(total));
74
137
  }
75
138
 
76
139
  return lines;
77
140
  }
78
141
 
79
142
  handleInput(keyData: string): void {
143
+ // Escape / cancel
144
+ if (matchesSelectCancel(keyData)) {
145
+ if (this.onCancel) {
146
+ this.onCancel();
147
+ }
148
+ return;
149
+ }
150
+
151
+ if (this.#handleSearchInput(keyData)) {
152
+ return;
153
+ }
154
+
80
155
  // Up arrow - go to previous (older) message, wrap to bottom when at top
81
156
  if (matchesSelectUp(keyData)) {
82
- this.#selectedIndex = this.#selectedIndex === 0 ? this.messages.length - 1 : this.#selectedIndex - 1;
157
+ if (this.#filteredMessages.length > 0) {
158
+ this.#selectedIndex =
159
+ this.#selectedIndex === 0 ? this.#filteredMessages.length - 1 : this.#selectedIndex - 1;
160
+ }
83
161
  }
84
162
  // Down arrow - go to next (newer) message, wrap to top when at bottom
85
163
  else if (matchesSelectDown(keyData)) {
86
- this.#selectedIndex = this.#selectedIndex === this.messages.length - 1 ? 0 : this.#selectedIndex + 1;
164
+ if (this.#filteredMessages.length > 0) {
165
+ this.#selectedIndex =
166
+ this.#selectedIndex === this.#filteredMessages.length - 1 ? 0 : this.#selectedIndex + 1;
167
+ }
87
168
  }
88
169
  // Enter - select message and branch
89
170
  else if (matchesKey(keyData, "enter") || matchesKey(keyData, "return") || keyData === "\n") {
90
- const selected = this.messages[this.#selectedIndex];
171
+ const selected = this.#filteredMessages[this.#selectedIndex];
91
172
  if (selected && this.onSelect) {
92
173
  this.onSelect(selected.id);
93
174
  }
94
175
  }
95
- // Escape / cancel
96
- else if (matchesSelectCancel(keyData)) {
97
- if (this.onCancel) {
98
- this.onCancel();
99
- }
100
- }
101
176
  }
102
177
  }
103
178