@oh-my-pi/pi-coding-agent 15.10.0 → 15.10.2

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 (238) hide show
  1. package/CHANGELOG.md +142 -1
  2. package/dist/types/cli/dry-balance-cli.d.ts +15 -1
  3. package/dist/types/cli/startup-cwd.d.ts +2 -0
  4. package/dist/types/commands/launch.d.ts +3 -0
  5. package/dist/types/commit/analysis/conventional.d.ts +2 -2
  6. package/dist/types/commit/analysis/summary.d.ts +2 -2
  7. package/dist/types/commit/changelog/generate.d.ts +2 -2
  8. package/dist/types/commit/changelog/index.d.ts +2 -2
  9. package/dist/types/commit/map-reduce/index.d.ts +3 -3
  10. package/dist/types/commit/map-reduce/map-phase.d.ts +2 -2
  11. package/dist/types/commit/map-reduce/reduce-phase.d.ts +2 -2
  12. package/dist/types/commit/model-selection.d.ts +10 -4
  13. package/dist/types/config/api-key-resolver.d.ts +34 -0
  14. package/dist/types/config/keybindings.d.ts +2 -2
  15. package/dist/types/config/model-provider-priority.d.ts +1 -0
  16. package/dist/types/config/model-registry.d.ts +17 -1
  17. package/dist/types/config/model-resolver.d.ts +4 -1
  18. package/dist/types/config/settings-schema.d.ts +9 -0
  19. package/dist/types/config/settings.d.ts +7 -2
  20. package/dist/types/dap/config.d.ts +14 -1
  21. package/dist/types/dap/types.d.ts +10 -0
  22. package/dist/types/debug/report-bundle.d.ts +3 -0
  23. package/dist/types/edit/file-snapshot-store.d.ts +18 -10
  24. package/dist/types/eval/py/__tests__/prelude.test.d.ts +1 -0
  25. package/dist/types/extensibility/extensions/types.d.ts +4 -1
  26. package/dist/types/lsp/client.d.ts +10 -0
  27. package/dist/types/lsp/utils.d.ts +3 -2
  28. package/dist/types/main.d.ts +3 -9
  29. package/dist/types/mcp/tool-bridge.d.ts +2 -0
  30. package/dist/types/modes/components/chat-block.d.ts +64 -0
  31. package/dist/types/modes/components/custom-editor.d.ts +4 -1
  32. package/dist/types/modes/components/overlay-box.d.ts +17 -0
  33. package/dist/types/modes/components/plan-review-overlay.d.ts +59 -0
  34. package/dist/types/modes/components/plan-toc.d.ts +41 -0
  35. package/dist/types/modes/components/read-tool-group.d.ts +2 -0
  36. package/dist/types/modes/components/status-line.d.ts +2 -0
  37. package/dist/types/modes/components/transcript-container.d.ts +11 -0
  38. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  39. package/dist/types/modes/controllers/event-controller.d.ts +17 -1
  40. package/dist/types/modes/controllers/extension-ui-controller.d.ts +0 -1
  41. package/dist/types/modes/controllers/input-controller.d.ts +1 -1
  42. package/dist/types/modes/controllers/streaming-reveal.d.ts +22 -0
  43. package/dist/types/modes/controllers/tan-command-controller.d.ts +6 -0
  44. package/dist/types/modes/interactive-mode.d.ts +16 -5
  45. package/dist/types/modes/magic-keywords.d.ts +1 -1
  46. package/dist/types/modes/markdown-prose.d.ts +1 -1
  47. package/dist/types/modes/theme/theme.d.ts +1 -1
  48. package/dist/types/modes/types.d.ts +21 -5
  49. package/dist/types/modes/utils/copy-targets.d.ts +21 -1
  50. package/dist/types/modes/workflow.d.ts +3 -3
  51. package/dist/types/plan-mode/approved-plan.d.ts +27 -8
  52. package/dist/types/plan-mode/plan-protection.d.ts +4 -4
  53. package/dist/types/sdk.d.ts +2 -0
  54. package/dist/types/session/agent-session.d.ts +21 -0
  55. package/dist/types/session/auth-storage.d.ts +1 -1
  56. package/dist/types/session/messages.d.ts +12 -0
  57. package/dist/types/session/session-manager.d.ts +8 -3
  58. package/dist/types/slash-commands/types.d.ts +4 -6
  59. package/dist/types/task/executor.d.ts +17 -0
  60. package/dist/types/task/index.d.ts +1 -0
  61. package/dist/types/task/render.d.ts +3 -2
  62. package/dist/types/tools/archive-reader.d.ts +5 -0
  63. package/dist/types/tools/ast-edit.d.ts +3 -0
  64. package/dist/types/tools/ast-grep.d.ts +3 -0
  65. package/dist/types/tools/bash.d.ts +1 -0
  66. package/dist/types/tools/eval.d.ts +8 -0
  67. package/dist/types/tools/find.d.ts +8 -4
  68. package/dist/types/tools/gh-cache-invalidation.d.ts +6 -0
  69. package/dist/types/tools/github-cache.d.ts +12 -0
  70. package/dist/types/tools/grouped-file-output.d.ts +95 -12
  71. package/dist/types/tools/memory-render.d.ts +4 -1
  72. package/dist/types/tools/path-utils.d.ts +8 -0
  73. package/dist/types/tools/plan-mode-guard.d.ts +8 -9
  74. package/dist/types/tools/render-utils.d.ts +5 -9
  75. package/dist/types/tools/search.d.ts +6 -2
  76. package/dist/types/tools/sqlite-reader.d.ts +1 -0
  77. package/dist/types/tools/todo.d.ts +3 -2
  78. package/dist/types/tools/write.d.ts +3 -0
  79. package/dist/types/tools/yield.d.ts +8 -0
  80. package/dist/types/tui/output-block.d.ts +16 -4
  81. package/dist/types/tui/status-line.d.ts +3 -0
  82. package/dist/types/utils/enhanced-paste.d.ts +20 -0
  83. package/dist/types/web/search/providers/kimi.d.ts +1 -1
  84. package/package.json +9 -9
  85. package/src/auto-thinking/classifier.ts +5 -1
  86. package/src/cli/args.ts +3 -1
  87. package/src/cli/dry-balance-cli.ts +54 -21
  88. package/src/cli/gallery-cli.ts +4 -1
  89. package/src/cli/gallery-fixtures/misc.ts +29 -0
  90. package/src/cli/startup-cwd.ts +68 -0
  91. package/src/commands/launch.ts +3 -0
  92. package/src/commit/analysis/conventional.ts +2 -2
  93. package/src/commit/analysis/summary.ts +2 -2
  94. package/src/commit/changelog/generate.ts +2 -2
  95. package/src/commit/changelog/index.ts +2 -2
  96. package/src/commit/map-reduce/index.ts +3 -3
  97. package/src/commit/map-reduce/map-phase.ts +2 -2
  98. package/src/commit/map-reduce/reduce-phase.ts +2 -2
  99. package/src/commit/model-selection.ts +36 -11
  100. package/src/commit/pipeline.ts +4 -4
  101. package/src/config/api-key-resolver.ts +58 -0
  102. package/src/config/model-provider-priority.ts +55 -0
  103. package/src/config/model-registry.ts +29 -24
  104. package/src/config/model-resolver.ts +39 -7
  105. package/src/config/settings-schema.ts +10 -0
  106. package/src/config/settings.ts +106 -43
  107. package/src/dap/config.ts +41 -2
  108. package/src/dap/defaults.json +1 -0
  109. package/src/dap/session.ts +1 -0
  110. package/src/dap/types.ts +10 -0
  111. package/src/debug/index.ts +47 -53
  112. package/src/debug/raw-sse-buffer.ts +7 -4
  113. package/src/debug/report-bundle.ts +9 -0
  114. package/src/edit/file-snapshot-store.ts +33 -1
  115. package/src/edit/hashline/filesystem.ts +2 -1
  116. package/src/edit/renderer.ts +82 -78
  117. package/src/eval/__tests__/llm-bridge.test.ts +110 -31
  118. package/src/eval/js/context-manager.ts +32 -15
  119. package/src/eval/llm-bridge.ts +22 -6
  120. package/src/eval/py/__tests__/prelude.test.ts +19 -0
  121. package/src/eval/py/executor.ts +23 -11
  122. package/src/eval/py/prelude.py +1 -1
  123. package/src/extensibility/extensions/types.ts +10 -1
  124. package/src/goals/tools/goal-tool.ts +36 -26
  125. package/src/internal-urls/docs-index.generated.ts +8 -8
  126. package/src/lsp/client.ts +23 -11
  127. package/src/lsp/config.ts +11 -1
  128. package/src/lsp/index.ts +61 -9
  129. package/src/lsp/utils.ts +3 -2
  130. package/src/main.ts +100 -72
  131. package/src/mcp/tool-bridge.ts +2 -0
  132. package/src/memories/index.ts +14 -7
  133. package/src/mnemopi/backend.ts +5 -1
  134. package/src/modes/acp/acp-agent.ts +33 -26
  135. package/src/modes/components/assistant-message.ts +2 -9
  136. package/src/modes/components/chat-block.ts +111 -0
  137. package/src/modes/components/copy-selector.ts +1 -44
  138. package/src/modes/components/custom-editor.ts +164 -109
  139. package/src/modes/components/custom-message.ts +1 -3
  140. package/src/modes/components/execution-shared.ts +1 -2
  141. package/src/modes/components/hook-message.ts +1 -3
  142. package/src/modes/components/model-selector.ts +59 -13
  143. package/src/modes/components/oauth-selector.ts +33 -7
  144. package/src/modes/components/overlay-box.ts +108 -0
  145. package/src/modes/components/plan-review-overlay.ts +799 -0
  146. package/src/modes/components/plan-toc.ts +138 -0
  147. package/src/modes/components/read-tool-group.ts +20 -4
  148. package/src/modes/components/skill-message.ts +0 -1
  149. package/src/modes/components/status-line.ts +19 -4
  150. package/src/modes/components/tips.txt +2 -1
  151. package/src/modes/components/todo-reminder.ts +0 -2
  152. package/src/modes/components/tool-execution.ts +68 -88
  153. package/src/modes/components/transcript-container.ts +84 -24
  154. package/src/modes/components/user-message.ts +2 -3
  155. package/src/modes/controllers/command-controller-shared.ts +7 -6
  156. package/src/modes/controllers/command-controller.ts +57 -55
  157. package/src/modes/controllers/event-controller.ts +67 -40
  158. package/src/modes/controllers/extension-ui-controller.ts +10 -73
  159. package/src/modes/controllers/input-controller.ts +170 -126
  160. package/src/modes/controllers/mcp-command-controller.ts +69 -60
  161. package/src/modes/controllers/selector-controller.ts +23 -25
  162. package/src/modes/controllers/streaming-reveal.ts +212 -0
  163. package/src/modes/controllers/tan-command-controller.ts +173 -0
  164. package/src/modes/interactive-mode.ts +274 -112
  165. package/src/modes/magic-keywords.ts +1 -1
  166. package/src/modes/markdown-prose.ts +1 -1
  167. package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
  168. package/src/modes/theme/shimmer.ts +20 -9
  169. package/src/modes/theme/theme-schema.json +1 -1
  170. package/src/modes/theme/theme.ts +8 -4
  171. package/src/modes/types.ts +21 -7
  172. package/src/modes/utils/copy-targets.ts +133 -27
  173. package/src/modes/utils/ui-helpers.ts +44 -46
  174. package/src/modes/workflow.ts +10 -10
  175. package/src/plan-mode/approved-plan.ts +66 -43
  176. package/src/plan-mode/plan-protection.ts +4 -4
  177. package/src/prompts/system/background-tan-dispatch.md +8 -0
  178. package/src/prompts/system/plan-mode-active.md +67 -58
  179. package/src/prompts/system/plan-mode-approved.md +1 -1
  180. package/src/prompts/system/workflow-notice.md +1 -1
  181. package/src/prompts/tools/bash.md +9 -0
  182. package/src/prompts/tools/browser.md +1 -1
  183. package/src/prompts/tools/eval.md +2 -1
  184. package/src/prompts/tools/read.md +2 -2
  185. package/src/sdk.ts +37 -46
  186. package/src/session/agent-session.ts +119 -18
  187. package/src/session/auth-storage.ts +2 -0
  188. package/src/session/messages.ts +26 -0
  189. package/src/session/session-manager.ts +109 -28
  190. package/src/slash-commands/builtin-registry.ts +36 -9
  191. package/src/slash-commands/types.ts +4 -6
  192. package/src/task/executor.ts +76 -38
  193. package/src/task/index.ts +4 -0
  194. package/src/task/render.ts +211 -147
  195. package/src/tools/archive-reader.ts +64 -0
  196. package/src/tools/ask.ts +119 -164
  197. package/src/tools/ast-edit.ts +98 -71
  198. package/src/tools/ast-grep.ts +37 -43
  199. package/src/tools/bash.ts +57 -6
  200. package/src/tools/browser/tab-supervisor.ts +13 -1
  201. package/src/tools/browser/tab-worker.ts +33 -4
  202. package/src/tools/debug.ts +20 -8
  203. package/src/tools/eval.ts +13 -2
  204. package/src/tools/fetch.ts +297 -7
  205. package/src/tools/find.ts +51 -30
  206. package/src/tools/gh-cache-invalidation.ts +200 -0
  207. package/src/tools/gh-renderer.ts +81 -42
  208. package/src/tools/github-cache.ts +25 -0
  209. package/src/tools/grouped-file-output.ts +272 -48
  210. package/src/tools/image-gen.ts +150 -103
  211. package/src/tools/inspect-image-renderer.ts +63 -41
  212. package/src/tools/inspect-image.ts +10 -3
  213. package/src/tools/job.ts +3 -4
  214. package/src/tools/memory-render.ts +4 -1
  215. package/src/tools/path-utils.ts +28 -2
  216. package/src/tools/plan-mode-guard.ts +66 -39
  217. package/src/tools/read.ts +48 -28
  218. package/src/tools/render-utils.ts +21 -37
  219. package/src/tools/resolve.ts +14 -0
  220. package/src/tools/search-tool-bm25.ts +36 -23
  221. package/src/tools/search.ts +118 -81
  222. package/src/tools/sqlite-reader.ts +9 -12
  223. package/src/tools/todo.ts +118 -52
  224. package/src/tools/write.ts +83 -64
  225. package/src/tools/yield.ts +10 -1
  226. package/src/tui/output-block.ts +60 -13
  227. package/src/tui/status-line.ts +5 -1
  228. package/src/utils/commit-message-generator.ts +11 -3
  229. package/src/utils/enhanced-paste.ts +230 -0
  230. package/src/utils/title-generator.ts +2 -1
  231. package/src/web/search/providers/anthropic.ts +25 -19
  232. package/src/web/search/providers/codex.ts +37 -8
  233. package/src/web/search/providers/exa.ts +11 -3
  234. package/src/web/search/providers/kimi.ts +28 -17
  235. package/src/web/search/providers/parallel.ts +35 -24
  236. package/src/web/search/providers/synthetic.ts +8 -6
  237. package/src/web/search/providers/tavily.ts +9 -8
  238. package/src/web/search/providers/zai.ts +8 -6
@@ -19,6 +19,7 @@ import {
19
19
  } from "@oh-my-pi/pi-tui";
20
20
  import { getSessionsDir } from "@oh-my-pi/pi-utils";
21
21
  import { DynamicBorder } from "../modes/components/dynamic-border";
22
+ import { TranscriptBlock } from "../modes/components/transcript-container";
22
23
  import { getSelectListTheme, getSymbolTheme, theme } from "../modes/theme/theme";
23
24
  import type { InteractiveModeContext } from "../modes/types";
24
25
  import { formatBytes } from "../tools/render-utils";
@@ -150,13 +151,13 @@ export class DebugSelectorComponent extends Container {
150
151
  }
151
152
 
152
153
  // Show message and wait for keypress
153
- this.ctx.chatContainer.addChild(new Spacer(1));
154
- this.ctx.chatContainer.addChild(new Text(theme.fg("accent", `${theme.status.info} CPU profiling started`), 1, 0));
155
- this.ctx.chatContainer.addChild(new Spacer(1));
156
- this.ctx.chatContainer.addChild(
154
+ const block = new TranscriptBlock();
155
+ block.addChild(new Text(theme.fg("accent", `${theme.status.info} CPU profiling started`), 1, 0));
156
+ block.addChild(new Spacer(1));
157
+ block.addChild(
157
158
  new Text(theme.fg("muted", "Reproduce the performance issue, then press Enter to stop profiling."), 1, 0),
158
159
  );
159
- this.ctx.ui.requestRender();
160
+ this.ctx.present(block);
160
161
 
161
162
  // Wait for Enter keypress
162
163
  const { promise, resolve } = Promise.withResolvers<void>();
@@ -194,6 +195,7 @@ export class DebugSelectorComponent extends Container {
194
195
  const result = await createReportBundle({
195
196
  sessionFile: this.ctx.sessionManager.getSessionFile(),
196
197
  settings: this.#getResolvedSettings(),
198
+ rawSseText: this.#getRawSseText(),
197
199
  cpuProfile,
198
200
  workProfile,
199
201
  });
@@ -201,19 +203,16 @@ export class DebugSelectorComponent extends Container {
201
203
  loader.stop();
202
204
  this.ctx.statusContainer.clear();
203
205
 
204
- this.ctx.chatContainer.addChild(new Spacer(1));
205
- this.ctx.chatContainer.addChild(
206
- new Text(theme.fg("success", `${theme.status.success} Performance report saved`), 1, 0),
207
- );
208
- this.ctx.chatContainer.addChild(new Text(theme.fg("dim", formatFileHyperlink(result.path)), 1, 0));
209
- this.ctx.chatContainer.addChild(new Text(theme.fg("dim", `Files: ${result.files.length}`), 1, 0));
206
+ const block = new TranscriptBlock();
207
+ block.addChild(new Text(theme.fg("success", `${theme.status.success} Performance report saved`), 1, 0));
208
+ block.addChild(new Text(theme.fg("dim", formatFileHyperlink(result.path)), 1, 0));
209
+ block.addChild(new Text(theme.fg("dim", `Files: ${result.files.length}`), 1, 0));
210
+ this.ctx.present(block);
210
211
  } catch (err) {
211
212
  loader.stop();
212
213
  this.ctx.statusContainer.clear();
213
214
  this.ctx.showError(`Failed to create report: ${err instanceof Error ? err.message : String(err)}`);
214
215
  }
215
-
216
- this.ctx.ui.requestRender();
217
216
  }
218
217
 
219
218
  async #handleWorkReport(): Promise<void> {
@@ -231,15 +230,13 @@ export class DebugSelectorComponent extends Container {
231
230
 
232
231
  openPath(tmpPath);
233
232
 
234
- this.ctx.chatContainer.addChild(new Spacer(1));
235
- this.ctx.chatContainer.addChild(
233
+ this.ctx.present([
234
+ new Spacer(1),
236
235
  new Text(theme.fg("dim", `Opened flamegraph (${workProfile.sampleCount} samples)`), 1, 0),
237
- );
236
+ ]);
238
237
  } catch (err) {
239
238
  this.ctx.showError(`Failed to open profile: ${err instanceof Error ? err.message : String(err)}`);
240
239
  }
241
-
242
- this.ctx.ui.requestRender();
243
240
  }
244
241
 
245
242
  async #handleDumpReport(): Promise<void> {
@@ -257,24 +254,22 @@ export class DebugSelectorComponent extends Container {
257
254
  const result = await createReportBundle({
258
255
  sessionFile: this.ctx.sessionManager.getSessionFile(),
259
256
  settings: this.#getResolvedSettings(),
257
+ rawSseText: this.#getRawSseText(),
260
258
  });
261
259
 
262
260
  loader.stop();
263
261
  this.ctx.statusContainer.clear();
264
262
 
265
- this.ctx.chatContainer.addChild(new Spacer(1));
266
- this.ctx.chatContainer.addChild(
267
- new Text(theme.fg("success", `${theme.status.success} Report bundle saved`), 1, 0),
268
- );
269
- this.ctx.chatContainer.addChild(new Text(theme.fg("dim", formatFileHyperlink(result.path)), 1, 0));
270
- this.ctx.chatContainer.addChild(new Text(theme.fg("dim", `Files: ${result.files.length}`), 1, 0));
263
+ const block = new TranscriptBlock();
264
+ block.addChild(new Text(theme.fg("success", `${theme.status.success} Report bundle saved`), 1, 0));
265
+ block.addChild(new Text(theme.fg("dim", formatFileHyperlink(result.path)), 1, 0));
266
+ block.addChild(new Text(theme.fg("dim", `Files: ${result.files.length}`), 1, 0));
267
+ this.ctx.present(block);
271
268
  } catch (err) {
272
269
  loader.stop();
273
270
  this.ctx.statusContainer.clear();
274
271
  this.ctx.showError(`Failed to create report: ${err instanceof Error ? err.message : String(err)}`);
275
272
  }
276
-
277
- this.ctx.ui.requestRender();
278
273
  }
279
274
 
280
275
  async #handleMemoryReport(): Promise<void> {
@@ -295,25 +290,23 @@ export class DebugSelectorComponent extends Container {
295
290
  const result = await createReportBundle({
296
291
  sessionFile: this.ctx.sessionManager.getSessionFile(),
297
292
  settings: this.#getResolvedSettings(),
293
+ rawSseText: this.#getRawSseText(),
298
294
  heapSnapshot,
299
295
  });
300
296
 
301
297
  loader.stop();
302
298
  this.ctx.statusContainer.clear();
303
299
 
304
- this.ctx.chatContainer.addChild(new Spacer(1));
305
- this.ctx.chatContainer.addChild(
306
- new Text(theme.fg("success", `${theme.status.success} Memory report saved`), 1, 0),
307
- );
308
- this.ctx.chatContainer.addChild(new Text(theme.fg("dim", formatFileHyperlink(result.path)), 1, 0));
309
- this.ctx.chatContainer.addChild(new Text(theme.fg("dim", `Files: ${result.files.length}`), 1, 0));
300
+ const block = new TranscriptBlock();
301
+ block.addChild(new Text(theme.fg("success", `${theme.status.success} Memory report saved`), 1, 0));
302
+ block.addChild(new Text(theme.fg("dim", formatFileHyperlink(result.path)), 1, 0));
303
+ block.addChild(new Text(theme.fg("dim", `Files: ${result.files.length}`), 1, 0));
304
+ this.ctx.present(block);
310
305
  } catch (err) {
311
306
  loader.stop();
312
307
  this.ctx.statusContainer.clear();
313
308
  this.ctx.showError(`Failed to create report: ${err instanceof Error ? err.message : String(err)}`);
314
309
  }
315
-
316
- this.ctx.ui.requestRender();
317
310
  }
318
311
 
319
312
  async #handleViewLogs(): Promise<void> {
@@ -365,15 +358,14 @@ export class DebugSelectorComponent extends Container {
365
358
  const info = await collectSystemInfo();
366
359
  const formatted = formatSystemInfo(info);
367
360
 
368
- this.ctx.chatContainer.addChild(new Spacer(1));
369
- this.ctx.chatContainer.addChild(new DynamicBorder());
370
- this.ctx.chatContainer.addChild(new Text(formatted, 1, 0));
371
- this.ctx.chatContainer.addChild(new DynamicBorder());
361
+ const block = new TranscriptBlock();
362
+ block.addChild(new DynamicBorder());
363
+ block.addChild(new Text(formatted, 1, 0));
364
+ block.addChild(new DynamicBorder());
365
+ this.ctx.present(block);
372
366
  } catch (err) {
373
367
  this.ctx.showError(`Failed to collect system info: ${err instanceof Error ? err.message : String(err)}`);
374
368
  }
375
-
376
- this.ctx.ui.requestRender();
377
369
  }
378
370
 
379
371
  async #handleViewTerminalState(): Promise<void> {
@@ -384,11 +376,11 @@ export class DebugSelectorComponent extends Container {
384
376
  });
385
377
  const formatted = formatTerminalState(info);
386
378
 
387
- this.ctx.chatContainer.addChild(new Spacer(1));
388
- this.ctx.chatContainer.addChild(new DynamicBorder());
389
- this.ctx.chatContainer.addChild(new Text(formatted, 1, 0));
390
- this.ctx.chatContainer.addChild(new DynamicBorder());
391
- this.ctx.ui.requestRender();
379
+ const block = new TranscriptBlock();
380
+ block.addChild(new DynamicBorder());
381
+ block.addChild(new Text(formatted, 1, 0));
382
+ block.addChild(new DynamicBorder());
383
+ this.ctx.present(block);
392
384
  }
393
385
 
394
386
  async #handleViewProtocols(): Promise<void> {
@@ -407,15 +399,14 @@ export class DebugSelectorComponent extends Container {
407
399
  TERMINAL.sendNotification(notification);
408
400
  }
409
401
 
410
- this.ctx.chatContainer.addChild(new Spacer(1));
411
- this.ctx.chatContainer.addChild(
402
+ this.ctx.present([
403
+ new Spacer(1),
412
404
  new ProtocolProbeComponent({
413
405
  image: buildSampleImage(),
414
406
  imageBudget: this.ctx.ui.imageBudget,
415
407
  notificationSuppressed: suppressed,
416
408
  }),
417
- );
418
- this.ctx.ui.requestRender();
409
+ ]);
419
410
  }
420
411
 
421
412
  async #handleTranscriptExport(): Promise<void> {
@@ -487,21 +478,24 @@ export class DebugSelectorComponent extends Container {
487
478
  loader.stop();
488
479
  this.ctx.statusContainer.clear();
489
480
 
490
- this.ctx.chatContainer.addChild(new Spacer(1));
491
- this.ctx.chatContainer.addChild(
481
+ this.ctx.present([
482
+ new Spacer(1),
492
483
  new Text(
493
484
  theme.fg("success", `${theme.status.success} Cleared ${result.removed} artifact directories`),
494
485
  1,
495
486
  0,
496
487
  ),
497
- );
488
+ ]);
498
489
  } catch (err) {
499
490
  loader.stop();
500
491
  this.ctx.statusContainer.clear();
501
492
  this.ctx.showError(`Failed to clear cache: ${err instanceof Error ? err.message : String(err)}`);
502
493
  }
494
+ }
503
495
 
504
- this.ctx.ui.requestRender();
496
+ #getRawSseText(): string | undefined {
497
+ const rawSseText = resolveRawSseDebugBuffer(this.ctx.session).toRawText();
498
+ return rawSseText.trim().length > 0 ? rawSseText : undefined;
505
499
  }
506
500
 
507
501
  #getResolvedSettings(): Record<string, unknown> {
@@ -152,9 +152,9 @@ export class RawSseDebugBuffer {
152
152
  }
153
153
 
154
154
  // Ownership contract for `event.raw`:
155
- // The caller (either `notifyRawSseEvent` in `packages/ai/src/utils/sse-debug.ts`
156
- // or `SseTeeParser.#dispatch` directly) hands us a freshly-allocated
157
- // `string[]` per event and never retains, mutates, or re-dispatches it.
155
+ // The caller (`notifyRawSseEvent` in `packages/ai/src/utils/sse-debug.ts`)
156
+ // hands us a freshly-allocated `string[]` per event and never retains,
157
+ // mutates, or re-dispatches it.
158
158
  // That lets `trimRawLines` keep the array by reference instead of
159
159
  // cloning on every chunk — a measurable savings on the streaming hot
160
160
  // path. If a future observer-chain mutates the array, restore the
@@ -192,7 +192,10 @@ export class RawSseDebugBuffer {
192
192
  toRawText(): string {
193
193
  // Reads the live array directly: `rawRecordText` only computes a string
194
194
  // from each record, so no caller-visible mutation is possible.
195
- return this.#records.map(rawRecordText).join("\n");
195
+ const body = this.#records.map(rawRecordText).join("\n");
196
+ if (this.#droppedRecords === 0) return body;
197
+ const dropped = `: omp-debug-dropped records=${this.#droppedRecords} chars=${this.#droppedChars}\n\n`;
198
+ return body.length > 0 ? `${dropped}${body}` : dropped;
196
199
  }
197
200
 
198
201
  #append(record: RawSseDebugRecord, chars: number): void {
@@ -45,6 +45,8 @@ export interface ReportBundleOptions {
45
45
  heapSnapshot?: HeapSnapshot;
46
46
  /** Work profile (for work scheduling reports) */
47
47
  workProfile?: WorkProfile;
48
+ /** Raw provider SSE diagnostics captured by the session buffer */
49
+ rawSseText?: string;
48
50
  }
49
51
 
50
52
  export interface ReportBundleResult {
@@ -70,6 +72,7 @@ export interface DebugLogSource {
70
72
  * - env.json: Sanitized environment variables
71
73
  * - config.json: Resolved settings
72
74
  * - profile.cpuprofile: CPU profile (performance report only)
75
+ * - raw-sse.txt: Recent raw provider SSE diagnostics (when captured)
73
76
  * - profile.md: Markdown CPU profile (performance report only)
74
77
  * - heap.heapsnapshot: Heap snapshot (memory report only)
75
78
  * - work.folded: Work profile folded stacks (work report only)
@@ -109,6 +112,12 @@ export async function createReportBundle(options: ReportBundleOptions): Promise<
109
112
  files.push("logs.txt");
110
113
  }
111
114
 
115
+ // Recent raw provider SSE diagnostics
116
+ if (options.rawSseText && options.rawSseText.trim().length > 0) {
117
+ data["raw-sse.txt"] = options.rawSseText;
118
+ files.push("raw-sse.txt");
119
+ }
120
+
112
121
  // Session file
113
122
  if (options.sessionFile) {
114
123
  try {
@@ -8,6 +8,8 @@
8
8
  * from `@oh-my-pi/hashline`; the only coding-agent-specific concern here
9
9
  * is wiring it onto the per-session owner object.
10
10
  */
11
+ import * as fs from "node:fs";
12
+ import * as path from "node:path";
11
13
  import { InMemorySnapshotStore } from "@oh-my-pi/hashline";
12
14
  import { normalizeToLF } from "./normalize";
13
15
 
@@ -33,6 +35,36 @@ export function getFileSnapshotStore(session: FileSnapshotStoreOwner): InMemoryS
33
35
  return session.fileSnapshotStore;
34
36
  }
35
37
 
38
+ /**
39
+ * Canonicalize an absolute path into the stable key the snapshot store uses.
40
+ *
41
+ * Different code paths reach the snapshot store via different path forms:
42
+ * `read local://foo.md` records under the file's `fs.realpath` (the local
43
+ * protocol handler resolves symlinks); a subsequent `edit` may address the
44
+ * same artifact via `local://foo.md`, whose resolver does NOT realpath, or
45
+ * via the absolute path returned in the `[path#tag]` header. macOS adds the
46
+ * same hazard at the working-tree level (`/tmp/...` vs `/private/tmp/...`).
47
+ * Collapsing every key through `realpath` makes those forms fuse onto one
48
+ * snapshot entry, so a freshly-minted tag is never rejected as stale just
49
+ * because the lookup spelled the same file differently.
50
+ *
51
+ * Non-existent paths (new-file writes) fall back to a realpath of the parent
52
+ * directory + basename, then to the input. This keeps creates and updates on
53
+ * the same canonical key.
54
+ */
55
+ export function canonicalSnapshotKey(absolutePath: string): string {
56
+ try {
57
+ return fs.realpathSync.native(absolutePath);
58
+ } catch {
59
+ try {
60
+ const parent = fs.realpathSync.native(path.dirname(absolutePath));
61
+ return path.join(parent, path.basename(absolutePath));
62
+ } catch {
63
+ return absolutePath;
64
+ }
65
+ }
66
+ }
67
+
36
68
  /**
37
69
  * Read the full text of `absolutePath` (within {@link SNAPSHOT_MAX_BYTES}),
38
70
  * record it as a version snapshot, and return its content-hash tag. Returns
@@ -52,7 +84,7 @@ export async function recordFileSnapshot(
52
84
  const file = Bun.file(absolutePath);
53
85
  if (file.size > SNAPSHOT_MAX_BYTES) return undefined;
54
86
  const normalized = normalizeToLF(await file.text());
55
- return getFileSnapshotStore(session).record(absolutePath, normalized);
87
+ return getFileSnapshotStore(session).record(canonicalSnapshotKey(absolutePath), normalized);
56
88
  } catch {
57
89
  return undefined;
58
90
  }
@@ -23,6 +23,7 @@ import type { ToolSession } from "../../tools";
23
23
  import { assertEditableFileContent } from "../../tools/auto-generated-guard";
24
24
  import { invalidateFsScanAfterWrite } from "../../tools/fs-cache-invalidation";
25
25
  import { enforcePlanModeWrite, resolvePlanPath } from "../../tools/plan-mode-guard";
26
+ import { canonicalSnapshotKey } from "../file-snapshot-store";
26
27
  import { readEditFileText, serializeEditFileText } from "../read-file";
27
28
  import type { LspBatchRequest } from "../renderer";
28
29
 
@@ -81,7 +82,7 @@ export class HashlineFilesystem extends Filesystem {
81
82
  }
82
83
 
83
84
  canonicalPath(relativePath: string): string {
84
- return this.resolveAbsolute(relativePath);
85
+ return canonicalSnapshotKey(this.resolveAbsolute(relativePath));
85
86
  }
86
87
 
87
88
  async readText(relativePath: string): Promise<string> {
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { HL_FILE_PREFIX, HL_FILE_SUFFIX } from "@oh-my-pi/hashline";
6
6
  import type { Component } from "@oh-my-pi/pi-tui";
7
- import { Text, visibleWidth, wrapTextWithAnsi } from "@oh-my-pi/pi-tui";
7
+ import { visibleWidth, wrapTextWithAnsi } from "@oh-my-pi/pi-tui";
8
8
  import { sanitizeText } from "@oh-my-pi/pi-utils";
9
9
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
10
10
  import type { FileDiagnosticsResult } from "../lsp";
@@ -16,7 +16,6 @@ import {
16
16
  formatDiffStats,
17
17
  formatExpandHint,
18
18
  formatStatusIcon,
19
- formatTitle,
20
19
  getDiffStats,
21
20
  getLspBatchRequest,
22
21
  type LspBatchRequest,
@@ -25,7 +24,7 @@ import {
25
24
  shortenPath,
26
25
  truncateDiffByHunk,
27
26
  } from "../tools/render-utils";
28
- import { fileHyperlink, Hasher, type RenderCache, renderStatusLine, truncateToWidth } from "../tui";
27
+ import { fileHyperlink, framedBlock, Hasher, type RenderCache, renderStatusLine, truncateToWidth } from "../tui";
29
28
  import type { EditMode } from "../utils/edit-mode";
30
29
  import type { DiffError, DiffResult } from "./diff";
31
30
  import { type ApplyPatchEntry, expandApplyPatchToEntries, expandApplyPatchToPreviewEntries } from "./modes/apply-patch";
@@ -186,10 +185,14 @@ function getOperationTitle(op: Operation | undefined): string {
186
185
  function formatEditPathDisplay(
187
186
  rawPath: string,
188
187
  uiTheme: Theme,
189
- options?: { rename?: string; firstChangedLine?: number },
188
+ options?: { rename?: string; firstChangedLine?: number; linkPath?: string; renameLinkPath?: string },
190
189
  ): string {
190
+ // `rawPath`/`rename` are shown (cwd-relative) but the OSC 8 link targets the
191
+ // absolute path when known — a relative `rawPath` would yield a `file:///rel`
192
+ // URI that resolves against filesystem root instead of cwd.
193
+ const linkTarget = options?.linkPath || rawPath;
191
194
  let pathDisplay = rawPath
192
- ? fileHyperlink(rawPath, uiTheme.fg("accent", shortenPath(rawPath)))
195
+ ? fileHyperlink(linkTarget, uiTheme.fg("accent", shortenPath(rawPath)))
193
196
  : uiTheme.fg("toolOutput", "…");
194
197
 
195
198
  if (options?.firstChangedLine) {
@@ -197,7 +200,8 @@ function formatEditPathDisplay(
197
200
  }
198
201
 
199
202
  if (options?.rename) {
200
- pathDisplay += ` ${uiTheme.fg("dim", "→")} ${fileHyperlink(options.rename, uiTheme.fg("accent", shortenPath(options.rename)))}`;
203
+ const renameTarget = options.renameLinkPath || options.rename;
204
+ pathDisplay += ` ${uiTheme.fg("dim", "→")} ${fileHyperlink(renameTarget, uiTheme.fg("accent", shortenPath(options.rename)))}`;
201
205
  }
202
206
 
203
207
  return pathDisplay;
@@ -206,7 +210,7 @@ function formatEditPathDisplay(
206
210
  function formatEditDescription(
207
211
  rawPath: string,
208
212
  uiTheme: Theme,
209
- options?: { rename?: string; firstChangedLine?: number },
213
+ options?: { rename?: string; firstChangedLine?: number; linkPath?: string; renameLinkPath?: string },
210
214
  ): { language: string; description: string } {
211
215
  const language = getLanguageFromPath(rawPath) ?? "text";
212
216
  const icon = uiTheme.fg("muted", uiTheme.getLangIcon(language));
@@ -459,23 +463,31 @@ export const editToolRenderer = {
459
463
  const rename = editArgs.rename || firstEdit?.rename || firstEdit?.move || firstApplyPatchEntry?.rename;
460
464
  const op = editArgs.op || firstEdit?.op || firstApplyPatchEntry?.op;
461
465
  const { description } = formatEditDescription(rawPath, uiTheme, { rename });
462
- const spinner =
463
- options?.spinnerFrame !== undefined ? formatStatusIcon("running", uiTheme, options.spinnerFrame) : "";
464
- let text = `${formatTitle(getOperationTitle(op), uiTheme)} ${spinner ? `${spinner} ` : ""}${description}`;
465
- // Show file count hint for multi-file edits
466
466
  let fileCount = hashlineInputSummary?.entries.length ?? applyPatchSummary?.entries.length ?? 0;
467
467
  if (Array.isArray(editArgs.edits)) {
468
468
  fileCount = countEditFiles(editArgs.edits);
469
469
  }
470
- if (fileCount > 1) {
471
- text += uiTheme.fg("dim", ` (+${fileCount - 1} more)`);
472
- }
473
- text += getCallPreview(editArgs, rawPath, uiTheme, renderContext, options.expanded);
474
- if (applyPatchSummary?.error) {
475
- text += `\n\n${uiTheme.fg("error", truncateToWidth(replaceTabs(applyPatchSummary.error, rawPath), CALL_TEXT_PREVIEW_WIDTH))}`;
476
- }
477
-
478
- return new Text(text, 0, 0);
470
+ return framedBlock(uiTheme, width => {
471
+ let header = renderStatusLine(
472
+ { icon: "pending", spinnerFrame: options?.spinnerFrame, title: getOperationTitle(op), description },
473
+ uiTheme,
474
+ );
475
+ if (fileCount > 1) header += uiTheme.fg("dim", ` (+${fileCount - 1} more)`);
476
+ let body = getCallPreview(editArgs, rawPath, uiTheme, renderContext, options.expanded);
477
+ if (applyPatchSummary?.error) {
478
+ body += `\n${uiTheme.fg("error", truncateToWidth(replaceTabs(applyPatchSummary.error, rawPath), Math.max(1, width - 2)))}`;
479
+ }
480
+ const bodyLines = body ? body.split("\n") : [];
481
+ while (bodyLines.length > 0 && bodyLines[0].trim() === "") bodyLines.shift();
482
+ return {
483
+ header,
484
+ sections: bodyLines.length > 0 ? [{ lines: bodyLines }] : [],
485
+ state: applyPatchSummary?.error ? "error" : "pending",
486
+ borderColor: applyPatchSummary?.error ? "error" : "borderMuted",
487
+ width,
488
+ contentPaddingLeft: 0,
489
+ };
490
+ });
479
491
  },
480
492
 
481
493
  renderResult(
@@ -525,65 +537,57 @@ function renderSingleFileResult(
525
537
  (result.content?.find(c => c.type === "text")?.text ?? "")
526
538
  : "";
527
539
 
528
- let cached: RenderCache | undefined;
529
-
530
- return {
531
- render(width) {
532
- const { expanded, renderContext } = options;
533
- const editDiffPreview = renderContext?.editDiffPreview;
534
- const renderDiffFn = renderContext?.renderDiff ?? ((t: string) => t);
535
- const key = new Hasher().bool(expanded).u32(width).digest();
536
- if (cached?.key === key) return cached.lines;
537
-
538
- const firstChangedLine =
539
- (editDiffPreview && "firstChangedLine" in editDiffPreview ? editDiffPreview.firstChangedLine : undefined) ||
540
- (details && !isError ? details.firstChangedLine : undefined);
541
- const { description } = formatEditDescription(rawPath, uiTheme, { rename, firstChangedLine });
542
-
543
- // Change stats ride inline on the header next to the path rather than a separate row.
544
- const previewDiff = editDiffPreview && !("error" in editDiffPreview) ? editDiffPreview.diff : undefined;
545
- const headerDiff = isError ? undefined : details?.diff || previewDiff;
546
- const statsSuffix = headerDiff ? formatDiffStatsSuffix(headerDiff, uiTheme) : "";
547
-
548
- const header = renderStatusLine(
549
- {
550
- icon: isError ? "error" : "success",
551
- title: getOperationTitle(op),
552
- description,
553
- },
554
- uiTheme,
540
+ return framedBlock(uiTheme, width => {
541
+ const { expanded, renderContext } = options;
542
+ const editDiffPreview = renderContext?.editDiffPreview;
543
+ const renderDiffFn = renderContext?.renderDiff ?? ((t: string) => t);
544
+
545
+ const firstChangedLine =
546
+ (editDiffPreview && "firstChangedLine" in editDiffPreview ? editDiffPreview.firstChangedLine : undefined) ||
547
+ (details && !isError ? details.firstChangedLine : undefined);
548
+ const linkPath = details && "path" in details ? details.path : undefined;
549
+ const { description } = formatEditDescription(rawPath, uiTheme, { rename, firstChangedLine, linkPath });
550
+
551
+ // Change stats ride inline on the header bar next to the path.
552
+ const previewDiff = editDiffPreview && !("error" in editDiffPreview) ? editDiffPreview.diff : undefined;
553
+ const headerDiff = isError ? undefined : details?.diff || previewDiff;
554
+ const statsSuffix = headerDiff ? formatDiffStatsSuffix(headerDiff, uiTheme) : "";
555
+ const header =
556
+ renderStatusLine({ icon: isError ? "error" : "success", title: getOperationTitle(op), description }, uiTheme) +
557
+ statsSuffix;
558
+
559
+ let body = "";
560
+ if (isError) {
561
+ if (errorText) body = uiTheme.fg("error", replaceTabs(errorText, rawPath));
562
+ } else if (details?.diff) {
563
+ body = renderDiffSection(details.diff, rawPath, expanded, uiTheme, renderDiffFn);
564
+ } else if (editDiffPreview) {
565
+ if ("error" in editDiffPreview) body = uiTheme.fg("error", replaceTabs(editDiffPreview.error, rawPath));
566
+ else if (editDiffPreview.diff)
567
+ body = renderDiffSection(editDiffPreview.diff, rawPath, expanded, uiTheme, renderDiffFn);
568
+ }
569
+ if (details?.diagnostics) {
570
+ body += formatDiagnostics(details.diagnostics, expanded, uiTheme, (fp: string) =>
571
+ uiTheme.getLangIcon(getLanguageFromPath(fp)),
555
572
  );
556
- let text = header + statsSuffix;
557
-
558
- if (isError) {
559
- if (errorText) {
560
- text += `\n\n${uiTheme.fg("error", replaceTabs(errorText, rawPath))}`;
561
- }
562
- } else if (details?.diff) {
563
- text += renderDiffSection(details.diff, rawPath, expanded, uiTheme, renderDiffFn);
564
- } else if (editDiffPreview) {
565
- if ("error" in editDiffPreview) {
566
- text += `\n\n${uiTheme.fg("error", replaceTabs(editDiffPreview.error, rawPath))}`;
567
- } else if (editDiffPreview.diff) {
568
- text += renderDiffSection(editDiffPreview.diff, rawPath, expanded, uiTheme, renderDiffFn);
569
- }
570
- }
571
-
572
- if (details?.diagnostics) {
573
- text += formatDiagnostics(details.diagnostics, expanded, uiTheme, (fp: string) =>
574
- uiTheme.getLangIcon(getLanguageFromPath(fp)),
575
- );
576
- }
573
+ }
577
574
 
578
- const lines =
579
- width > 0 ? text.split("\n").flatMap(line => wrapEditRendererLine(line, width)) : text.split("\n");
580
- cached = { key, lines };
581
- return lines;
582
- },
583
- invalidate() {
584
- cached = undefined;
585
- },
586
- };
575
+ // Diff lines self-wrap with a continuation gutter; pre-wrap to the frame's
576
+ // inner width so renderOutputBlock's generic wrap is a no-op. Edit frames
577
+ // use a flush left border because code-frame gutters already provide padding.
578
+ const innerWidth = Math.max(1, width - 2);
579
+ const bodyLines = body.length > 0 ? body.split("\n").flatMap(line => wrapEditRendererLine(line, innerWidth)) : [];
580
+ while (bodyLines.length > 0 && bodyLines[0].trim() === "") bodyLines.shift();
581
+
582
+ return {
583
+ header,
584
+ sections: bodyLines.length > 0 ? [{ lines: bodyLines }] : [],
585
+ state: isError ? "error" : options.isPartial ? "pending" : "success",
586
+ borderColor: isError ? "error" : "borderMuted",
587
+ width,
588
+ contentPaddingLeft: 0,
589
+ };
590
+ });
587
591
  }
588
592
 
589
593
  function renderMultiFileResult(
@@ -638,7 +642,7 @@ function renderMultiFileResult(
638
642
  },
639
643
  invalidate() {
640
644
  cached = undefined;
641
- for (const c of fileComponents) c.invalidate();
645
+ for (const c of fileComponents) c.invalidate?.();
642
646
  },
643
647
  };
644
648
  }