@oh-my-pi/pi-coding-agent 14.1.2 → 14.2.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 (130) hide show
  1. package/CHANGELOG.md +47 -2
  2. package/package.json +8 -8
  3. package/scripts/build-binary.ts +61 -0
  4. package/src/autoresearch/helpers.ts +10 -0
  5. package/src/autoresearch/index.ts +1 -11
  6. package/src/autoresearch/tools/init-experiment.ts +1 -10
  7. package/src/autoresearch/tools/log-experiment.ts +1 -11
  8. package/src/autoresearch/tools/run-experiment.ts +1 -10
  9. package/src/bun-imports.d.ts +6 -0
  10. package/src/cli/plugin-cli.ts +23 -45
  11. package/src/commit/agentic/tools/propose-commit.ts +1 -14
  12. package/src/commit/agentic/tools/split-commit.ts +1 -15
  13. package/src/commit/utils.ts +15 -1
  14. package/src/config/model-registry.ts +3 -3
  15. package/src/config/prompt-templates.ts +4 -12
  16. package/src/config/settings-schema.ts +27 -2
  17. package/src/config/settings.ts +1 -1
  18. package/src/dap/session.ts +8 -2
  19. package/src/discovery/claude-plugins.ts +61 -6
  20. package/src/discovery/codex.ts +2 -15
  21. package/src/discovery/gemini.ts +2 -15
  22. package/src/discovery/helpers.ts +40 -1
  23. package/src/discovery/opencode.ts +2 -15
  24. package/src/edit/apply-patch/index.ts +87 -0
  25. package/src/edit/apply-patch/parser.ts +174 -0
  26. package/src/edit/diff.ts +3 -14
  27. package/src/edit/index.ts +67 -3
  28. package/src/edit/modes/apply-patch.lark +19 -0
  29. package/src/edit/modes/apply-patch.ts +63 -0
  30. package/src/edit/modes/chunk.ts +6 -2
  31. package/src/edit/modes/hashline.ts +3 -3
  32. package/src/edit/modes/replace.ts +2 -13
  33. package/src/edit/read-file.ts +18 -0
  34. package/src/edit/renderer.ts +61 -33
  35. package/src/extensibility/extensions/compact-handler.ts +40 -0
  36. package/src/extensibility/extensions/runner.ts +11 -29
  37. package/src/extensibility/utils.ts +7 -1
  38. package/src/internal-urls/docs-index.generated.ts +9 -2
  39. package/src/lsp/client.ts +14 -5
  40. package/src/lsp/index.ts +53 -10
  41. package/src/lsp/render.ts +14 -2
  42. package/src/lsp/types.ts +2 -0
  43. package/src/main.ts +1 -0
  44. package/src/mcp/manager.ts +29 -48
  45. package/src/memories/index.ts +7 -1
  46. package/src/modes/acp/acp-agent.ts +3 -16
  47. package/src/modes/components/model-selector.ts +15 -24
  48. package/src/modes/components/plugin-settings.ts +16 -5
  49. package/src/modes/components/read-tool-group.ts +92 -9
  50. package/src/modes/components/settings-defs.ts +18 -0
  51. package/src/modes/components/settings-selector.ts +2 -6
  52. package/src/modes/components/tool-execution.ts +61 -28
  53. package/src/modes/controllers/event-controller.ts +3 -1
  54. package/src/modes/controllers/extension-ui-controller.ts +99 -150
  55. package/src/modes/controllers/selector-controller.ts +3 -12
  56. package/src/modes/interactive-mode.ts +4 -2
  57. package/src/modes/print-mode.ts +4 -22
  58. package/src/modes/rpc/rpc-mode.ts +18 -38
  59. package/src/modes/shared.ts +10 -1
  60. package/src/modes/utils/ui-helpers.ts +6 -2
  61. package/src/plan-mode/approved-plan.ts +5 -4
  62. package/src/prompts/system/subagent-system-prompt.md +4 -4
  63. package/src/prompts/system/subagent-user-prompt.md +2 -2
  64. package/src/prompts/system/system-prompt.md +208 -243
  65. package/src/prompts/tools/apply-patch.md +67 -0
  66. package/src/prompts/tools/ast-edit.md +18 -23
  67. package/src/prompts/tools/ast-grep.md +25 -32
  68. package/src/prompts/tools/bash.md +11 -23
  69. package/src/prompts/tools/debug.md +8 -22
  70. package/src/prompts/tools/find.md +0 -4
  71. package/src/prompts/tools/grep.md +3 -5
  72. package/src/prompts/tools/hashline.md +16 -10
  73. package/src/prompts/tools/python.md +10 -14
  74. package/src/prompts/tools/read.md +17 -24
  75. package/src/prompts/tools/task.md +57 -21
  76. package/src/prompts/tools/todo-write.md +45 -67
  77. package/src/session/agent-session.ts +4 -4
  78. package/src/session/session-manager.ts +15 -7
  79. package/src/session/streaming-output.ts +24 -0
  80. package/src/slash-commands/builtin-registry.ts +3 -14
  81. package/src/task/executor.ts +13 -34
  82. package/src/task/index.ts +82 -18
  83. package/src/task/simple-mode.ts +27 -0
  84. package/src/task/template.ts +17 -3
  85. package/src/task/types.ts +77 -30
  86. package/src/tools/ask.ts +2 -4
  87. package/src/tools/ast-edit.ts +41 -17
  88. package/src/tools/ast-grep.ts +8 -27
  89. package/src/tools/bash-skill-urls.ts +9 -7
  90. package/src/tools/bash.ts +66 -24
  91. package/src/tools/browser.ts +1 -1
  92. package/src/tools/fetch.ts +1 -14
  93. package/src/tools/file-recorder.ts +35 -0
  94. package/src/tools/find.ts +25 -29
  95. package/src/tools/gh-format.ts +12 -0
  96. package/src/tools/gh-renderer.ts +1 -8
  97. package/src/tools/gh.ts +6 -13
  98. package/src/tools/grep.ts +103 -59
  99. package/src/tools/jtd-to-json-schema.ts +16 -0
  100. package/src/tools/match-line-format.ts +20 -0
  101. package/src/tools/path-utils.ts +61 -5
  102. package/src/tools/plan-mode-guard.ts +6 -5
  103. package/src/tools/python.ts +1 -1
  104. package/src/tools/read.ts +1 -1
  105. package/src/tools/render-utils.ts +38 -6
  106. package/src/tools/renderers.ts +1 -0
  107. package/src/tools/resolve.ts +12 -3
  108. package/src/tools/ssh.ts +3 -11
  109. package/src/tools/submit-result.ts +1 -13
  110. package/src/tools/todo-write.ts +137 -103
  111. package/src/tools/vim.ts +1 -1
  112. package/src/tools/write.ts +2 -23
  113. package/src/tui/code-cell.ts +12 -7
  114. package/src/utils/edit-mode.ts +3 -2
  115. package/src/utils/git.ts +1 -1
  116. package/src/vim/engine.ts +41 -58
  117. package/src/web/scrapers/crates-io.ts +1 -14
  118. package/src/web/scrapers/types.ts +13 -0
  119. package/src/web/search/providers/base.ts +13 -0
  120. package/src/web/search/providers/brave.ts +2 -5
  121. package/src/web/search/providers/codex.ts +20 -24
  122. package/src/web/search/providers/gemini.ts +39 -1
  123. package/src/web/search/providers/jina.ts +2 -5
  124. package/src/web/search/providers/kagi.ts +3 -8
  125. package/src/web/search/providers/kimi.ts +3 -7
  126. package/src/web/search/providers/parallel.ts +3 -8
  127. package/src/web/search/providers/synthetic.ts +3 -7
  128. package/src/web/search/providers/tavily.ts +15 -11
  129. package/src/web/search/providers/utils.ts +36 -0
  130. package/src/web/search/providers/zai.ts +3 -7
@@ -25,7 +25,7 @@ import { getCurrentThemeName, getSelectListTheme, getSettingsListTheme, theme }
25
25
  import { matchesAppInterrupt } from "../../modes/utils/keybinding-matchers";
26
26
  import { getTabBarTheme } from "../shared";
27
27
  import { DynamicBorder } from "./dynamic-border";
28
- import { PluginSettingsComponent } from "./plugin-settings";
28
+ import { handleInputOrEscape, PluginSettingsComponent } from "./plugin-settings";
29
29
  import { getSettingsForTab, type SettingDef } from "./settings-defs";
30
30
  import { getPreset } from "./status-line/presets";
31
31
 
@@ -70,11 +70,7 @@ class TextInputSubmenu extends Container {
70
70
  }
71
71
 
72
72
  handleInput(data: string): void {
73
- if (data === "\x1b" || data === "\x1b\x1b") {
74
- this.onCancel();
75
- return;
76
- }
77
- this.#input.handleInput(data);
73
+ handleInputOrEscape(data, this.#input, this.onCancel);
78
74
  }
79
75
  }
80
76
 
@@ -14,7 +14,14 @@ import {
14
14
  type TUI,
15
15
  } from "@oh-my-pi/pi-tui";
16
16
  import { getProjectDir, logger } from "@oh-my-pi/pi-utils";
17
- import { computeEditDiff, computeHashlineDiff, computePatchDiff, type DiffError, type DiffResult } from "../../edit";
17
+ import {
18
+ computeEditDiff,
19
+ computeHashlineDiff,
20
+ computePatchDiff,
21
+ type DiffError,
22
+ type DiffResult,
23
+ expandApplyPatchToEntries,
24
+ } from "../../edit";
18
25
  import type { Theme } from "../../modes/theme/theme";
19
26
  import { theme } from "../../modes/theme/theme";
20
27
  import { BASH_DEFAULT_PREVIEW_LINES } from "../../tools/bash";
@@ -54,6 +61,10 @@ function cloneToolArgs<T>(args: T): T {
54
61
  }
55
62
  }
56
63
 
64
+ function isEditLikeToolName(toolName: string): boolean {
65
+ return toolName === "edit" || toolName === "apply_patch";
66
+ }
67
+
57
68
  export interface ToolExecutionOptions {
58
69
  showImages?: boolean; // default: true (only used if terminal supports images)
59
70
  editFuzzyThreshold?: number;
@@ -166,7 +177,7 @@ export class ToolExecutionComponent extends Container {
166
177
 
167
178
  /**
168
179
  * Signal that args are complete (tool is about to execute).
169
- * This triggers diff computation for edit tool.
180
+ * This triggers diff computation for edit-like tools.
170
181
  */
171
182
  setArgsComplete(_toolCallId?: string): void {
172
183
  this.#argsComplete = true;
@@ -179,10 +190,39 @@ export class ToolExecutionComponent extends Container {
179
190
  * This runs async and updates display when done.
180
191
  */
181
192
  #maybeComputeEditDiff(): void {
182
- if (this.#toolName !== "edit") return;
193
+ if (!isEditLikeToolName(this.#toolName)) return;
183
194
 
184
195
  const edits = this.#args?.edits;
185
- if (!Array.isArray(edits) || edits.length === 0) return;
196
+ if (!Array.isArray(edits) || edits.length === 0) {
197
+ if (this.#toolName !== "apply_patch" || typeof this.#args?.input !== "string") {
198
+ return;
199
+ }
200
+
201
+ const input = this.#args.input;
202
+ const argsKey = JSON.stringify({ input });
203
+ if (this.#editDiffArgsKey === argsKey) return;
204
+ this.#editDiffArgsKey = argsKey;
205
+
206
+ try {
207
+ const first = expandApplyPatchToEntries({ input })[0];
208
+ if (!first?.path) return;
209
+ computePatchDiff({ ...first, op: first.op ?? "update" }, this.#cwd, {
210
+ fuzzyThreshold: this.#editFuzzyThreshold,
211
+ allowFuzzy: this.#editAllowFuzzy,
212
+ }).then(result => {
213
+ if (this.#editDiffArgsKey === argsKey) {
214
+ this.#editDiffPreview = result;
215
+ this.#updateDisplay();
216
+ this.#ui.requestRender();
217
+ }
218
+ });
219
+ } catch (err) {
220
+ this.#editDiffPreview = { error: err instanceof Error ? err.message : String(err) };
221
+ this.#updateDisplay();
222
+ this.#ui.requestRender();
223
+ }
224
+ return;
225
+ }
186
226
 
187
227
  const first = edits[0];
188
228
  if (!first || typeof first !== "object") return;
@@ -197,13 +237,9 @@ export class ToolExecutionComponent extends Container {
197
237
  if (this.#editDiffArgsKey === argsKey) return;
198
238
  this.#editDiffArgsKey = argsKey;
199
239
 
200
- computeEditDiff(path, oldText, newText, this.#cwd, true, all, this.#editFuzzyThreshold).then(result => {
201
- if (this.#editDiffArgsKey === argsKey) {
202
- this.#editDiffPreview = result;
203
- this.#updateDisplay();
204
- this.#ui.requestRender();
205
- }
206
- });
240
+ computeEditDiff(path, oldText, newText, this.#cwd, true, all, this.#editFuzzyThreshold).then(result =>
241
+ this.#applyEditDiffResult(argsKey, result),
242
+ );
207
243
  } else if ("path" in first && ("diff" in first || ("op" in first && !("content" in first)))) {
208
244
  // Patch mode (has diff or op without content — chunk edits always have content)
209
245
  const { path, op, rename, diff } = first;
@@ -216,13 +252,7 @@ export class ToolExecutionComponent extends Container {
216
252
  computePatchDiff({ path, op, rename, diff }, this.#cwd, {
217
253
  fuzzyThreshold: this.#editFuzzyThreshold,
218
254
  allowFuzzy: this.#editAllowFuzzy,
219
- }).then(result => {
220
- if (this.#editDiffArgsKey === argsKey) {
221
- this.#editDiffPreview = result;
222
- this.#updateDisplay();
223
- this.#ui.requestRender();
224
- }
225
- });
255
+ }).then(result => this.#applyEditDiffResult(argsKey, result));
226
256
  } else if ("loc" in first && "path" in first) {
227
257
  // Hashline mode — group edits by path, preview first file
228
258
  const path = first.path;
@@ -234,17 +264,20 @@ export class ToolExecutionComponent extends Container {
234
264
  if (this.#editDiffArgsKey === argsKey) return;
235
265
  this.#editDiffArgsKey = argsKey;
236
266
 
237
- computeHashlineDiff({ path, edits: fileEdits, move }, this.#cwd).then(result => {
238
- if (this.#editDiffArgsKey === argsKey) {
239
- this.#editDiffPreview = result;
240
- this.#updateDisplay();
241
- this.#ui.requestRender();
242
- }
243
- });
267
+ computeHashlineDiff({ path, edits: fileEdits, move }, this.#cwd).then(result =>
268
+ this.#applyEditDiffResult(argsKey, result),
269
+ );
244
270
  }
245
271
  // Chunk mode edits don't have a pre-execution diff preview
246
272
  }
247
273
 
274
+ #applyEditDiffResult(argsKey: string, result: DiffResult | DiffError): void {
275
+ if (this.#editDiffArgsKey !== argsKey) return;
276
+ this.#editDiffPreview = result;
277
+ this.#updateDisplay();
278
+ this.#ui.requestRender();
279
+ }
280
+
248
281
  updateResult(
249
282
  result: {
250
283
  content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;
@@ -316,7 +349,7 @@ export class ToolExecutionComponent extends Container {
316
349
  */
317
350
  #updateSpinnerAnimation(): void {
318
351
  // Spinner for: task tool with partial result, or edit/write while args streaming
319
- const isStreamingArgs = !this.#argsComplete && (this.#toolName === "edit" || this.#toolName === "write");
352
+ const isStreamingArgs = !this.#argsComplete && (isEditLikeToolName(this.#toolName) || this.#toolName === "write");
320
353
  const isBackgroundAsyncTask =
321
354
  this.#toolName === "task" &&
322
355
  (this.#result?.details as { async?: { state?: string } } | undefined)?.async?.state === "running";
@@ -610,7 +643,7 @@ export class ToolExecutionComponent extends Container {
610
643
  }
611
644
 
612
645
  #getCallArgsForRender(): any {
613
- if (this.#toolName !== "edit") {
646
+ if (!isEditLikeToolName(this.#toolName)) {
614
647
  return this.#args;
615
648
  }
616
649
  if (!this.#editDiffPreview || !("diff" in this.#editDiffPreview) || !this.#editDiffPreview.diff) {
@@ -646,7 +679,7 @@ export class ToolExecutionComponent extends Container {
646
679
  context.expanded = this.#expanded;
647
680
  context.previewLines = PYTHON_DEFAULT_PREVIEW_LINES;
648
681
  context.timeout = normalizeTimeoutSeconds(this.#args?.timeout, 600);
649
- } else if (this.#toolName === "edit") {
682
+ } else if (isEditLikeToolName(this.#toolName)) {
650
683
  // Edit needs diff preview and renderDiff function
651
684
  context.editDiffPreview = this.#editDiffPreview;
652
685
  context.renderDiff = renderDiff;
@@ -66,7 +66,9 @@ export class EventController {
66
66
  #getReadGroup(): ReadToolGroupComponent {
67
67
  if (!this.#lastReadGroup) {
68
68
  this.ctx.chatContainer.addChild(new Text("", 0, 0));
69
- const group = new ReadToolGroupComponent();
69
+ const group = new ReadToolGroupComponent({
70
+ showContentPreview: this.ctx.settings.get("read.toolResultPreview"),
71
+ });
70
72
  group.setExpanded(this.ctx.toolOutputExpanded);
71
73
  this.ctx.chatContainer.addChild(group);
72
74
  this.#lastReadGroup = group;
@@ -3,6 +3,7 @@ import { Container, Spacer, Text } from "@oh-my-pi/pi-tui";
3
3
  import { logger } from "@oh-my-pi/pi-utils";
4
4
  import { KeybindingsManager } from "../../config/keybindings";
5
5
  import type {
6
+ CompactOptions,
6
7
  ExtensionActions,
7
8
  ExtensionCommandContextActions,
8
9
  ExtensionContextActions,
@@ -12,6 +13,7 @@ import type {
12
13
  ExtensionUiComponent,
13
14
  ExtensionWidgetContent,
14
15
  ExtensionWidgetOptions,
16
+ SendUserMessageHandler,
15
17
  TerminalInputHandler,
16
18
  } from "../../extensibility/extensions";
17
19
  import { HookEditorComponent } from "../../modes/components/hook-editor";
@@ -82,26 +84,14 @@ export class ExtensionUiController {
82
84
  const wasStreaming = this.ctx.session.isStreaming;
83
85
  this.ctx.session
84
86
  .sendCustomMessage(message, options)
85
- .then(() => {
86
- // For non-streaming cases with display=true, update UI
87
- // (streaming cases update via message_end event)
88
- if (!this.ctx.isBackgrounded && !wasStreaming && message.display) {
89
- this.ctx.rebuildChatFromMessages();
90
- }
91
- })
87
+ .then(() => this.#applyCustomMessageDisplay(wasStreaming, message.display))
92
88
  .catch((err: unknown) => {
93
89
  this.ctx.showError(
94
90
  `Extension sendMessage failed: ${err instanceof Error ? err.message : String(err)}`,
95
91
  );
96
92
  });
97
93
  },
98
- sendUserMessage: (content, options) => {
99
- this.ctx.session.sendUserMessage(content, options).catch((err: unknown) => {
100
- this.ctx.showError(
101
- `Extension sendUserMessage failed: ${err instanceof Error ? err.message : String(err)}`,
102
- );
103
- });
104
- },
94
+ sendUserMessage: this.#sendExtensionUserMessage,
105
95
  appendEntry: (customType, data) => {
106
96
  this.ctx.sessionManager.appendCustomEntry(customType, data);
107
97
  },
@@ -121,14 +111,7 @@ export class ExtensionUiController {
121
111
  setThinkingLevel: level => this.ctx.session.setThinkingLevel(level),
122
112
  getCommands: () => [],
123
113
  getSessionName: () => this.ctx.sessionManager.getSessionName(),
124
- setSessionName: async name => {
125
- await this.ctx.sessionManager.setSessionName(name, "user");
126
- setSessionTerminalTitle(
127
- this.ctx.sessionManager.getSessionName(),
128
- this.ctx.sessionManager.getCwd(),
129
- this.ctx.sessionManager.titleSource,
130
- );
131
- },
114
+ setSessionName: name => this.#updateSessionName(name),
132
115
  };
133
116
  const contextActions: ExtensionContextActions = {
134
117
  getModel: () => this.ctx.session.model,
@@ -139,12 +122,7 @@ export class ExtensionUiController {
139
122
  // Signal shutdown request (will be handled by main loop)
140
123
  },
141
124
  getContextUsage: () => this.ctx.session.getContextUsage(),
142
- compact: async instructionsOrOptions => {
143
- const instructions = typeof instructionsOrOptions === "string" ? instructionsOrOptions : undefined;
144
- const options =
145
- instructionsOrOptions && typeof instructionsOrOptions === "object" ? instructionsOrOptions : undefined;
146
- await this.ctx.session.compact(instructions, options);
147
- },
125
+ compact: instructionsOrOptions => this.#compactSession(instructionsOrOptions),
148
126
  getSystemPrompt: () => this.ctx.session.systemPrompt,
149
127
  };
150
128
  const commandActions: ExtensionCommandContextActions = {
@@ -238,16 +216,7 @@ export class ExtensionUiController {
238
216
 
239
217
  return { cancelled: false };
240
218
  },
241
- compact: async instructionsOrOptions => {
242
- const instructions = typeof instructionsOrOptions === "string" ? instructionsOrOptions : undefined;
243
- const options =
244
- instructionsOrOptions && typeof instructionsOrOptions === "object" ? instructionsOrOptions : undefined;
245
- if (this.ctx.isBackgrounded) {
246
- await this.ctx.session.compact(instructions, options);
247
- return;
248
- }
249
- await this.ctx.executeCompaction(instructionsOrOptions, false);
250
- },
219
+ compact: async instructionsOrOptions => this.#handleInteractiveCompact(instructionsOrOptions),
251
220
  switchSession: async sessionPath => {
252
221
  this.clearHookWidgets();
253
222
  const result = await this.ctx.session.switchSession(sessionPath);
@@ -357,13 +326,7 @@ export class ExtensionUiController {
357
326
  const wasStreaming = this.ctx.session.isStreaming;
358
327
  this.ctx.session
359
328
  .sendCustomMessage(message, options)
360
- .then(() => {
361
- // For non-streaming cases with display=true, update UI
362
- // (streaming cases update via message_end event)
363
- if (!this.ctx.isBackgrounded && !wasStreaming && message.display) {
364
- this.ctx.rebuildChatFromMessages();
365
- }
366
- })
329
+ .then(() => this.#applyCustomMessageDisplay(wasStreaming, message.display))
367
330
  .catch((err: unknown) => {
368
331
  const errorText = `Extension sendMessage failed: ${err instanceof Error ? err.message : String(err)}`;
369
332
  if (this.ctx.isBackgrounded) {
@@ -373,13 +336,7 @@ export class ExtensionUiController {
373
336
  this.ctx.showError(errorText);
374
337
  });
375
338
  },
376
- sendUserMessage: (content, options) => {
377
- this.ctx.session.sendUserMessage(content, options).catch((err: unknown) => {
378
- this.ctx.showError(
379
- `Extension sendUserMessage failed: ${err instanceof Error ? err.message : String(err)}`,
380
- );
381
- });
382
- },
339
+ sendUserMessage: this.#sendExtensionUserMessage,
383
340
  appendEntry: (customType, data) => {
384
341
  this.ctx.sessionManager.appendCustomEntry(customType, data);
385
342
  },
@@ -399,14 +356,7 @@ export class ExtensionUiController {
399
356
  setThinkingLevel: (level, persist) => this.ctx.session.setThinkingLevel(level, persist),
400
357
  getCommands: () => [],
401
358
  getSessionName: () => this.ctx.sessionManager.getSessionName(),
402
- setSessionName: async name => {
403
- await this.ctx.sessionManager.setSessionName(name, "user");
404
- setSessionTerminalTitle(
405
- this.ctx.sessionManager.getSessionName(),
406
- this.ctx.sessionManager.getCwd(),
407
- this.ctx.sessionManager.titleSource,
408
- );
409
- },
359
+ setSessionName: name => this.#updateSessionName(name),
410
360
  };
411
361
  const contextActions: ExtensionContextActions = {
412
362
  getModel: () => this.ctx.session.model,
@@ -417,12 +367,7 @@ export class ExtensionUiController {
417
367
  // Signal shutdown request (will be handled by main loop)
418
368
  },
419
369
  getContextUsage: () => this.ctx.session.getContextUsage(),
420
- compact: async instructionsOrOptions => {
421
- const instructions = typeof instructionsOrOptions === "string" ? instructionsOrOptions : undefined;
422
- const options =
423
- instructionsOrOptions && typeof instructionsOrOptions === "object" ? instructionsOrOptions : undefined;
424
- await this.ctx.session.compact(instructions, options);
425
- },
370
+ compact: instructionsOrOptions => this.#compactSession(instructionsOrOptions),
426
371
  getSystemPrompt: () => this.ctx.session.systemPrompt,
427
372
  };
428
373
  const commandActions: ExtensionCommandContextActions = {
@@ -517,16 +462,7 @@ export class ExtensionUiController {
517
462
 
518
463
  return { cancelled: false };
519
464
  },
520
- compact: async instructionsOrOptions => {
521
- const instructions = typeof instructionsOrOptions === "string" ? instructionsOrOptions : undefined;
522
- const options =
523
- instructionsOrOptions && typeof instructionsOrOptions === "object" ? instructionsOrOptions : undefined;
524
- if (this.ctx.isBackgrounded) {
525
- await this.ctx.session.compact(instructions, options);
526
- return;
527
- }
528
- await this.ctx.executeCompaction(instructionsOrOptions, false);
529
- },
465
+ compact: async instructionsOrOptions => this.#handleInteractiveCompact(instructionsOrOptions),
530
466
  switchSession: async sessionPath => {
531
467
  if (this.ctx.isBackgrounded) {
532
468
  return { cancelled: true };
@@ -594,14 +530,7 @@ export class ExtensionUiController {
594
530
  await registeredTool.definition.onSession(event, {
595
531
  ui: uiContext,
596
532
  getContextUsage: () => this.ctx.session.getContextUsage(),
597
- compact: async instructionsOrOptions => {
598
- const instructions = typeof instructionsOrOptions === "string" ? instructionsOrOptions : undefined;
599
- const options =
600
- instructionsOrOptions && typeof instructionsOrOptions === "object"
601
- ? instructionsOrOptions
602
- : undefined;
603
- await this.ctx.session.compact(instructions, options);
604
- },
533
+ compact: instructionsOrOptions => this.#compactSession(instructionsOrOptions),
605
534
  hasUI: !this.ctx.isBackgrounded,
606
535
  cwd: this.ctx.sessionManager.getCwd(),
607
536
  sessionManager: this.ctx.session.sessionManager,
@@ -657,21 +586,10 @@ export class ExtensionUiController {
657
586
  options: string[],
658
587
  dialogOptions?: ExtensionUIDialogOptions,
659
588
  ): Promise<string | undefined> {
660
- const { promise, resolve } = Promise.withResolvers<string | undefined>();
661
- let settled = false;
662
- const onAbort = () => {
663
- this.hideHookSelector();
664
- if (!settled) {
665
- settled = true;
666
- resolve(undefined);
667
- }
668
- };
669
- const finish = (value: string | undefined) => {
670
- if (settled) return;
671
- settled = true;
672
- dialogOptions?.signal?.removeEventListener("abort", onAbort);
673
- resolve(value);
674
- };
589
+ const { promise, finish, attachAbort } = this.#createHookDialogState(
590
+ () => this.hideHookSelector(),
591
+ dialogOptions?.signal,
592
+ );
675
593
  const maxVisible = Math.max(4, Math.min(15, this.ctx.ui.terminal.rows - 12));
676
594
  this.ctx.hookSelector = new HookSelectorComponent(
677
595
  title,
@@ -713,13 +631,7 @@ export class ExtensionUiController {
713
631
  this.ctx.editorContainer.addChild(this.ctx.hookSelector);
714
632
  this.ctx.ui.setFocus(this.ctx.hookSelector);
715
633
  this.ctx.ui.requestRender();
716
- if (dialogOptions?.signal) {
717
- if (dialogOptions.signal.aborted) {
718
- onAbort();
719
- } else {
720
- dialogOptions.signal.addEventListener("abort", onAbort, { once: true });
721
- }
722
- }
634
+ attachAbort();
723
635
  return promise;
724
636
  }
725
637
  /**
@@ -750,21 +662,10 @@ export class ExtensionUiController {
750
662
  placeholder?: string,
751
663
  dialogOptions?: ExtensionUIDialogOptions,
752
664
  ): Promise<string | undefined> {
753
- const { promise, resolve } = Promise.withResolvers<string | undefined>();
754
- let settled = false;
755
- const onAbort = () => {
756
- this.hideHookInput();
757
- if (!settled) {
758
- settled = true;
759
- resolve(undefined);
760
- }
761
- };
762
- const finish = (value: string | undefined) => {
763
- if (settled) return;
764
- settled = true;
765
- dialogOptions?.signal?.removeEventListener("abort", onAbort);
766
- resolve(value);
767
- };
665
+ const { promise, finish, attachAbort } = this.#createHookDialogState(
666
+ () => this.hideHookInput(),
667
+ dialogOptions?.signal,
668
+ );
768
669
  this.ctx.hookInput = new HookInputComponent(
769
670
  title,
770
671
  placeholder,
@@ -786,13 +687,7 @@ export class ExtensionUiController {
786
687
  this.ctx.editorContainer.addChild(this.ctx.hookInput);
787
688
  this.ctx.ui.setFocus(this.ctx.hookInput);
788
689
  this.ctx.ui.requestRender();
789
- if (dialogOptions?.signal) {
790
- if (dialogOptions.signal.aborted) {
791
- onAbort();
792
- } else {
793
- dialogOptions.signal.addEventListener("abort", onAbort, { once: true });
794
- }
795
- }
690
+ attachAbort();
796
691
  return promise;
797
692
  }
798
693
 
@@ -817,21 +712,10 @@ export class ExtensionUiController {
817
712
  dialogOptions?: ExtensionUIDialogOptions,
818
713
  editorOptions?: { promptStyle?: boolean },
819
714
  ): Promise<string | undefined> {
820
- const { promise, resolve } = Promise.withResolvers<string | undefined>();
821
- let settled = false;
822
- const onAbort = () => {
823
- this.hideHookEditor();
824
- if (!settled) {
825
- settled = true;
826
- resolve(undefined);
827
- }
828
- };
829
- const finish = (value: string | undefined) => {
830
- if (settled) return;
831
- settled = true;
832
- dialogOptions?.signal?.removeEventListener("abort", onAbort);
833
- resolve(value);
834
- };
715
+ const { promise, finish, attachAbort } = this.#createHookDialogState(
716
+ () => this.hideHookEditor(),
717
+ dialogOptions?.signal,
718
+ );
835
719
  this.ctx.hookEditor = new HookEditorComponent(
836
720
  this.ctx.ui,
837
721
  title,
@@ -851,13 +735,7 @@ export class ExtensionUiController {
851
735
  this.ctx.editorContainer.addChild(this.ctx.hookEditor);
852
736
  this.ctx.ui.setFocus(this.ctx.hookEditor);
853
737
  this.ctx.ui.requestRender();
854
- if (dialogOptions?.signal) {
855
- if (dialogOptions.signal.aborted) {
856
- onAbort();
857
- } else {
858
- dialogOptions.signal.addEventListener("abort", onAbort, { once: true });
859
- }
860
- }
738
+ attachAbort();
861
739
  return promise;
862
740
  }
863
741
 
@@ -980,4 +858,75 @@ export class ExtensionUiController {
980
858
  this.ctx.chatContainer.addChild(errorText);
981
859
  this.ctx.ui.requestRender();
982
860
  }
861
+ async #handleInteractiveCompact(instructionsOrOptions: string | CompactOptions | undefined): Promise<void> {
862
+ if (this.ctx.isBackgrounded) {
863
+ await this.#compactSession(instructionsOrOptions);
864
+ return;
865
+ }
866
+ await this.ctx.executeCompaction(instructionsOrOptions, false);
867
+ }
868
+
869
+ async #compactSession(instructionsOrOptions: string | CompactOptions | undefined): Promise<void> {
870
+ const instructions = typeof instructionsOrOptions === "string" ? instructionsOrOptions : undefined;
871
+ const options =
872
+ instructionsOrOptions && typeof instructionsOrOptions === "object" ? instructionsOrOptions : undefined;
873
+ await this.ctx.session.compact(instructions, options);
874
+ }
875
+
876
+ async #updateSessionName(name: string): Promise<void> {
877
+ await this.ctx.sessionManager.setSessionName(name, "user");
878
+ setSessionTerminalTitle(
879
+ this.ctx.sessionManager.getSessionName(),
880
+ this.ctx.sessionManager.getCwd(),
881
+ this.ctx.sessionManager.titleSource,
882
+ );
883
+ }
884
+
885
+ #sendExtensionUserMessage: SendUserMessageHandler = (content, options) => {
886
+ this.ctx.session.sendUserMessage(content, options).catch((err: unknown) => {
887
+ this.ctx.showError(`Extension sendUserMessage failed: ${err instanceof Error ? err.message : String(err)}`);
888
+ });
889
+ };
890
+
891
+ #applyCustomMessageDisplay(wasStreaming: boolean, shouldDisplay: boolean | undefined): void {
892
+ // For non-streaming cases with display=true, update UI
893
+ // (streaming cases update via message_end event)
894
+ if (!this.ctx.isBackgrounded && !wasStreaming && shouldDisplay) {
895
+ this.ctx.rebuildChatFromMessages();
896
+ }
897
+ }
898
+
899
+ #createHookDialogState(
900
+ hide: () => void,
901
+ signal: AbortSignal | undefined,
902
+ ): {
903
+ promise: Promise<string | undefined>;
904
+ finish: (value: string | undefined) => void;
905
+ attachAbort: () => void;
906
+ } {
907
+ const { promise, resolve } = Promise.withResolvers<string | undefined>();
908
+ let settled = false;
909
+ const onAbort = () => {
910
+ hide();
911
+ if (!settled) {
912
+ settled = true;
913
+ resolve(undefined);
914
+ }
915
+ };
916
+ const finish = (value: string | undefined) => {
917
+ if (settled) return;
918
+ settled = true;
919
+ signal?.removeEventListener("abort", onAbort);
920
+ resolve(value);
921
+ };
922
+ const attachAbort = () => {
923
+ if (!signal) return;
924
+ if (signal.aborted) {
925
+ onAbort();
926
+ } else {
927
+ signal.addEventListener("abort", onAbort, { once: true });
928
+ }
929
+ };
930
+ return { promise, finish, attachAbort };
931
+ }
983
932
  }
@@ -1,17 +1,14 @@
1
- import * as os from "node:os";
2
- import * as path from "node:path";
3
1
  import { ThinkingLevel } from "@oh-my-pi/pi-agent-core";
4
2
  import { getOAuthProviders, type OAuthProvider } from "@oh-my-pi/pi-ai";
5
3
  import type { Component } from "@oh-my-pi/pi-tui";
6
4
  import { Input, Loader, Spacer, Text } from "@oh-my-pi/pi-tui";
7
- import { getAgentDbPath, getConfigDirName, getProjectDir } from "@oh-my-pi/pi-utils";
8
- import { invalidate as invalidateFsCache } from "../../capability/fs";
5
+ import { getAgentDbPath, getProjectDir } from "@oh-my-pi/pi-utils";
9
6
  import { getRoleInfo } from "../../config/model-registry";
10
7
  import { formatModelSelectorValue } from "../../config/model-resolver";
11
8
  import { settings } from "../../config/settings";
12
9
  import { DebugSelectorComponent } from "../../debug";
13
10
  import { disableProvider, enableProvider } from "../../discovery";
14
- import { clearClaudePluginRootsCache, resolveActiveProjectRegistryPath } from "../../discovery/helpers";
11
+ import { clearPluginRootsAndCaches, resolveActiveProjectRegistryPath } from "../../discovery/helpers";
15
12
  import {
16
13
  getInstalledPluginsRegistryPath,
17
14
  getMarketplacesCacheDir,
@@ -443,13 +440,7 @@ export class SelectorController {
443
440
  projectInstalledRegistryPath: (await resolveActiveProjectRegistryPath(getProjectDir())) ?? undefined,
444
441
  marketplacesCacheDir: getMarketplacesCacheDir(),
445
442
  pluginsCacheDir: getPluginsCacheDir(),
446
- clearPluginRootsCache: (extraPaths?: readonly string[]) => {
447
- const home = os.homedir();
448
- invalidateFsCache(path.join(home, ".claude", "plugins", "installed_plugins.json"));
449
- invalidateFsCache(path.join(home, getConfigDirName(), "plugins", "installed_plugins.json"));
450
- for (const p of extraPaths ?? []) invalidateFsCache(p);
451
- clearClaudePluginRootsCache();
452
- },
443
+ clearPluginRootsCache: clearPluginRootsAndCaches,
453
444
  });
454
445
 
455
446
  const [marketplaces, installed] = await Promise.all([mgr.listMarketplaces(), mgr.listInstalledPlugins()]);
@@ -37,6 +37,7 @@ import type { SessionContext, SessionManager } from "../session/session-manager"
37
37
  import { getRecentSessions } from "../session/session-manager";
38
38
  import { STTController, type SttState } from "../stt";
39
39
  import type { ExitPlanModeDetails, LspStartupServerInfo } from "../tools";
40
+ import { normalizeLocalScheme } from "../tools/path-utils";
40
41
  import type { EventBus } from "../utils/event-bus";
41
42
  import { getEditorCommand, openInEditor } from "../utils/external-editor";
42
43
  import { getSessionAccentAnsi, getSessionAccentHexForTitle } from "../utils/session-color";
@@ -637,8 +638,9 @@ export class InteractiveMode implements InteractiveModeContext {
637
638
  }
638
639
 
639
640
  #resolvePlanFilePath(planFilePath: string): string {
640
- if (planFilePath.startsWith("local://")) {
641
- return resolveLocalUrlToPath(planFilePath, {
641
+ if (planFilePath.startsWith("local:")) {
642
+ const normalized = normalizeLocalScheme(planFilePath);
643
+ return resolveLocalUrlToPath(normalized, {
642
644
  getArtifactsDir: () => this.sessionManager.getArtifactsDir(),
643
645
  getSessionId: () => this.sessionManager.getSessionId(),
644
646
  });