@oh-my-pi/pi-coding-agent 15.9.67 → 15.10.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 (266) hide show
  1. package/CHANGELOG.md +136 -0
  2. package/dist/types/cli/args.d.ts +1 -1
  3. package/dist/types/cli/dry-balance-cli.d.ts +15 -1
  4. package/dist/types/cli/gallery-cli.d.ts +43 -0
  5. package/dist/types/cli/gallery-fixtures/agentic.d.ts +2 -0
  6. package/dist/types/cli/gallery-fixtures/codeintel.d.ts +3 -0
  7. package/dist/types/cli/gallery-fixtures/edit.d.ts +3 -0
  8. package/dist/types/cli/gallery-fixtures/fs.d.ts +2 -0
  9. package/dist/types/cli/gallery-fixtures/index.d.ts +4 -0
  10. package/dist/types/cli/gallery-fixtures/interaction.d.ts +3 -0
  11. package/dist/types/cli/gallery-fixtures/memory.d.ts +2 -0
  12. package/dist/types/cli/gallery-fixtures/misc.d.ts +3 -0
  13. package/dist/types/cli/gallery-fixtures/search.d.ts +3 -0
  14. package/dist/types/cli/gallery-fixtures/shell.d.ts +3 -0
  15. package/dist/types/cli/gallery-fixtures/types.d.ts +44 -0
  16. package/dist/types/cli/gallery-fixtures/web.d.ts +2 -0
  17. package/dist/types/cli/gallery-screenshot.d.ts +35 -0
  18. package/dist/types/commands/gallery.d.ts +47 -0
  19. package/dist/types/commit/analysis/conventional.d.ts +2 -2
  20. package/dist/types/commit/analysis/summary.d.ts +2 -2
  21. package/dist/types/commit/changelog/generate.d.ts +2 -2
  22. package/dist/types/commit/changelog/index.d.ts +2 -2
  23. package/dist/types/commit/map-reduce/index.d.ts +3 -3
  24. package/dist/types/commit/map-reduce/map-phase.d.ts +2 -2
  25. package/dist/types/commit/map-reduce/reduce-phase.d.ts +2 -2
  26. package/dist/types/commit/model-selection.d.ts +10 -4
  27. package/dist/types/config/api-key-resolver.d.ts +34 -0
  28. package/dist/types/config/keybindings.d.ts +6 -1
  29. package/dist/types/config/model-id-affixes.d.ts +2 -0
  30. package/dist/types/config/model-registry.d.ts +25 -2
  31. package/dist/types/config/settings-schema.d.ts +41 -6
  32. package/dist/types/dap/config.d.ts +14 -1
  33. package/dist/types/dap/types.d.ts +10 -0
  34. package/dist/types/extensibility/plugins/marketplace-auto-update.d.ts +8 -0
  35. package/dist/types/lsp/types.d.ts +10 -0
  36. package/dist/types/lsp/utils.d.ts +3 -2
  37. package/dist/types/main.d.ts +3 -2
  38. package/dist/types/memory-backend/index.d.ts +2 -1
  39. package/dist/types/memory-backend/resolve.d.ts +1 -1
  40. package/dist/types/memory-backend/types.d.ts +1 -1
  41. package/dist/types/modes/components/chat-block.d.ts +64 -0
  42. package/dist/types/modes/components/custom-editor.d.ts +5 -1
  43. package/dist/types/modes/components/overlay-box.d.ts +17 -0
  44. package/dist/types/modes/components/plan-review-overlay.d.ts +59 -0
  45. package/dist/types/modes/components/plan-toc.d.ts +41 -0
  46. package/dist/types/modes/components/read-tool-group.d.ts +2 -0
  47. package/dist/types/modes/components/tool-execution.d.ts +18 -0
  48. package/dist/types/modes/components/transcript-container.d.ts +11 -0
  49. package/dist/types/modes/controllers/command-controller.d.ts +1 -0
  50. package/dist/types/modes/controllers/event-controller.d.ts +0 -1
  51. package/dist/types/modes/controllers/extension-ui-controller.d.ts +0 -1
  52. package/dist/types/modes/controllers/input-controller.d.ts +1 -1
  53. package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
  54. package/dist/types/modes/controllers/streaming-reveal.d.ts +22 -0
  55. package/dist/types/modes/controllers/tan-command-controller.d.ts +6 -0
  56. package/dist/types/modes/index.d.ts +5 -4
  57. package/dist/types/modes/interactive-mode.d.ts +16 -6
  58. package/dist/types/modes/setup-version.d.ts +11 -0
  59. package/dist/types/modes/setup-wizard/index.d.ts +2 -1
  60. package/dist/types/modes/setup-wizard/scenes/web-search.d.ts +2 -1
  61. package/dist/types/modes/theme/theme.d.ts +1 -1
  62. package/dist/types/modes/types.d.ts +19 -6
  63. package/dist/types/modes/utils/copy-targets.d.ts +21 -1
  64. package/dist/types/plan-mode/approved-plan.d.ts +27 -8
  65. package/dist/types/plan-mode/plan-protection.d.ts +4 -4
  66. package/dist/types/sdk.d.ts +3 -1
  67. package/dist/types/session/agent-session.d.ts +21 -0
  68. package/dist/types/session/messages.d.ts +12 -0
  69. package/dist/types/session/session-manager.d.ts +3 -1
  70. package/dist/types/slash-commands/types.d.ts +4 -6
  71. package/dist/types/task/executor.d.ts +14 -0
  72. package/dist/types/task/index.d.ts +1 -0
  73. package/dist/types/task/render.d.ts +3 -2
  74. package/dist/types/telemetry-export.d.ts +1 -1
  75. package/dist/types/tools/archive-reader.d.ts +5 -0
  76. package/dist/types/tools/ast-edit.d.ts +3 -0
  77. package/dist/types/tools/ast-grep.d.ts +3 -0
  78. package/dist/types/tools/bash.d.ts +1 -0
  79. package/dist/types/tools/eval-render.d.ts +1 -8
  80. package/dist/types/tools/fetch.d.ts +15 -7
  81. package/dist/types/tools/find.d.ts +8 -4
  82. package/dist/types/tools/grouped-file-output.d.ts +95 -12
  83. package/dist/types/tools/memory-render.d.ts +4 -1
  84. package/dist/types/tools/plan-mode-guard.d.ts +8 -9
  85. package/dist/types/tools/render-utils.d.ts +13 -9
  86. package/dist/types/tools/renderers.d.ts +16 -2
  87. package/dist/types/tools/search.d.ts +5 -1
  88. package/dist/types/tools/sqlite-reader.d.ts +1 -0
  89. package/dist/types/tools/todo.d.ts +3 -2
  90. package/dist/types/tools/write.d.ts +5 -0
  91. package/dist/types/tui/output-block.d.ts +16 -4
  92. package/dist/types/tui/status-line.d.ts +3 -0
  93. package/dist/types/utils/enhanced-paste.d.ts +20 -0
  94. package/dist/types/web/scrapers/github.d.ts +22 -0
  95. package/dist/types/web/search/providers/kimi.d.ts +1 -1
  96. package/dist/types/web/search/providers/perplexity.d.ts +8 -1
  97. package/dist/types/web/search/types.d.ts +1 -1
  98. package/package.json +9 -9
  99. package/scripts/dev-launch +42 -0
  100. package/scripts/dev-launch-preload.ts +19 -0
  101. package/src/auto-thinking/classifier.ts +5 -1
  102. package/src/cli/args.ts +2 -2
  103. package/src/cli/dry-balance-cli.ts +52 -17
  104. package/src/cli/gallery-cli.ts +226 -0
  105. package/src/cli/gallery-fixtures/agentic.ts +292 -0
  106. package/src/cli/gallery-fixtures/codeintel.ts +188 -0
  107. package/src/cli/gallery-fixtures/edit.ts +194 -0
  108. package/src/cli/gallery-fixtures/fs.ts +153 -0
  109. package/src/cli/gallery-fixtures/index.ts +40 -0
  110. package/src/cli/gallery-fixtures/interaction.ts +49 -0
  111. package/src/cli/gallery-fixtures/memory.ts +81 -0
  112. package/src/cli/gallery-fixtures/misc.ts +250 -0
  113. package/src/cli/gallery-fixtures/search.ts +213 -0
  114. package/src/cli/gallery-fixtures/shell.ts +167 -0
  115. package/src/cli/gallery-fixtures/types.ts +41 -0
  116. package/src/cli/gallery-fixtures/web.ts +158 -0
  117. package/src/cli/gallery-screenshot.ts +279 -0
  118. package/src/cli-commands.ts +1 -0
  119. package/src/commands/gallery.ts +52 -0
  120. package/src/commands/launch.ts +1 -1
  121. package/src/commit/analysis/conventional.ts +2 -2
  122. package/src/commit/analysis/summary.ts +2 -2
  123. package/src/commit/changelog/generate.ts +2 -2
  124. package/src/commit/changelog/index.ts +2 -2
  125. package/src/commit/map-reduce/index.ts +3 -3
  126. package/src/commit/map-reduce/map-phase.ts +2 -2
  127. package/src/commit/map-reduce/reduce-phase.ts +2 -2
  128. package/src/commit/model-selection.ts +33 -9
  129. package/src/commit/pipeline.ts +4 -4
  130. package/src/config/api-key-resolver.ts +58 -0
  131. package/src/config/keybindings.ts +15 -6
  132. package/src/config/model-equivalence.ts +35 -12
  133. package/src/config/model-id-affixes.ts +39 -22
  134. package/src/config/model-registry.ts +41 -18
  135. package/src/config/settings-schema.ts +28 -5
  136. package/src/config/settings.ts +31 -2
  137. package/src/dap/client.ts +14 -16
  138. package/src/dap/config.ts +41 -2
  139. package/src/dap/defaults.json +1 -0
  140. package/src/dap/session.ts +1 -0
  141. package/src/dap/types.ts +10 -0
  142. package/src/debug/index.ts +40 -54
  143. package/src/edit/renderer.ts +111 -119
  144. package/src/eval/__tests__/agent-bridge.test.ts +75 -32
  145. package/src/eval/__tests__/llm-bridge.test.ts +90 -31
  146. package/src/eval/agent-bridge.ts +34 -7
  147. package/src/eval/llm-bridge.ts +8 -3
  148. package/src/extensibility/extensions/runner.ts +1 -0
  149. package/src/extensibility/plugins/doctor.ts +0 -1
  150. package/src/extensibility/plugins/marketplace-auto-update.ts +49 -0
  151. package/src/goals/tools/goal-tool.ts +37 -27
  152. package/src/internal-urls/docs-index.generated.ts +10 -10
  153. package/src/lsp/client.ts +104 -55
  154. package/src/lsp/types.ts +10 -0
  155. package/src/lsp/utils.ts +3 -2
  156. package/src/main.ts +53 -56
  157. package/src/memories/index.ts +12 -5
  158. package/src/memory-backend/index.ts +13 -1
  159. package/src/memory-backend/resolve.ts +3 -5
  160. package/src/memory-backend/types.ts +1 -1
  161. package/src/mnemopi/backend.ts +5 -1
  162. package/src/modes/acp/acp-agent.ts +33 -26
  163. package/src/modes/components/assistant-message.ts +2 -9
  164. package/src/modes/components/chat-block.ts +111 -0
  165. package/src/modes/components/copy-selector.ts +1 -44
  166. package/src/modes/components/custom-editor.ts +33 -1
  167. package/src/modes/components/custom-message.ts +1 -3
  168. package/src/modes/components/execution-shared.ts +1 -2
  169. package/src/modes/components/hook-message.ts +1 -3
  170. package/src/modes/components/overlay-box.ts +108 -0
  171. package/src/modes/components/plan-review-overlay.ts +799 -0
  172. package/src/modes/components/plan-toc.ts +138 -0
  173. package/src/modes/components/read-tool-group.ts +20 -4
  174. package/src/modes/components/skill-message.ts +0 -1
  175. package/src/modes/components/status-line.ts +3 -5
  176. package/src/modes/components/tips.txt +1 -0
  177. package/src/modes/components/todo-reminder.ts +0 -2
  178. package/src/modes/components/tool-execution.ts +115 -90
  179. package/src/modes/components/transcript-container.ts +84 -24
  180. package/src/modes/components/user-message.ts +1 -2
  181. package/src/modes/controllers/command-controller-shared.ts +7 -6
  182. package/src/modes/controllers/command-controller.ts +70 -57
  183. package/src/modes/controllers/event-controller.ts +41 -40
  184. package/src/modes/controllers/extension-ui-controller.ts +10 -73
  185. package/src/modes/controllers/input-controller.ts +135 -122
  186. package/src/modes/controllers/mcp-command-controller.ts +69 -60
  187. package/src/modes/controllers/selector-controller.ts +25 -27
  188. package/src/modes/controllers/streaming-reveal.ts +212 -0
  189. package/src/modes/controllers/tan-command-controller.ts +173 -0
  190. package/src/modes/index.ts +5 -4
  191. package/src/modes/interactive-mode.ts +171 -82
  192. package/src/modes/setup-version.ts +11 -0
  193. package/src/modes/setup-wizard/index.ts +3 -2
  194. package/src/modes/setup-wizard/scenes/web-search.ts +3 -2
  195. package/src/modes/setup-wizard/wizard-overlay.ts +1 -1
  196. package/src/modes/theme/theme-schema.json +1 -1
  197. package/src/modes/theme/theme.ts +8 -4
  198. package/src/modes/types.ts +19 -8
  199. package/src/modes/utils/context-usage.ts +10 -6
  200. package/src/modes/utils/copy-targets.ts +133 -27
  201. package/src/modes/utils/hotkeys-markdown.ts +1 -0
  202. package/src/modes/utils/ui-helpers.ts +44 -46
  203. package/src/plan-mode/approved-plan.ts +66 -43
  204. package/src/plan-mode/plan-protection.ts +4 -4
  205. package/src/prompts/system/background-tan-dispatch.md +8 -0
  206. package/src/prompts/system/plan-mode-active.md +67 -58
  207. package/src/prompts/system/plan-mode-approved.md +1 -1
  208. package/src/sdk.ts +32 -60
  209. package/src/session/agent-session.ts +89 -13
  210. package/src/session/messages.ts +26 -0
  211. package/src/session/session-manager.ts +13 -5
  212. package/src/slash-commands/builtin-registry.ts +37 -10
  213. package/src/slash-commands/helpers/usage-report.ts +2 -0
  214. package/src/slash-commands/types.ts +4 -6
  215. package/src/task/executor.ts +25 -4
  216. package/src/task/index.ts +4 -0
  217. package/src/task/render.ts +212 -148
  218. package/src/telemetry-export.ts +25 -7
  219. package/src/tools/archive-reader.ts +64 -0
  220. package/src/tools/ask.ts +119 -164
  221. package/src/tools/ast-edit.ts +98 -71
  222. package/src/tools/ast-grep.ts +37 -43
  223. package/src/tools/bash.ts +50 -6
  224. package/src/tools/debug.ts +20 -8
  225. package/src/tools/eval-backends.ts +6 -17
  226. package/src/tools/eval-render.ts +21 -18
  227. package/src/tools/eval.ts +5 -4
  228. package/src/tools/fetch.ts +391 -91
  229. package/src/tools/find.ts +44 -30
  230. package/src/tools/gh-renderer.ts +81 -42
  231. package/src/tools/grouped-file-output.ts +272 -48
  232. package/src/tools/image-gen.ts +150 -103
  233. package/src/tools/inspect-image-renderer.ts +63 -41
  234. package/src/tools/inspect-image.ts +8 -1
  235. package/src/tools/job.ts +3 -4
  236. package/src/tools/memory-render.ts +4 -1
  237. package/src/tools/plan-mode-guard.ts +21 -39
  238. package/src/tools/read.ts +23 -16
  239. package/src/tools/render-utils.ts +38 -40
  240. package/src/tools/renderers.ts +16 -1
  241. package/src/tools/report-tool-issue.ts +1 -1
  242. package/src/tools/resolve.ts +14 -0
  243. package/src/tools/search-tool-bm25.ts +36 -23
  244. package/src/tools/search.ts +189 -95
  245. package/src/tools/sqlite-reader.ts +9 -12
  246. package/src/tools/todo.ts +138 -59
  247. package/src/tools/write.ts +100 -60
  248. package/src/tui/output-block.ts +60 -13
  249. package/src/tui/status-line.ts +5 -1
  250. package/src/utils/commit-message-generator.ts +9 -1
  251. package/src/utils/enhanced-paste.ts +202 -0
  252. package/src/utils/title-generator.ts +2 -1
  253. package/src/web/scrapers/github.ts +255 -3
  254. package/src/web/scrapers/youtube.ts +3 -2
  255. package/src/web/search/providers/anthropic.ts +25 -19
  256. package/src/web/search/providers/exa.ts +11 -3
  257. package/src/web/search/providers/kimi.ts +28 -17
  258. package/src/web/search/providers/parallel.ts +35 -24
  259. package/src/web/search/providers/perplexity.ts +199 -51
  260. package/src/web/search/providers/synthetic.ts +8 -6
  261. package/src/web/search/providers/tavily.ts +9 -8
  262. package/src/web/search/providers/zai.ts +8 -6
  263. package/src/web/search/render.ts +39 -54
  264. package/src/web/search/types.ts +5 -1
  265. package/dist/types/eval/__tests__/shared-executors.test.d.ts +0 -1
  266. package/src/eval/__tests__/shared-executors.test.ts +0 -609
@@ -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>();
@@ -201,19 +202,16 @@ export class DebugSelectorComponent extends Container {
201
202
  loader.stop();
202
203
  this.ctx.statusContainer.clear();
203
204
 
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));
205
+ const block = new TranscriptBlock();
206
+ block.addChild(new Text(theme.fg("success", `${theme.status.success} Performance report saved`), 1, 0));
207
+ block.addChild(new Text(theme.fg("dim", formatFileHyperlink(result.path)), 1, 0));
208
+ block.addChild(new Text(theme.fg("dim", `Files: ${result.files.length}`), 1, 0));
209
+ this.ctx.present(block);
210
210
  } catch (err) {
211
211
  loader.stop();
212
212
  this.ctx.statusContainer.clear();
213
213
  this.ctx.showError(`Failed to create report: ${err instanceof Error ? err.message : String(err)}`);
214
214
  }
215
-
216
- this.ctx.ui.requestRender();
217
215
  }
218
216
 
219
217
  async #handleWorkReport(): Promise<void> {
@@ -231,15 +229,13 @@ export class DebugSelectorComponent extends Container {
231
229
 
232
230
  openPath(tmpPath);
233
231
 
234
- this.ctx.chatContainer.addChild(new Spacer(1));
235
- this.ctx.chatContainer.addChild(
232
+ this.ctx.present([
233
+ new Spacer(1),
236
234
  new Text(theme.fg("dim", `Opened flamegraph (${workProfile.sampleCount} samples)`), 1, 0),
237
- );
235
+ ]);
238
236
  } catch (err) {
239
237
  this.ctx.showError(`Failed to open profile: ${err instanceof Error ? err.message : String(err)}`);
240
238
  }
241
-
242
- this.ctx.ui.requestRender();
243
239
  }
244
240
 
245
241
  async #handleDumpReport(): Promise<void> {
@@ -262,19 +258,16 @@ export class DebugSelectorComponent extends Container {
262
258
  loader.stop();
263
259
  this.ctx.statusContainer.clear();
264
260
 
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));
261
+ const block = new TranscriptBlock();
262
+ block.addChild(new Text(theme.fg("success", `${theme.status.success} Report bundle saved`), 1, 0));
263
+ block.addChild(new Text(theme.fg("dim", formatFileHyperlink(result.path)), 1, 0));
264
+ block.addChild(new Text(theme.fg("dim", `Files: ${result.files.length}`), 1, 0));
265
+ this.ctx.present(block);
271
266
  } catch (err) {
272
267
  loader.stop();
273
268
  this.ctx.statusContainer.clear();
274
269
  this.ctx.showError(`Failed to create report: ${err instanceof Error ? err.message : String(err)}`);
275
270
  }
276
-
277
- this.ctx.ui.requestRender();
278
271
  }
279
272
 
280
273
  async #handleMemoryReport(): Promise<void> {
@@ -301,19 +294,16 @@ export class DebugSelectorComponent extends Container {
301
294
  loader.stop();
302
295
  this.ctx.statusContainer.clear();
303
296
 
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));
297
+ const block = new TranscriptBlock();
298
+ block.addChild(new Text(theme.fg("success", `${theme.status.success} Memory report saved`), 1, 0));
299
+ block.addChild(new Text(theme.fg("dim", formatFileHyperlink(result.path)), 1, 0));
300
+ block.addChild(new Text(theme.fg("dim", `Files: ${result.files.length}`), 1, 0));
301
+ this.ctx.present(block);
310
302
  } catch (err) {
311
303
  loader.stop();
312
304
  this.ctx.statusContainer.clear();
313
305
  this.ctx.showError(`Failed to create report: ${err instanceof Error ? err.message : String(err)}`);
314
306
  }
315
-
316
- this.ctx.ui.requestRender();
317
307
  }
318
308
 
319
309
  async #handleViewLogs(): Promise<void> {
@@ -365,15 +355,14 @@ export class DebugSelectorComponent extends Container {
365
355
  const info = await collectSystemInfo();
366
356
  const formatted = formatSystemInfo(info);
367
357
 
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());
358
+ const block = new TranscriptBlock();
359
+ block.addChild(new DynamicBorder());
360
+ block.addChild(new Text(formatted, 1, 0));
361
+ block.addChild(new DynamicBorder());
362
+ this.ctx.present(block);
372
363
  } catch (err) {
373
364
  this.ctx.showError(`Failed to collect system info: ${err instanceof Error ? err.message : String(err)}`);
374
365
  }
375
-
376
- this.ctx.ui.requestRender();
377
366
  }
378
367
 
379
368
  async #handleViewTerminalState(): Promise<void> {
@@ -384,11 +373,11 @@ export class DebugSelectorComponent extends Container {
384
373
  });
385
374
  const formatted = formatTerminalState(info);
386
375
 
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();
376
+ const block = new TranscriptBlock();
377
+ block.addChild(new DynamicBorder());
378
+ block.addChild(new Text(formatted, 1, 0));
379
+ block.addChild(new DynamicBorder());
380
+ this.ctx.present(block);
392
381
  }
393
382
 
394
383
  async #handleViewProtocols(): Promise<void> {
@@ -407,15 +396,14 @@ export class DebugSelectorComponent extends Container {
407
396
  TERMINAL.sendNotification(notification);
408
397
  }
409
398
 
410
- this.ctx.chatContainer.addChild(new Spacer(1));
411
- this.ctx.chatContainer.addChild(
399
+ this.ctx.present([
400
+ new Spacer(1),
412
401
  new ProtocolProbeComponent({
413
402
  image: buildSampleImage(),
414
403
  imageBudget: this.ctx.ui.imageBudget,
415
404
  notificationSuppressed: suppressed,
416
405
  }),
417
- );
418
- this.ctx.ui.requestRender();
406
+ ]);
419
407
  }
420
408
 
421
409
  async #handleTranscriptExport(): Promise<void> {
@@ -487,21 +475,19 @@ export class DebugSelectorComponent extends Container {
487
475
  loader.stop();
488
476
  this.ctx.statusContainer.clear();
489
477
 
490
- this.ctx.chatContainer.addChild(new Spacer(1));
491
- this.ctx.chatContainer.addChild(
478
+ this.ctx.present([
479
+ new Spacer(1),
492
480
  new Text(
493
481
  theme.fg("success", `${theme.status.success} Cleared ${result.removed} artifact directories`),
494
482
  1,
495
483
  0,
496
484
  ),
497
- );
485
+ ]);
498
486
  } catch (err) {
499
487
  loader.stop();
500
488
  this.ctx.statusContainer.clear();
501
489
  this.ctx.showError(`Failed to clear cache: ${err instanceof Error ? err.message : String(err)}`);
502
490
  }
503
-
504
- this.ctx.ui.requestRender();
505
491
  }
506
492
 
507
493
  #getResolvedSettings(): Record<string, unknown> {
@@ -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";
@@ -179,11 +178,6 @@ function countEditFiles(edits: EditRenderEntry[]): number {
179
178
  return new Set(edits.map(edit => filePathFromEditEntry(edit.path)).filter(Boolean)).size;
180
179
  }
181
180
 
182
- function countLines(text: string): number {
183
- if (!text) return 0;
184
- return text.split("\n").length;
185
- }
186
-
187
181
  function getOperationTitle(op: Operation | undefined): string {
188
182
  return op === "create" ? "Create" : op === "delete" ? "Delete" : "Edit";
189
183
  }
@@ -191,10 +185,14 @@ function getOperationTitle(op: Operation | undefined): string {
191
185
  function formatEditPathDisplay(
192
186
  rawPath: string,
193
187
  uiTheme: Theme,
194
- options?: { rename?: string; firstChangedLine?: number },
188
+ options?: { rename?: string; firstChangedLine?: number; linkPath?: string; renameLinkPath?: string },
195
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;
196
194
  let pathDisplay = rawPath
197
- ? fileHyperlink(rawPath, uiTheme.fg("accent", shortenPath(rawPath)))
195
+ ? fileHyperlink(linkTarget, uiTheme.fg("accent", shortenPath(rawPath)))
198
196
  : uiTheme.fg("toolOutput", "…");
199
197
 
200
198
  if (options?.firstChangedLine) {
@@ -202,7 +200,8 @@ function formatEditPathDisplay(
202
200
  }
203
201
 
204
202
  if (options?.rename) {
205
- 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)))}`;
206
205
  }
207
206
 
208
207
  return pathDisplay;
@@ -211,7 +210,7 @@ function formatEditPathDisplay(
211
210
  function formatEditDescription(
212
211
  rawPath: string,
213
212
  uiTheme: Theme,
214
- options?: { rename?: string; firstChangedLine?: number },
213
+ options?: { rename?: string; firstChangedLine?: number; linkPath?: string; renameLinkPath?: string },
215
214
  ): { language: string; description: string } {
216
215
  const language = getLanguageFromPath(rawPath) ?? "text";
217
216
  const icon = uiTheme.fg("muted", uiTheme.getLangIcon(language));
@@ -233,19 +232,22 @@ function renderPlainTextPreview(text: string, uiTheme: Theme, filePath?: string)
233
232
  return preview.trimEnd();
234
233
  }
235
234
 
236
- function formatStreamingDiff(diff: string, rawPath: string, uiTheme: Theme, label = "streaming"): string {
235
+ function formatStreamingDiff(
236
+ diff: string,
237
+ rawPath: string,
238
+ uiTheme: Theme,
239
+ expanded: boolean,
240
+ label = "streaming",
241
+ ): string {
237
242
  if (!diff) return "";
238
- // "Cursor" tail window: pin the last EDIT_STREAMING_PREVIEW_LINES rows to the
239
- // bottom of the diff so freshly streamed changes stay on screen, and accept
240
- // the trailing rows "from the back" once the diff outgrows the window. The
241
- // whole-file diff is recomputed on every streamed chunk and its Myers
242
- // alignment is not monotonic in payload length, so a hunk-aware window that
243
- // kept whole change segments gained and lost rows tick to tick — the box
244
- // stuttered, and the earlier high-water fix traded that for a half-empty
245
- // rectangle. A strict fixed-height window keeps the box steady and always
246
- // full of real diff context instead of blank padding.
243
+ // Collapsed uses a "Cursor" tail window: pin the last
244
+ // EDIT_STREAMING_PREVIEW_LINES rows to the bottom so freshly streamed changes
245
+ // stay on screen. The whole-file diff is recomputed on every streamed chunk
246
+ // and its Myers alignment is not monotonic in payload length, so a hunk-aware
247
+ // window stutters as rows move between hunks. Expanded deliberately lifts that
248
+ // cap for the approval-time full view.
247
249
  const allLines = diff.replace(/\n+$/u, "").split("\n");
248
- const hiddenLines = Math.max(0, allLines.length - EDIT_STREAMING_PREVIEW_LINES);
250
+ const hiddenLines = expanded ? 0 : Math.max(0, allLines.length - EDIT_STREAMING_PREVIEW_LINES);
249
251
  const visible = hiddenLines > 0 ? allLines.slice(hiddenLines) : allLines;
250
252
  let text = "\n\n";
251
253
  if (hiddenLines > 0) {
@@ -256,19 +258,11 @@ function formatStreamingDiff(diff: string, rawPath: string, uiTheme: Theme, labe
256
258
  text += `${uiTheme.fg("dim", `… (${remainder.join(", ")} above)`)}\n`;
257
259
  }
258
260
  text += renderDiffColored(visible.join("\n"), { filePath: rawPath });
259
- text += uiTheme.fg("dim", `\n(${label})`);
261
+ if (!expanded || label !== "preview") text += uiTheme.fg("dim", `\n(${label})`);
260
262
  return text;
261
263
  }
262
264
 
263
- function formatMetadataLine(lineCount: number | null, language: string | undefined, uiTheme: Theme): string {
264
- const icon = uiTheme.getLangIcon(language);
265
- if (lineCount !== null) {
266
- return uiTheme.fg("dim", `${icon} ${lineCount} lines`);
267
- }
268
- return uiTheme.fg("dim", `${icon}`);
269
- }
270
-
271
- function formatMultiFileStreamingDiff(previews: PerFileDiffPreview[], uiTheme: Theme): string {
265
+ function formatMultiFileStreamingDiff(previews: PerFileDiffPreview[], uiTheme: Theme, expanded: boolean): string {
272
266
  const parts: string[] = [];
273
267
  for (const preview of previews) {
274
268
  if (!preview.diff && !preview.error) continue;
@@ -278,7 +272,7 @@ function formatMultiFileStreamingDiff(previews: PerFileDiffPreview[], uiTheme: T
278
272
  continue;
279
273
  }
280
274
  if (preview.diff) {
281
- parts.push(`${header}${formatStreamingDiff(preview.diff, preview.path, uiTheme, "preview")}`);
275
+ parts.push(`${header}${formatStreamingDiff(preview.diff, preview.path, uiTheme, expanded, "preview")}`);
282
276
  }
283
277
  }
284
278
  return parts.join("");
@@ -289,16 +283,17 @@ function getCallPreview(
289
283
  rawPath: string,
290
284
  uiTheme: Theme,
291
285
  renderContext: EditRenderContext | undefined,
286
+ expanded: boolean,
292
287
  ): string {
293
288
  const multi = renderContext?.perFileDiffPreview;
294
289
  if (multi && multi.length > 1 && multi.some(p => p.diff || p.error)) {
295
- return formatMultiFileStreamingDiff(multi, uiTheme);
290
+ return formatMultiFileStreamingDiff(multi, uiTheme, expanded);
296
291
  }
297
292
  if (args.previewDiff) {
298
- return formatStreamingDiff(args.previewDiff, rawPath, uiTheme, "preview");
293
+ return formatStreamingDiff(args.previewDiff, rawPath, uiTheme, expanded, "preview");
299
294
  }
300
295
  if (args.diff && args.op) {
301
- return formatStreamingDiff(args.diff, rawPath, uiTheme);
296
+ return formatStreamingDiff(args.diff, rawPath, uiTheme, expanded);
302
297
  }
303
298
  if (args.diff) {
304
299
  return renderPlainTextPreview(args.diff, uiTheme, rawPath);
@@ -383,6 +378,13 @@ function getApplyPatchRenderSummary(
383
378
  }
384
379
  }
385
380
 
381
+ function formatDiffStatsSuffix(diff: string, uiTheme: Theme): string {
382
+ const { added, removed, hunks } = getDiffStats(diff);
383
+ const stats = formatDiffStats(added, removed, hunks, uiTheme);
384
+ if (!stats) return "";
385
+ return ` ${uiTheme.fg("dim", uiTheme.format.bracketLeft)}${stats}${uiTheme.fg("dim", uiTheme.format.bracketRight)}`;
386
+ }
387
+
386
388
  function renderDiffSection(
387
389
  diff: string,
388
390
  rawPath: string,
@@ -390,15 +392,6 @@ function renderDiffSection(
390
392
  uiTheme: Theme,
391
393
  renderDiffFn: (t: string, o?: { filePath?: string }) => string,
392
394
  ): string {
393
- let text = "";
394
- const diffStats = getDiffStats(diff);
395
- text += `\n${uiTheme.fg("dim", uiTheme.format.bracketLeft)}${formatDiffStats(
396
- diffStats.added,
397
- diffStats.removed,
398
- diffStats.hunks,
399
- uiTheme,
400
- )}${uiTheme.fg("dim", uiTheme.format.bracketRight)}`;
401
-
402
395
  const {
403
396
  text: truncatedDiff,
404
397
  hiddenHunks,
@@ -407,7 +400,7 @@ function renderDiffSection(
407
400
  ? { text: diff, hiddenHunks: 0, hiddenLines: 0 }
408
401
  : truncateDiffByHunk(diff, PREVIEW_LIMITS.DIFF_COLLAPSED_HUNKS, PREVIEW_LIMITS.DIFF_COLLAPSED_LINES);
409
402
 
410
- text += `\n\n${renderDiffFn(truncatedDiff, { filePath: rawPath })}`;
403
+ let text = `\n${renderDiffFn(truncatedDiff, { filePath: rawPath })}`;
411
404
  if (!expanded && (hiddenHunks > 0 || hiddenLines > 0)) {
412
405
  const remainder: string[] = [];
413
406
  if (hiddenHunks > 0) remainder.push(`${hiddenHunks} more hunks`);
@@ -470,23 +463,31 @@ export const editToolRenderer = {
470
463
  const rename = editArgs.rename || firstEdit?.rename || firstEdit?.move || firstApplyPatchEntry?.rename;
471
464
  const op = editArgs.op || firstEdit?.op || firstApplyPatchEntry?.op;
472
465
  const { description } = formatEditDescription(rawPath, uiTheme, { rename });
473
- const spinner =
474
- options?.spinnerFrame !== undefined ? formatStatusIcon("running", uiTheme, options.spinnerFrame) : "";
475
- let text = `${formatTitle(getOperationTitle(op), uiTheme)} ${spinner ? `${spinner} ` : ""}${description}`;
476
- // Show file count hint for multi-file edits
477
466
  let fileCount = hashlineInputSummary?.entries.length ?? applyPatchSummary?.entries.length ?? 0;
478
467
  if (Array.isArray(editArgs.edits)) {
479
468
  fileCount = countEditFiles(editArgs.edits);
480
469
  }
481
- if (fileCount > 1) {
482
- text += uiTheme.fg("dim", ` (+${fileCount - 1} more)`);
483
- }
484
- text += getCallPreview(editArgs, rawPath, uiTheme, renderContext);
485
- if (applyPatchSummary?.error) {
486
- text += `\n\n${uiTheme.fg("error", truncateToWidth(replaceTabs(applyPatchSummary.error, rawPath), CALL_TEXT_PREVIEW_WIDTH))}`;
487
- }
488
-
489
- 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
+ });
490
491
  },
491
492
 
492
493
  renderResult(
@@ -528,11 +529,6 @@ function renderSingleFileResult(
528
529
  "";
529
530
  const op = args?.op || firstEdit?.op || details?.op;
530
531
  const rename = args?.rename || firstEdit?.rename || firstEdit?.move || details?.move;
531
- const { language } = formatEditDescription(rawPath, uiTheme, { rename });
532
-
533
- const editTextSource = args?.newText ?? args?.oldText ?? args?.diff ?? args?.patch;
534
- const metadataLineCount = editTextSource ? countLines(editTextSource) : null;
535
- const metadataLine = op !== "delete" ? `\n${formatMetadataLine(metadataLineCount, language, uiTheme)}` : "";
536
532
 
537
533
  const displayErrorText = isError && details && "displayErrorText" in details ? details.displayErrorText : undefined;
538
534
  const errorText = isError
@@ -541,61 +537,57 @@ function renderSingleFileResult(
541
537
  (result.content?.find(c => c.type === "text")?.text ?? "")
542
538
  : "";
543
539
 
544
- let cached: RenderCache | undefined;
545
-
546
- return {
547
- render(width) {
548
- const { expanded, renderContext } = options;
549
- const editDiffPreview = renderContext?.editDiffPreview;
550
- const renderDiffFn = renderContext?.renderDiff ?? ((t: string) => t);
551
- const key = new Hasher().bool(expanded).u32(width).digest();
552
- if (cached?.key === key) return cached.lines;
553
-
554
- const firstChangedLine =
555
- (editDiffPreview && "firstChangedLine" in editDiffPreview ? editDiffPreview.firstChangedLine : undefined) ||
556
- (details && !isError ? details.firstChangedLine : undefined);
557
- const { description } = formatEditDescription(rawPath, uiTheme, { rename, firstChangedLine });
558
-
559
- const header = renderStatusLine(
560
- {
561
- icon: isError ? "error" : "success",
562
- title: getOperationTitle(op),
563
- description,
564
- },
565
- 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)),
566
572
  );
567
- let text = header;
568
- text += metadataLine;
569
-
570
- if (isError) {
571
- if (errorText) {
572
- text += `\n\n${uiTheme.fg("error", replaceTabs(errorText, rawPath))}`;
573
- }
574
- } else if (details?.diff) {
575
- text += renderDiffSection(details.diff, rawPath, expanded, uiTheme, renderDiffFn);
576
- } else if (editDiffPreview) {
577
- if ("error" in editDiffPreview) {
578
- text += `\n\n${uiTheme.fg("error", replaceTabs(editDiffPreview.error, rawPath))}`;
579
- } else if (editDiffPreview.diff) {
580
- text += renderDiffSection(editDiffPreview.diff, rawPath, expanded, uiTheme, renderDiffFn);
581
- }
582
- }
583
-
584
- if (details?.diagnostics) {
585
- text += formatDiagnostics(details.diagnostics, expanded, uiTheme, (fp: string) =>
586
- uiTheme.getLangIcon(getLanguageFromPath(fp)),
587
- );
588
- }
573
+ }
589
574
 
590
- const lines =
591
- width > 0 ? text.split("\n").flatMap(line => wrapEditRendererLine(line, width)) : text.split("\n");
592
- cached = { key, lines };
593
- return lines;
594
- },
595
- invalidate() {
596
- cached = undefined;
597
- },
598
- };
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
+ });
599
591
  }
600
592
 
601
593
  function renderMultiFileResult(
@@ -650,7 +642,7 @@ function renderMultiFileResult(
650
642
  },
651
643
  invalidate() {
652
644
  cached = undefined;
653
- for (const c of fileComponents) c.invalidate();
645
+ for (const c of fileComponents) c.invalidate?.();
654
646
  },
655
647
  };
656
648
  }