@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
package/src/tools/ask.ts CHANGED
@@ -16,22 +16,14 @@
16
16
  */
17
17
 
18
18
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
19
- import {
20
- type Component,
21
- Container,
22
- Markdown,
23
- type MarkdownTheme,
24
- renderInlineMarkdown,
25
- TERMINAL,
26
- Text,
27
- } from "@oh-my-pi/pi-tui";
19
+ import { type Component, Markdown, type MarkdownTheme, renderInlineMarkdown, TERMINAL, Text } from "@oh-my-pi/pi-tui";
28
20
  import { prompt, untilAborted } from "@oh-my-pi/pi-utils";
29
21
  import * as z from "zod/v4";
30
22
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
31
23
  import type { ExtensionUISelectItem } from "../extensibility/extensions";
32
24
  import { getMarkdownTheme, type Theme, theme } from "../modes/theme/theme";
33
25
  import askDescription from "../prompts/tools/ask.md" with { type: "text" };
34
- import { renderStatusLine } from "../tui";
26
+ import { framedBlock, renderStatusLine } from "../tui";
35
27
  import type { ToolSession } from ".";
36
28
  import { formatErrorMessage, formatMeta, formatTitle } from "./render-utils";
37
29
  import { ToolAbortError } from "./tool-errors";
@@ -626,23 +618,14 @@ interface AskRenderArgs {
626
618
  }>;
627
619
  }
628
620
 
629
- /** Render custom input as a single block with continuation lines (not one entry per line) */
630
- function renderCustomInput(
631
- uiTheme: Theme,
632
- prefix: string,
633
- customInput: string,
634
- isLastEntry: boolean,
635
- includeLeadingNewline = true,
636
- ): string {
621
+ /** Render a custom free-text answer as a status line plus indented continuation rows. */
622
+ function renderCustomInputLines(uiTheme: Theme, customInput: string): string[] {
637
623
  const lines = customInput.split("\n");
638
- const branch = isLastEntry ? uiTheme.tree.last : uiTheme.tree.branch;
639
- const firstLine = lines[0] ?? "";
640
- let text = `${includeLeadingNewline ? "\n" : ""}${prefix}${uiTheme.fg("dim", branch)} ${uiTheme.styledSymbol("status.success", "success")} ${uiTheme.fg("toolOutput", firstLine)}`;
641
- const continuationIndent = isLastEntry ? " " : `${uiTheme.fg("dim", uiTheme.tree.vertical)} `;
642
- for (let i = 1; i < lines.length; i++) {
643
- text += `\n${prefix}${continuationIndent} ${uiTheme.fg("toolOutput", lines[i])}`;
644
- }
645
- return text;
624
+ const out: string[] = [
625
+ ` ${uiTheme.styledSymbol("status.success", "success")} ${uiTheme.fg("toolOutput", lines[0] ?? "")}`,
626
+ ];
627
+ for (let i = 1; i < lines.length; i++) out.push(` ${uiTheme.fg("toolOutput", lines[i])}`);
628
+ return out;
646
629
  }
647
630
 
648
631
  /**
@@ -654,25 +637,38 @@ function optionMarker(uiTheme: Theme, multi: boolean | undefined, selected: bool
654
637
  return selected ? uiTheme.radio.selected : uiTheme.radio.unselected;
655
638
  }
656
639
 
640
+ /** Render the offered options for a question form as flat marker bullets (no tree guides). */
641
+ function renderQuestionOptionLines(
642
+ uiTheme: Theme,
643
+ mdTheme: MarkdownTheme,
644
+ options: AskRenderOption[],
645
+ multi: boolean | undefined,
646
+ ): string[] {
647
+ const out: string[] = [];
648
+ for (const opt of options) {
649
+ const optLabel = renderInlineMarkdown(opt.label, mdTheme, t => uiTheme.fg("muted", t));
650
+ out.push(` ${uiTheme.fg("dim", optionMarker(uiTheme, multi, false))} ${optLabel}`);
651
+ if (opt.description?.trim()) {
652
+ const description = renderInlineMarkdown(opt.description.trim(), mdTheme, t => uiTheme.fg("dim", t));
653
+ out.push(` ${uiTheme.fg("dim", "↳")} ${description}`);
654
+ }
655
+ }
656
+ return out;
657
+ }
658
+
657
659
  /**
658
660
  * Render the answered option list for a question: every offered option with its
659
- * selection marker filled in, plus any custom free-text answer. This keeps the
660
- * result visually identical to the question form (`renderCall`) so the answer
661
- * reads in place rather than as a detached summary block.
662
- *
663
- * `linePrefix` is the indent that precedes each entry's tree branch — a single
664
- * leading space for top-level (single-question) entries, or the question's
665
- * vertical continuation for nested (multi-question) entries.
661
+ * selection marker filled in, plus any custom free-text answer. Flat marker
662
+ * bullets the frame is the container, so no tree guides are drawn.
666
663
  */
667
- function renderAnswerOptions(
664
+ function renderAnswerOptionLines(
668
665
  uiTheme: Theme,
669
666
  mdTheme: MarkdownTheme,
670
- linePrefix: string,
671
667
  options: string[] | undefined,
672
668
  selectedOptions: string[] | undefined,
673
669
  multi: boolean | undefined,
674
670
  customInput: string | undefined,
675
- ): string {
671
+ ): string[] {
676
672
  const selected = new Set(selectedOptions ?? []);
677
673
  // Prefer the full recorded option set; fall back to the selected labels when
678
674
  // details omit the options array.
@@ -680,26 +676,21 @@ function renderAnswerOptions(
680
676
 
681
677
  // Nothing was chosen (and no custom answer) → a lone cancelled marker.
682
678
  if (selected.size === 0 && customInput === undefined) {
683
- return `${linePrefix}${uiTheme.fg("dim", uiTheme.tree.last)} ${uiTheme.styledSymbol("status.warning", "warning")} ${uiTheme.fg("warning", "Cancelled")}`;
679
+ return [` ${uiTheme.styledSymbol("status.warning", "warning")} ${uiTheme.fg("warning", "Cancelled")}`];
684
680
  }
685
681
 
686
- let text = "";
687
- for (let i = 0; i < list.length; i++) {
688
- const label = list[i];
682
+ const out: string[] = [];
683
+ for (const label of list) {
689
684
  const isSelected = selected.has(label);
690
- const isLastEntry = i === list.length - 1 && customInput === undefined;
691
- const branch = isLastEntry ? uiTheme.tree.last : uiTheme.tree.branch;
692
685
  const marker = optionMarker(uiTheme, multi, isSelected);
693
686
  const markerStyled = isSelected ? uiTheme.fg("success", marker) : uiTheme.fg("dim", marker);
694
687
  const labelStyled = renderInlineMarkdown(label, mdTheme, t =>
695
688
  isSelected ? uiTheme.fg("toolOutput", t) : uiTheme.fg("muted", t),
696
689
  );
697
- text += `${text ? "\n" : ""}${linePrefix}${uiTheme.fg("dim", branch)} ${markerStyled} ${labelStyled}`;
690
+ out.push(` ${markerStyled} ${labelStyled}`);
698
691
  }
699
- if (customInput !== undefined) {
700
- text += renderCustomInput(uiTheme, linePrefix, customInput, true, text.length > 0);
701
- }
702
- return text;
692
+ if (customInput !== undefined) out.push(...renderCustomInputLines(uiTheme, customInput));
693
+ return out;
703
694
  }
704
695
 
705
696
  export const askToolRenderer = {
@@ -708,80 +699,58 @@ export const askToolRenderer = {
708
699
  const label = formatTitle("Ask", uiTheme);
709
700
  const mdTheme = getMarkdownTheme();
710
701
  const accentStyle = { color: (t: string) => uiTheme.fg("accent", t) };
702
+ const md = (text: string, width: number) =>
703
+ new Markdown(text, 1, 0, mdTheme, accentStyle).render(Math.max(1, width - 3 + 1));
711
704
 
712
- // Multi-part questions
705
+ // Multi-part questions: one divider-labelled section per question.
713
706
  if (args.questions && args.questions.length > 0) {
714
- const container = new Container();
715
- container.addChild(new Text(`${label} ${uiTheme.fg("muted", `${args.questions.length} questions`)}`, 0, 0));
716
-
717
- for (let i = 0; i < args.questions.length; i++) {
718
- const q = args.questions[i];
719
- const isLastQ = i === args.questions.length - 1;
720
- const qBranch = isLastQ ? uiTheme.tree.last : uiTheme.tree.branch;
721
- const continuation = isLastQ ? " " : uiTheme.tree.vertical;
722
-
723
- const meta: string[] = [];
724
- if (q.multi) meta.push("multi");
725
- if (q.options?.length) meta.push(`options:${q.options.length}`);
726
- const metaStr = meta.length > 0 ? uiTheme.fg("dim", ` · ${meta.join(" · ")}`) : "";
727
-
728
- container.addChild(
729
- new Text(` ${uiTheme.fg("dim", qBranch)} ${uiTheme.fg("dim", `[${q.id}]`)}${metaStr}`, 0, 0),
730
- );
731
- container.addChild(new Markdown(q.question, 3, 0, mdTheme, accentStyle));
732
-
733
- if (q.options?.length) {
734
- let optText = "";
735
- for (let j = 0; j < q.options.length; j++) {
736
- const opt = q.options[j];
737
- const isLastOpt = j === q.options.length - 1;
738
- const optBranch = isLastOpt ? uiTheme.tree.last : uiTheme.tree.branch;
739
- const optLabel = renderInlineMarkdown(opt.label, mdTheme, t => uiTheme.fg("muted", t));
740
- optText += `\n ${uiTheme.fg("dim", continuation)} ${uiTheme.fg("dim", optBranch)} ${uiTheme.fg("dim", optionMarker(uiTheme, q.multi, false))} ${optLabel}`;
741
- if (opt.description?.trim()) {
742
- const optContinuation = isLastOpt ? " " : uiTheme.tree.vertical;
743
- const description = renderInlineMarkdown(opt.description.trim(), mdTheme, t =>
744
- uiTheme.fg("dim", t),
745
- );
746
- optText += `\n ${uiTheme.fg("dim", continuation)} ${uiTheme.fg("dim", optContinuation)} ${uiTheme.fg("dim", "↳")} ${description}`;
747
- }
748
- }
749
- container.addChild(new Text(optText, 0, 0));
750
- }
751
- }
752
- return container;
707
+ const questions = args.questions;
708
+ const header = `${label} ${uiTheme.fg("muted", `${questions.length} questions`)}`;
709
+ return framedBlock(uiTheme, width => {
710
+ const sections = questions.map(q => {
711
+ const meta: string[] = [];
712
+ if (q.multi) meta.push("multi");
713
+ if (q.options?.length) meta.push(`options:${q.options.length}`);
714
+ const metaStr = meta.length > 0 ? uiTheme.fg("dim", ` · ${meta.join(" · ")}`) : "";
715
+ const lines = md(q.question, width);
716
+ if (q.options?.length) lines.push(...renderQuestionOptionLines(uiTheme, mdTheme, q.options, q.multi));
717
+ return { label: `${uiTheme.fg("dim", `[${q.id}]`)}${metaStr}`, lines };
718
+ });
719
+ return { header, sections, state: "pending", borderColor: "borderMuted", width };
720
+ });
753
721
  }
754
722
 
755
723
  // Single question
756
724
  if (!args.question) {
757
- return new Text(formatErrorMessage("No question provided", uiTheme), 0, 0);
725
+ const errorLine = formatErrorMessage("No question provided", uiTheme);
726
+ return framedBlock(uiTheme, width => ({
727
+ header: errorLine,
728
+ sections: [],
729
+ state: "error",
730
+ borderColor: "error",
731
+ width,
732
+ }));
758
733
  }
759
734
 
760
- const container = new Container();
735
+ const question = args.question;
761
736
  const meta: string[] = [];
762
737
  if (args.multi) meta.push("multi");
763
738
  if (args.options?.length) meta.push(`options:${args.options.length}`);
764
- container.addChild(new Text(`${label}${formatMeta(meta, uiTheme)}`, 0, 0));
765
- container.addChild(new Markdown(args.question, 1, 0, mdTheme, accentStyle));
766
-
767
- if (args.options?.length) {
768
- let optText = "";
769
- for (let i = 0; i < args.options.length; i++) {
770
- const opt = args.options[i];
771
- const isLast = i === args.options.length - 1;
772
- const branch = isLast ? uiTheme.tree.last : uiTheme.tree.branch;
773
- const optLabel = renderInlineMarkdown(opt.label, mdTheme, t => uiTheme.fg("muted", t));
774
- optText += `\n ${uiTheme.fg("dim", branch)} ${uiTheme.fg("dim", optionMarker(uiTheme, args.multi, false))} ${optLabel}`;
775
- if (opt.description?.trim()) {
776
- const continuation = isLast ? " " : uiTheme.tree.vertical;
777
- const description = renderInlineMarkdown(opt.description.trim(), mdTheme, t => uiTheme.fg("dim", t));
778
- optText += `\n ${uiTheme.fg("dim", continuation)} ${uiTheme.fg("dim", "↳")} ${description}`;
779
- }
780
- }
781
- container.addChild(new Text(optText, 0, 0));
782
- }
783
-
784
- return container;
739
+ const header = `${label}${formatMeta(meta, uiTheme)}`;
740
+ const questionOptions = args.options;
741
+ const multi = args.multi;
742
+ return framedBlock(uiTheme, width => {
743
+ const bodyLines = md(question, width);
744
+ if (questionOptions?.length)
745
+ bodyLines.push(...renderQuestionOptionLines(uiTheme, mdTheme, questionOptions, multi));
746
+ return {
747
+ header,
748
+ sections: bodyLines.length > 0 ? [{ lines: bodyLines }] : [],
749
+ state: "pending",
750
+ borderColor: "borderMuted",
751
+ width,
752
+ };
753
+ });
785
754
  },
786
755
 
787
756
  renderResult(
@@ -792,56 +761,47 @@ export const askToolRenderer = {
792
761
  const { details } = result;
793
762
  const mdTheme = getMarkdownTheme();
794
763
  const accentStyle = { color: (t: string) => uiTheme.fg("accent", t) };
764
+ const md = (text: string, width: number) =>
765
+ new Markdown(text, 1, 0, mdTheme, accentStyle).render(Math.max(1, width - 3 + 1));
795
766
 
796
767
  if (!details) {
797
768
  const txt = result.content[0];
798
769
  const fallback = txt?.type === "text" && txt.text ? txt.text : "";
799
770
  const header = renderStatusLine({ icon: "warning", title: "Ask" }, uiTheme);
800
- return new Text(`${header}\n${uiTheme.fg("dim", fallback)}`, 0, 0);
771
+ const body = fallback ? `\n${uiTheme.fg("dim", fallback)}` : "";
772
+ return new Text(`${header}${body}`, 0, 0);
801
773
  }
802
774
 
803
- // Multi-part results
775
+ // Multi-part results: one divider-labelled section per question.
804
776
  if (details.results && details.results.length > 0) {
805
- const hasAnySelection = details.results.some(
777
+ const results = details.results;
778
+ const hasAnySelection = results.some(
806
779
  r => r.customInput !== undefined || (r.selectedOptions && r.selectedOptions.length > 0),
807
780
  );
808
781
  const header = renderStatusLine(
809
782
  {
810
783
  icon: hasAnySelection ? "success" : "warning",
811
784
  title: "Ask",
812
- meta: [`${details.results.length} questions`],
785
+ meta: [`${results.length} questions`],
813
786
  },
814
787
  uiTheme,
815
788
  );
816
- const container = new Container();
817
- container.addChild(new Text(header, 0, 0));
818
-
819
- for (let i = 0; i < details.results.length; i++) {
820
- const r = details.results[i];
821
- const isLastQuestion = i === details.results.length - 1;
822
- const qBranch = isLastQuestion ? uiTheme.tree.last : uiTheme.tree.branch;
823
- const continuation = isLastQuestion ? " " : uiTheme.tree.vertical;
824
- const linePrefix = ` ${uiTheme.fg("dim", continuation)} `;
825
-
826
- container.addChild(new Text(` ${uiTheme.fg("dim", qBranch)} ${uiTheme.fg("dim", `[${r.id}]`)}`, 0, 0));
827
- container.addChild(new Markdown(r.question, 3, 0, mdTheme, accentStyle));
828
- container.addChild(
829
- new Text(
830
- renderAnswerOptions(
831
- uiTheme,
832
- mdTheme,
833
- linePrefix,
834
- r.options,
835
- r.selectedOptions,
836
- r.multi,
837
- r.customInput,
838
- ),
839
- 0,
840
- 0,
841
- ),
842
- );
843
- }
844
- return container;
789
+ return framedBlock(uiTheme, width => {
790
+ const sections = results.map(r => {
791
+ const lines = md(r.question, width);
792
+ lines.push(
793
+ ...renderAnswerOptionLines(uiTheme, mdTheme, r.options, r.selectedOptions, r.multi, r.customInput),
794
+ );
795
+ return { label: uiTheme.fg("dim", `[${r.id}]`), lines };
796
+ });
797
+ return {
798
+ header,
799
+ sections,
800
+ state: hasAnySelection ? "success" : "warning",
801
+ borderColor: "borderMuted",
802
+ width,
803
+ };
804
+ });
845
805
  }
846
806
 
847
807
  // Single question result
@@ -851,29 +811,24 @@ export const askToolRenderer = {
851
811
  return new Text(fallback, 0, 0);
852
812
  }
853
813
 
814
+ const question = details.question;
854
815
  const hasSelection =
855
816
  details.customInput !== undefined || (details.selectedOptions && details.selectedOptions.length > 0);
856
817
  const header = renderStatusLine({ icon: hasSelection ? "success" : "warning", title: "Ask" }, uiTheme);
857
- const container = new Container();
858
- container.addChild(new Text(header, 0, 0));
859
- container.addChild(new Markdown(details.question, 1, 0, mdTheme, accentStyle));
860
-
861
- container.addChild(
862
- new Text(
863
- renderAnswerOptions(
864
- uiTheme,
865
- mdTheme,
866
- " ",
867
- details.options,
868
- details.selectedOptions,
869
- details.multi,
870
- details.customInput,
871
- ),
872
- 0,
873
- 0,
874
- ),
875
- );
876
-
877
- return container;
818
+ const dOptions = details.options;
819
+ const dSelected = details.selectedOptions;
820
+ const dMulti = details.multi;
821
+ const dCustom = details.customInput;
822
+ return framedBlock(uiTheme, width => {
823
+ const bodyLines = md(question, width);
824
+ bodyLines.push(...renderAnswerOptionLines(uiTheme, mdTheme, dOptions, dSelected, dMulti, dCustom));
825
+ return {
826
+ header,
827
+ sections: bodyLines.length > 0 ? [{ lines: bodyLines }] : [],
828
+ state: hasSelection ? "success" : "warning",
829
+ borderColor: "borderMuted",
830
+ width,
831
+ };
832
+ });
878
833
  },
879
834
  };
@@ -3,7 +3,7 @@ import { formatHashlineHeader } from "@oh-my-pi/hashline";
3
3
  import type { AgentTool, AgentToolContext, AgentToolResult, AgentToolUpdateCallback } from "@oh-my-pi/pi-agent-core";
4
4
  import { type AstReplaceChange, type AstReplaceFileChange, astEdit } from "@oh-my-pi/pi-natives";
5
5
  import type { Component } from "@oh-my-pi/pi-tui";
6
- import { Text } from "@oh-my-pi/pi-tui";
6
+ import { replaceTabs, Text } from "@oh-my-pi/pi-tui";
7
7
  import { $envpos, prompt, untilAborted } from "@oh-my-pi/pi-utils";
8
8
  import * as z from "zod/v4";
9
9
  import { getFileSnapshotStore } from "../edit/file-snapshot-store";
@@ -11,26 +11,24 @@ import { normalizeToLF } from "../edit/normalize";
11
11
  import type { RenderResultOptions } from "../extensibility/custom-tools/types";
12
12
  import type { Theme } from "../modes/theme/theme";
13
13
  import astEditDescription from "../prompts/tools/ast-edit.md" with { type: "text" };
14
- import { Ellipsis, fileHyperlink, renderStatusLine, renderTreeList, truncateToWidth } from "../tui";
14
+ import { Ellipsis, fileHyperlink, framedBlock, renderStatusLine, truncateToWidth } from "../tui";
15
15
  import { resolveFileDisplayMode } from "../utils/file-display-mode";
16
16
  import type { ToolSession } from ".";
17
17
  import { truncateForPrompt } from "./approval";
18
18
  import { createFileRecorder, formatResultPath } from "./file-recorder";
19
- import { formatGroupedFiles } from "./grouped-file-output";
19
+ import { classifyGroupedLines, formatGroupedFiles, groupLineIndicesByBlank } from "./grouped-file-output";
20
20
  import type { OutputMeta } from "./output-meta";
21
21
  import { isInternalUrlPath, resolveToolSearchScope } from "./path-utils";
22
22
  import {
23
23
  appendParseErrorsBulletList,
24
24
  capParseErrors,
25
- createCachedComponent,
26
25
  formatCodeFrameLine,
27
26
  formatCount,
28
- formatEmptyMessage,
29
- formatErrorMessage,
27
+ formatErrorDetail,
28
+ formatMoreItems,
30
29
  formatParseErrors,
31
30
  formatParseErrorsCountLabel,
32
31
  PREVIEW_LIMITS,
33
- splitGroupsByBlankLine,
34
32
  } from "./render-utils";
35
33
  import { queueResolveHandler } from "./resolve";
36
34
  import { ToolError } from "./tool-errors";
@@ -161,6 +159,9 @@ export interface AstEditToolDetails {
161
159
  /** Absolute base directory used during the edit. Used by the renderer to resolve
162
160
  * display-relative paths to absolute paths for OSC 8 hyperlinks. */
163
161
  searchPath?: string;
162
+ /** Session cwd at edit time. Display header paths are cwd-relative, so the
163
+ * renderer resolves them against this; `searchPath` is the scope target. */
164
+ cwd?: string;
164
165
  }
165
166
 
166
167
  export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolDetails> {
@@ -274,6 +275,7 @@ export class AstEditTool implements AgentTool<typeof astEditSchema, AstEditToolD
274
275
  ...(cappedParseErrors.length > 0 ? { parseErrors: cappedParseErrors, parseErrorsTotal } : {}),
275
276
  scopePath,
276
277
  searchPath: resolvedSearchPath,
278
+ cwd: this.session.cwd,
277
279
  files: fileList,
278
280
  fileReplacements: [],
279
281
  };
@@ -464,6 +466,32 @@ interface AstEditRenderArgs {
464
466
 
465
467
  const COLLAPSED_CHANGE_LIMIT = PREVIEW_LIMITS.COLLAPSED_LINES * 2;
466
468
 
469
+ /**
470
+ * Flatten pre-styled change groups into frame body lines. Groups are separated
471
+ * by a blank line and carry no tree guides — the frame border is the container,
472
+ * so nested `├─ │` gutters would just be noise. Collapsed mode always shows at
473
+ * least the first group, then fills up to `budget` lines before summarizing the
474
+ * rest as `… N more changes`.
475
+ */
476
+ function buildChangeBody(groups: string[][], expanded: boolean, budget: number, theme: Theme): string[] {
477
+ const lines: string[] = [];
478
+ let shown = 0;
479
+ for (let i = 0; i < groups.length; i++) {
480
+ const group = groups[i]!;
481
+ const separator = shown > 0 ? 1 : 0;
482
+ const remainingAfter = groups.length - (i + 1);
483
+ const reserved = !expanded && remainingAfter > 0 ? 1 : 0;
484
+ // Always emit the first group; budget only gates subsequent ones.
485
+ if (!expanded && shown > 0 && lines.length + separator + group.length + reserved > budget) break;
486
+ if (separator) lines.push("");
487
+ lines.push(...group);
488
+ shown++;
489
+ }
490
+ const remaining = groups.length - shown;
491
+ if (!expanded && remaining > 0) lines.push(theme.fg("muted", formatMoreItems(remaining, "change")));
492
+ return lines;
493
+ }
494
+
467
495
  export const astEditToolRenderer = {
468
496
  inline: true,
469
497
  renderCall(args: AstEditRenderArgs, _options: RenderResultOptions, uiTheme: Theme): Component {
@@ -473,8 +501,9 @@ export const astEditToolRenderer = {
473
501
  if (rewriteCount > 1) meta.push(`${rewriteCount} rewrites`);
474
502
 
475
503
  const description = rewriteCount === 1 ? args.ops?.[0]?.pat : rewriteCount ? `${rewriteCount} rewrites` : "?";
476
- const text = renderStatusLine({ icon: "pending", title: "AST Edit", description, meta }, uiTheme);
477
- return new Text(text, 0, 0);
504
+ const header = renderStatusLine({ icon: "pending", title: "AST Edit", description, meta }, uiTheme);
505
+ // Pending call has no body yet — a lone status line is sleeker than an empty frame.
506
+ return new Text(header, 0, 0);
478
507
  },
479
508
 
480
509
  renderResult(
@@ -487,7 +516,14 @@ export const astEditToolRenderer = {
487
516
 
488
517
  if (result.isError) {
489
518
  const errorText = result.content?.find(c => c.type === "text")?.text || "Unknown error";
490
- return new Text(formatErrorMessage(errorText, uiTheme), 0, 0);
519
+ const header = renderStatusLine({ icon: "error", title: "AST Edit" }, uiTheme);
520
+ return framedBlock(uiTheme, width => ({
521
+ header,
522
+ sections: [{ lines: formatErrorDetail(errorText, uiTheme).split("\n") }],
523
+ state: "error",
524
+ borderColor: "error",
525
+ width,
526
+ }));
491
527
  }
492
528
 
493
529
  const totalReplacements = details?.totalReplacements ?? 0;
@@ -502,9 +538,18 @@ export const astEditToolRenderer = {
502
538
  if (details?.scopePath) meta.push(`in ${details.scopePath}`);
503
539
  if (filesSearched > 0) meta.push(`searched ${filesSearched}`);
504
540
  const header = renderStatusLine({ icon: "warning", title: "AST Edit", description, meta }, uiTheme);
505
- const lines = [header, formatEmptyMessage("No replacements made", uiTheme)];
506
- appendParseErrorsBulletList(lines, details?.parseErrors, uiTheme, details?.parseErrorsTotal);
507
- return new Text(lines.join("\n"), 0, 0);
541
+ // The "0 replacements" count already rides on the status line; only parse
542
+ // errors are worth a body, so frame solely when there are some.
543
+ const bodyLines: string[] = [];
544
+ appendParseErrorsBulletList(bodyLines, details?.parseErrors, uiTheme, details?.parseErrorsTotal);
545
+ if (bodyLines.length === 0) return new Text(header, 0, 0);
546
+ return framedBlock(uiTheme, width => ({
547
+ header,
548
+ sections: [{ lines: bodyLines }],
549
+ state: "warning",
550
+ borderColor: "borderMuted",
551
+ width,
552
+ }));
508
553
  }
509
554
 
510
555
  const summaryParts = [formatCount("replacement", totalReplacements), formatCount("file", filesTouched)];
@@ -516,10 +561,33 @@ export const astEditToolRenderer = {
516
561
  const description = rewriteCount === 1 ? args?.ops?.[0]?.pat : undefined;
517
562
 
518
563
  const textContent = result.details?.displayContent ?? result.content?.find(c => c.type === "text")?.text ?? "";
519
- const allGroups = splitGroupsByBlankLine(textContent.split("\n"));
520
- const changeGroups = allGroups.filter(
521
- group => !group[0]?.startsWith("Safety cap reached") && !group[0]?.startsWith("Parse issues:"),
522
- );
564
+ const allLines = textContent.split("\n");
565
+ // Resolve hyperlinks over the whole output so nested directory headers
566
+ // reconstruct across the blank-line groups the tree list collapses by.
567
+ const contexts = classifyGroupedLines(allLines, details?.cwd ?? details?.searchPath, details?.searchPath);
568
+ const styledLines = allLines.map((line, index) => {
569
+ const ctx = contexts[index]!;
570
+ // Swap the inner code-frame gutter `│` for a space so it does not nest a
571
+ // second vertical bar inside the frame border.
572
+ const display = replaceTabs(line.replace("│", " "));
573
+ if (ctx.kind === "dir") {
574
+ const styled = uiTheme.fg("accent", display);
575
+ return ctx.headerPath ? fileHyperlink(ctx.headerPath, styled) : styled;
576
+ }
577
+ if (ctx.kind === "file") {
578
+ const styled = uiTheme.fg(ctx.depth === 1 ? "accent" : "dim", display);
579
+ return ctx.headerPath ? fileHyperlink(ctx.headerPath, styled) : styled;
580
+ }
581
+ if (display.startsWith("+")) return uiTheme.fg("toolDiffAdded", display);
582
+ if (display.startsWith("-")) return uiTheme.fg("toolDiffRemoved", display);
583
+ return uiTheme.fg("toolOutput", display);
584
+ });
585
+ const changeGroups = groupLineIndicesByBlank(allLines)
586
+ .filter(indices => {
587
+ const first = allLines[indices[0]!]!;
588
+ return !first.startsWith("Safety cap reached") && !first.startsWith("Parse issues:");
589
+ })
590
+ .map(indices => indices.map(index => styledLines[index]!));
523
591
 
524
592
  const badge = { label: "proposed", color: "warning" as const };
525
593
  const header = renderStatusLine(
@@ -536,60 +604,19 @@ export const astEditToolRenderer = {
536
604
  uiTheme.fg("warning", formatParseErrorsCountLabel(details.parseErrors, details.parseErrorsTotal)),
537
605
  );
538
606
  }
539
- return createCachedComponent(
540
- () => options.expanded,
541
- width => {
542
- const searchBase = details?.searchPath;
543
- const changeLines = renderTreeList(
544
- {
545
- items: changeGroups,
546
- expanded: options.expanded,
547
- maxCollapsed: changeGroups.length,
548
- maxCollapsedLines: COLLAPSED_CHANGE_LIMIT,
549
- itemType: "change",
550
- renderItem: group => {
551
- let contextDir = searchBase ?? "";
552
- return group.map(line => {
553
- if (line.startsWith("## ")) {
554
- // Strip ` (3 replacements)` and `#hash` suffixes from formatGroupedFiles.
555
- const fileName = line
556
- .slice(3)
557
- .trimEnd()
558
- .replace(/\s+\([^)]*\)\s*$/, "")
559
- .replace(/#[0-9a-f]+$/, "");
560
- const absPath = contextDir && fileName ? path.join(contextDir, fileName) : undefined;
561
- const styled = uiTheme.fg("dim", line);
562
- return absPath ? fileHyperlink(absPath, styled) : styled;
563
- }
564
- if (line.startsWith("# ")) {
565
- const raw = line
566
- .slice(2)
567
- .trimEnd()
568
- .replace(/\s+\([^)]*\)\s*$/, "");
569
- const isDirectory = raw.endsWith("/");
570
- const name = isDirectory ? raw.replace(/\/$/, "") : raw.replace(/#[0-9a-f]+$/, "");
571
- if (isDirectory) {
572
- if (searchBase) {
573
- contextDir = name === "." ? searchBase : path.join(searchBase, name);
574
- }
575
- return uiTheme.fg("accent", line);
576
- }
577
- // Root-level file with optional `#hash` and ` (3 replacements)` suffixes.
578
- const absPath = searchBase && name ? path.join(searchBase, name) : undefined;
579
- const styled = uiTheme.fg("accent", line);
580
- return absPath ? fileHyperlink(absPath, styled) : styled;
581
- }
582
- if (line.startsWith("+")) return uiTheme.fg("toolDiffAdded", line);
583
- if (line.startsWith("-")) return uiTheme.fg("toolDiffRemoved", line);
584
- return uiTheme.fg("toolOutput", line);
585
- });
586
- },
587
- },
588
- uiTheme,
589
- );
590
- return [header, ...changeLines, ...extraLines].map(l => truncateToWidth(l, width, Ellipsis.Omit));
591
- },
592
- );
607
+ return framedBlock(uiTheme, width => {
608
+ const changeLines = buildChangeBody(changeGroups, Boolean(options.expanded), COLLAPSED_CHANGE_LIMIT, uiTheme);
609
+ const innerWidth = Math.max(1, width - 3);
610
+ const bodyLines = [...changeLines, ...extraLines].map(l => truncateToWidth(l, innerWidth, Ellipsis.Omit));
611
+ while (bodyLines.length > 0 && bodyLines[0].trim() === "") bodyLines.shift();
612
+ return {
613
+ header,
614
+ sections: bodyLines.length > 0 ? [{ lines: bodyLines }] : [],
615
+ state: options.isPartial ? "pending" : "success",
616
+ borderColor: "borderMuted",
617
+ width,
618
+ };
619
+ });
593
620
  },
594
621
  mergeCallAndResult: true,
595
622
  };