@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
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Pure heading/section parser for the plan-review overlay. It splits a plan's
3
+ * markdown into a flat list of sections — a leading preamble (text before the
4
+ * first heading) followed by one entry per ATX heading — preserving the exact
5
+ * source bytes of each section so the overlay can render, reorder-free delete,
6
+ * and round-trip the document without a full markdown re-render.
7
+ *
8
+ * No TUI dependencies: this module is unit-tested in isolation.
9
+ */
10
+
11
+ /** ATX heading: 1-6 `#`, required whitespace, a title, optional closing `#`s. */
12
+ const HEADING_RE = /^(#{1,6})[ \t]+(.+?)[ \t]*#*[ \t]*$/;
13
+ /** Opening/closing code fence run (``` or ~~~), allowing up to 3 lead spaces. */
14
+ const FENCE_RE = /^ {0,3}(`{3,}|~{3,})(.*)$/;
15
+
16
+ export interface PlanSection {
17
+ /** `0` = preamble (no heading, no ToC entry); `1..6` = heading depth. */
18
+ level: number;
19
+ /** Plain-text heading label with inline markdown lightly stripped. */
20
+ title: string;
21
+ /** Exact source slice for this section, including its trailing newline(s). */
22
+ raw: string;
23
+ }
24
+
25
+ /**
26
+ * Collapse inline markdown emphasis/link/code syntax to readable text. This is
27
+ * a deliberately light strip (not a full markdown render) just so ToC entries
28
+ * read cleanly — `**Goal** & [docs](x)` becomes `Goal & docs`.
29
+ */
30
+ export function stripInlineMarkdown(text: string): string {
31
+ let out = text;
32
+ // Images first (so the link pass below does not eat the `(url)`), then links.
33
+ out = out.replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1");
34
+ out = out.replace(/\[([^\]]*)\]\([^)]*\)/g, "$1");
35
+ out = out.replace(/\[([^\]]*)\]\[[^\]]*\]/g, "$1");
36
+ // Autolinks `<https://…>` keep their URL as the readable text.
37
+ out = out.replace(/<([^>\s]+)>/g, "$1");
38
+ // Inline code, then bold/italic/strikethrough emphasis runs.
39
+ out = out.replace(/`([^`]+)`/g, "$1");
40
+ out = out.replace(/(\*\*|__)(.+?)\1/g, "$2");
41
+ out = out.replace(/(\*|_)(.+?)\1/g, "$2");
42
+ out = out.replace(/~~(.+?)~~/g, "$1");
43
+ return out.replace(/\s+/g, " ").trim();
44
+ }
45
+
46
+ /**
47
+ * Split `text` into preamble + heading sections. `#` characters inside fenced
48
+ * code blocks are never treated as headings. Concatenating every section's
49
+ * `raw` reproduces the original text exactly.
50
+ */
51
+ export function parsePlanSections(text: string): PlanSection[] {
52
+ const lines = text.split("\n");
53
+ // Character offset of each line start so section `raw` can slice the source.
54
+ const offsets: number[] = new Array(lines.length);
55
+ let cursor = 0;
56
+ for (let i = 0; i < lines.length; i++) {
57
+ offsets[i] = cursor;
58
+ cursor += lines[i]!.length + 1; // +1 for the "\n" join separator
59
+ }
60
+
61
+ // Heading line indices (start of each heading section), with metadata.
62
+ const heads: { line: number; level: number; title: string }[] = [];
63
+ let fenceChar: string | null = null;
64
+ let fenceLen = 0;
65
+ for (let i = 0; i < lines.length; i++) {
66
+ const line = lines[i]!;
67
+ const fence = FENCE_RE.exec(line);
68
+ if (fenceChar === null) {
69
+ if (fence) {
70
+ fenceChar = fence[1]![0]!;
71
+ fenceLen = fence[1]!.length;
72
+ }
73
+ // Opening-fence lines are body, not headings.
74
+ if (fence) continue;
75
+ } else {
76
+ // Inside a fence: only a matching-or-longer run of the same char closes.
77
+ if (fence && fence[1]![0] === fenceChar && fence[1]!.length >= fenceLen && fence[2]!.trim() === "") {
78
+ fenceChar = null;
79
+ fenceLen = 0;
80
+ }
81
+ continue;
82
+ }
83
+ const heading = HEADING_RE.exec(line);
84
+ if (heading) {
85
+ heads.push({ line: i, level: heading[1]!.length, title: stripInlineMarkdown(heading[2]!) });
86
+ }
87
+ }
88
+
89
+ const sections: PlanSection[] = [];
90
+ const sliceRaw = (startLine: number, endLine: number): string => {
91
+ const startOffset = offsets[startLine]!;
92
+ const endOffset = endLine < lines.length ? offsets[endLine]! : text.length;
93
+ return text.slice(startOffset, endOffset);
94
+ };
95
+
96
+ // Preamble: everything before the first heading (only when non-empty).
97
+ const firstHeadLine = heads.length > 0 ? heads[0]!.line : lines.length;
98
+ if (firstHeadLine > 0) {
99
+ const raw = sliceRaw(0, firstHeadLine);
100
+ if (raw.length > 0) sections.push({ level: 0, title: "", raw });
101
+ }
102
+
103
+ for (let h = 0; h < heads.length; h++) {
104
+ const head = heads[h]!;
105
+ const endLine = h + 1 < heads.length ? heads[h + 1]!.line : lines.length;
106
+ sections.push({ level: head.level, title: head.title, raw: sliceRaw(head.line, endLine) });
107
+ }
108
+
109
+ return sections;
110
+ }
111
+
112
+ /**
113
+ * Concatenate every section's `raw` back into a single document and guarantee a
114
+ * single trailing newline. Inverse of {@link parsePlanSections} for any input
115
+ * that already ends with a newline.
116
+ */
117
+ export function joinPlanSections(sections: readonly PlanSection[]): string {
118
+ let joined = "";
119
+ for (const section of sections) joined += section.raw;
120
+ if (joined.length === 0) return "";
121
+ return joined.endsWith("\n") ? joined : `${joined}\n`;
122
+ }
123
+
124
+ /**
125
+ * Indices to remove when deleting `sections[index]`: the heading itself plus
126
+ * every following section nested deeper than it (its sub-headings). The
127
+ * preamble (level 0) is never a deletion target and yields an empty span.
128
+ */
129
+ export function sectionDeletionSpan(sections: readonly PlanSection[], index: number): number[] {
130
+ const target = sections[index];
131
+ if (!target || target.level === 0) return [];
132
+ const span = [index];
133
+ for (let j = index + 1; j < sections.length; j++) {
134
+ if (sections[j]!.level > target.level) span.push(j);
135
+ else break;
136
+ }
137
+ return span;
138
+ }
@@ -81,6 +81,14 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
81
81
  #text: Text;
82
82
  #expanded = false;
83
83
  #showContentPreview: boolean;
84
+ // A read group accretes entries across multiple assistant completions for as
85
+ // long as the run of reads is uninterrupted. While it is the active group it
86
+ // must stay in the transcript's repaintable live region — its header line
87
+ // re-layouts from `Read <path>` to `Read (N)` + tree as entries arrive, so a
88
+ // frozen snapshot taken on a risk terminal would strand the single-entry form
89
+ // (see TranscriptContainer / NativeScrollbackLiveRegion). The controller calls
90
+ // `finalize()` once the run breaks so the block can commit to native scrollback.
91
+ #finalized = false;
84
92
 
85
93
  constructor(options: ReadToolGroupOptions = {}) {
86
94
  super();
@@ -90,6 +98,14 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
90
98
  this.#updateDisplay();
91
99
  }
92
100
 
101
+ isTranscriptBlockFinalized(): boolean {
102
+ return this.#finalized;
103
+ }
104
+
105
+ finalize(): void {
106
+ this.#finalized = true;
107
+ }
108
+
93
109
  updateArgs(args: ReadRenderArgs, toolCallId?: string): void {
94
110
  if (!toolCallId) return;
95
111
  const basePath = args.file_path || args.path || "";
@@ -181,9 +197,9 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
181
197
  const total = entriesWithoutPreview.length;
182
198
  for (const [index, entry] of entriesWithoutPreview.entries()) {
183
199
  const connector = index === total - 1 ? theme.tree.last : theme.tree.branch;
184
- const statusSymbol = this.#formatStatus(entry.status);
200
+ const statusPrefix = entry.status === "success" ? "" : `${this.#formatStatus(entry.status)} `;
185
201
  const pathDisplay = this.#formatPath(entry);
186
- lines.push(` ${theme.fg("dim", connector)} ${statusSymbol} ${pathDisplay}`.trimEnd());
202
+ lines.push(` ${theme.fg("dim", connector)} ${statusPrefix}${pathDisplay}`.trimEnd());
187
203
  }
188
204
 
189
205
  this.#text.setText(lines.join("\n"));
@@ -198,7 +214,7 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
198
214
 
199
215
  /**
200
216
  * Add a code-cell content preview below the entry summary.
201
- * When collapsed: shows first COLLAPSED_PREVIEW_LINES lines with "… N more lines (Ctrl+O for more)" hint.
217
+ * When collapsed: shows first COLLAPSED_PREVIEW_LINES lines with a "… N more lines ⟨<key>: Expand⟩" hint.
202
218
  * When expanded: shows full content.
203
219
  */
204
220
  #addContentPreview(entry: ReadEntry): void {
@@ -254,7 +270,7 @@ export class ReadToolGroupComponent extends Container implements ToolExecutionHa
254
270
 
255
271
  #formatStatus(status: ReadEntry["status"]): string {
256
272
  if (status === "success") {
257
- return theme.fg("success", theme.status.success);
273
+ return theme.fg("text", theme.status.enabled);
258
274
  }
259
275
  if (status === "warning") {
260
276
  return theme.fg("warning", theme.status.warning);
@@ -11,7 +11,6 @@ export class SkillMessageComponent extends Container {
11
11
 
12
12
  constructor(private readonly message: CustomMessage<SkillPromptDetails>) {
13
13
  super();
14
- this.addChild(new Spacer(1));
15
14
 
16
15
  this.#box = new Box(1, 1, t => theme.bg("customMessageBg", t));
17
16
  this.#rebuild();
@@ -546,7 +546,7 @@ export class StatusLineComponent implements Component {
546
546
  return `${modelId}|${sp.length}:${sp[0]?.length ?? 0}|${tools.length}|${skills.length}`;
547
547
  }
548
548
 
549
- #buildSegmentContext(width: number): SegmentContext {
549
+ #buildSegmentContext(width: number, segmentOptions: StatusLineSettings["segmentOptions"]): SegmentContext {
550
550
  const state = this.session.state;
551
551
 
552
552
  // Trigger background fetch (5-min TTL); render uses cached value
@@ -575,7 +575,7 @@ export class StatusLineComponent implements Component {
575
575
  return {
576
576
  session: this.session,
577
577
  width,
578
- options: this.#resolveSettings().segmentOptions ?? {},
578
+ options: segmentOptions ?? {},
579
579
  planMode: this.#planModeStatus,
580
580
  loopMode: this.#loopModeStatus,
581
581
  goalMode: this.#goalModeStatus,
@@ -632,8 +632,8 @@ export class StatusLineComponent implements Component {
632
632
  }
633
633
 
634
634
  #buildStatusLine(width: number): string {
635
- const ctx = this.#buildSegmentContext(width);
636
635
  const effectiveSettings = this.#resolveSettings();
636
+ const ctx = this.#buildSegmentContext(width, effectiveSettings.segmentOptions);
637
637
  const separatorDef = getSeparator(effectiveSettings.separator ?? "powerline-thin", theme);
638
638
 
639
639
  const bgAnsi = theme.getBgAnsi("statusLineBg");
@@ -759,8 +759,6 @@ export class StatusLineComponent implements Component {
759
759
  return leftGroup + (leftGroup && rightGroup ? " " : "") + rightGroup;
760
760
  }
761
761
 
762
- leftWidth = groupWidth(left, leftCapWidth, leftSepWidth);
763
- rightWidth = groupWidth(right, rightCapWidth, rightSepWidth);
764
762
  const gapWidth = Math.max(1, topFillWidth - leftWidth - rightWidth);
765
763
  const sessionName =
766
764
  effectiveSettings.sessionAccent !== false ? this.session.sessionManager?.getSessionName() : undefined;
@@ -1,5 +1,6 @@
1
1
  Tired of typing "keep going"? Just send a '.'
2
2
  You can /btw to ask a side question
3
+ Use /tan to fork the current conversation into a background agent
3
4
  Ctrl+D can be used to exit, but with your draft saved!
4
5
  Find out which model you emotionally abuse the most with `omp stats`
5
6
  Try task isolation to create CoW worktrees
@@ -16,8 +16,6 @@ export class TodoReminderComponent extends Container {
16
16
  ) {
17
17
  super();
18
18
 
19
- this.addChild(new Spacer(1));
20
-
21
19
  this.#box = new Box(1, 1, t => theme.inverse(theme.fg("warning", t)));
22
20
  this.addChild(this.#box);
23
21
 
@@ -31,26 +31,12 @@ import {
31
31
  renderJsonTreeLines,
32
32
  } from "../../tools/json-tree";
33
33
  import { formatExpandHint, replaceTabs, resolveImageOptions, truncateToWidth } from "../../tools/render-utils";
34
- import { toolRenderers } from "../../tools/renderers";
34
+ import { type ToolRenderer, toolRenderers } from "../../tools/renderers";
35
35
  import { TODO_STRIKE_TOTAL_FRAMES } from "../../tools/todo";
36
36
  import { isFramedBlockComponent, renderStatusLine } from "../../tui";
37
37
  import { sanitizeWithOptionalSixelPassthrough } from "../../utils/sixel";
38
38
  import { renderDiff } from "./diff";
39
39
 
40
- function ensureInvalidate(component: unknown): Component {
41
- const c = component as { render: Component["render"]; invalidate?: () => void };
42
- if (!c.invalidate) {
43
- c.invalidate = () => {};
44
- }
45
- return c as Component;
46
- }
47
-
48
- function addBoxChild(box: Box, component: unknown): boolean {
49
- const child = ensureInvalidate(component);
50
- box.addChild(child);
51
- return isFramedBlockComponent(child);
52
- }
53
-
54
40
  /**
55
41
  * Drop trailing removal/hunk-header lines that appear in a streaming diff
56
42
  * before the matching `+added` lines have arrived. Without this, a partial
@@ -147,12 +133,12 @@ export interface ToolExecutionHandle {
147
133
  setExpanded(expanded: boolean): void;
148
134
  }
149
135
 
150
- /** Drive pending-tool redraws at ~60fps so the animated border sweep is smooth.
151
- * The TUI already throttles at its 16ms `MIN_RENDER_INTERVAL_MS`, so this is the
152
- * natural upper bound and static frames diff to a no-op redraw at ~zero cost. */
153
- const SPINNER_RENDER_INTERVAL_MS = 16;
136
+ /** Drive pending-tool redraws at 30fps so the animated border sweep stays
137
+ * smooth without spending twice the frame budget. The TUI throttles at the same
138
+ * cadence, and static frames diff to a no-op redraw at ~zero cost. */
139
+ const SPINNER_RENDER_INTERVAL_MS = 1000 / 30;
154
140
  /** Advance the spinner glyph at its classic ~12.5fps step, decoupled from the
155
- * 60fps render cadence (mirrors `Loader`). */
141
+ * render cadence (mirrors `Loader`). */
156
142
  const SPINNER_GLYPH_ADVANCE_MS = 80;
157
143
 
158
144
  // Stable per-instance counter so each tool execution's inline images get a
@@ -239,11 +225,13 @@ export class ToolExecutionComponent extends Container {
239
225
  this.#cwd = cwd;
240
226
  this.#args = args;
241
227
 
242
- this.addChild(new Spacer(1));
243
-
244
- // Always create both - contentBox for custom tools/bash/tools with renderers, contentText for other built-ins
245
- this.#contentBox = new Box(1, 1, (text: string) => theme.bg("toolPendingBg", text));
246
- this.#contentText = new Text("", 1, 1, (text: string) => theme.bg("toolPendingBg", text));
228
+ // Always create both - contentBox for custom tools/bash/tools with renderers, contentText for other built-ins.
229
+ // paddingY is 1 so background-tinted blocks (custom/extension tools and the
230
+ // generic fallback) get top/bottom breathing room. TranscriptContainer
231
+ // strips PLAIN-blank edges, so framed/minimal blocks (no bg set) drop these
232
+ // lines and keep their tight spacing only tinted lines survive.
233
+ this.#contentBox = new Box(0, 1);
234
+ this.#contentText = new Text("", 1, 1);
247
235
 
248
236
  // Use Box for custom tools or built-in tools that have renderers
249
237
  const hasRenderer = toolName in toolRenderers;
@@ -433,26 +421,43 @@ export class ToolExecutionComponent extends Container {
433
421
  #updateSpinnerAnimation(): void {
434
422
  // Spinner for: task tool with partial result, or edit/write while args streaming
435
423
  const isStreamingArgs = !this.#argsComplete && (isEditLikeToolName(this.#toolName) || this.#toolName === "write");
436
- const isBackgroundAsyncTask =
437
- this.#toolName === "task" &&
424
+ const isBackgroundAsyncRunning =
438
425
  (this.#result?.details as { async?: { state?: string } } | undefined)?.async?.state === "running";
426
+ const isBackgroundAsyncTask = this.#toolName === "task" && isBackgroundAsyncRunning;
439
427
  const isPartialTask = this.#isPartial && this.#toolName === "task" && !isBackgroundAsyncTask;
440
- // Sweep the border of bash/eval execution blocks while they're pending.
428
+ // Sweep the border of bash/eval execution blocks while they're pending — but
429
+ // not once they've been backgrounded: a backgrounded job's block gets
430
+ // committed to scrollback and finalizes later via the async update path, so a
431
+ // mid-sweep frame would freeze a stray dark "bar" segment into the border.
441
432
  const isPendingExecBlock =
442
- this.#isPartial && shimmerEnabled() && (this.#toolName === "bash" || this.#toolName === "eval");
433
+ this.#isPartial &&
434
+ shimmerEnabled() &&
435
+ (this.#toolName === "bash" || this.#toolName === "eval") &&
436
+ !isBackgroundAsyncRunning;
443
437
  const needsSpinner = isStreamingArgs || isPartialTask || isPendingExecBlock;
444
438
  if (needsSpinner && !this.#spinnerInterval) {
445
- this.#lastSpinnerAdvanceAt = performance.now();
439
+ const now = performance.now();
440
+ const frameCount = theme.spinnerFrames.length;
441
+ this.#lastSpinnerAdvanceAt = now;
442
+ if (frameCount > 0 && this.#spinnerFrame === undefined) {
443
+ this.#spinnerFrame = 0;
444
+ this.#renderState.spinnerFrame = 0;
445
+ }
446
446
  this.#spinnerInterval = setInterval(() => {
447
447
  const now = performance.now();
448
448
  const frameCount = theme.spinnerFrames.length;
449
- // Redraw at ~60fps for a smooth border sweep, but only step the spinner
450
- // glyph at its classic ~12.5fps cadence. The TUI throttles renders at
451
- // 16ms and the differ drops no-op redraws, so the extra ticks are free.
452
- if (frameCount > 0 && now - this.#lastSpinnerAdvanceAt >= SPINNER_GLYPH_ADVANCE_MS) {
453
- this.#spinnerFrame = ((this.#spinnerFrame ?? -1) + 1) % frameCount;
454
- this.#renderState.spinnerFrame = this.#spinnerFrame;
455
- this.#lastSpinnerAdvanceAt = now;
449
+ // Redraw at 30fps for a smooth border sweep, but keep the spinner
450
+ // glyph phase-locked to its classic ~12.5fps cadence. Advancing the
451
+ // anchor by elapsed frames instead of resetting to `now` avoids the
452
+ // 30fps timer quantizing the glyph down to one step every three ticks.
453
+ if (frameCount > 0) {
454
+ const elapsed = now - this.#lastSpinnerAdvanceAt;
455
+ if (elapsed >= SPINNER_GLYPH_ADVANCE_MS) {
456
+ const steps = Math.floor(elapsed / SPINNER_GLYPH_ADVANCE_MS);
457
+ this.#spinnerFrame = ((this.#spinnerFrame ?? 0) + steps) % frameCount;
458
+ this.#renderState.spinnerFrame = this.#spinnerFrame;
459
+ this.#lastSpinnerAdvanceAt += steps * SPINNER_GLYPH_ADVANCE_MS;
460
+ }
456
461
  }
457
462
  this.#ui.requestRender();
458
463
  }, SPINNER_RENDER_INTERVAL_MS);
@@ -524,6 +529,39 @@ export class ToolExecutionComponent extends Container {
524
529
  return (this.#result.details as { async?: { state?: string } } | undefined)?.async?.state === "running";
525
530
  }
526
531
 
532
+ /**
533
+ * While a tool's preview is still streaming, a block whose preview is
534
+ * append-only (rows only grow at the bottom, never re-layout) lets the
535
+ * renderer commit the scrolled-off head of an over-tall preview to native
536
+ * scrollback instead of dropping it — the same anti-yank path a streaming
537
+ * assistant reply uses (see {@link TranscriptContainer} +
538
+ * `NativeScrollbackLiveRegion`). Covers both phases: a pre-result call preview
539
+ * (a `write` whose content streams in) and a partial-result preview that
540
+ * streams output below fixed input (an `eval`/`bash` whose stdout grows under
541
+ * its code cell). Gated on {@link isTranscriptBlockFinalized} so the boundary
542
+ * closes the instant the block reaches a terminal state — a final result that
543
+ * may collapse to a compact view, a backgrounded async tool, or a seal — and
544
+ * the renderer decides whether its current preview shape qualifies via
545
+ * `isStreamingPreviewAppendOnly` (typically: only the expanded full view,
546
+ * which is top-anchored; the collapsed tail window re-layouts but is bounded
547
+ * so it never overflows anyway).
548
+ */
549
+ isTranscriptBlockAppendOnly(): boolean {
550
+ // A finalized block's preview can collapse/re-layout; only a live,
551
+ // still-streaming block is a candidate.
552
+ if (this.isTranscriptBlockFinalized()) return false;
553
+ const predicate =
554
+ (this.#tool as { isStreamingPreviewAppendOnly?: ToolRenderer["isStreamingPreviewAppendOnly"] } | undefined)
555
+ ?.isStreamingPreviewAppendOnly ?? toolRenderers[this.#toolName]?.isStreamingPreviewAppendOnly;
556
+ if (!predicate) return false;
557
+ try {
558
+ return predicate(this.#getCallArgsForRender(), this.#renderState, this.#result);
559
+ } catch (err) {
560
+ logger.warn("Tool append-only predicate failed", { tool: this.#toolName, error: String(err) });
561
+ return false;
562
+ }
563
+ }
564
+
527
565
  /**
528
566
  * Mark the tool terminal even though no result arrived (the turn aborted or
529
567
  * abandoned it) and stop animating, so it can freeze and stops pinning the
@@ -568,49 +606,49 @@ export class ToolExecutionComponent extends Container {
568
606
  }
569
607
 
570
608
  #updateDisplay(): void {
571
- // Set background based on state
572
- const bgFn = this.#isPartial
573
- ? (text: string) => theme.bg("toolPendingBg", text)
574
- : this.#result?.isError
575
- ? (text: string) => theme.bg("toolErrorBg", text)
576
- : (text: string) => theme.bg("toolSuccessBg", text);
577
-
578
609
  // Sync shared mutable render state for component closures
579
610
  this.#renderState.expanded = this.#expanded;
580
611
  this.#renderState.isPartial = this.#isPartial;
581
612
  this.#renderState.spinnerFrame = this.#spinnerFrame;
582
613
 
614
+ // Non-self-framing tools (custom/extension renderers and the generic
615
+ // fallback) get a padded, state-tinted block — built-ins that draw their
616
+ // own frame opt out below via the framed-component mark.
617
+ const stateBgKey = this.#isPartial ? "toolPendingBg" : this.#result?.isError ? "toolErrorBg" : "toolSuccessBg";
618
+ const stateBgFn = (t: string) => theme.bg(stateBgKey, t);
619
+
583
620
  // Check for custom tool rendering
584
621
  if (this.#tool && (this.#tool.renderCall || this.#tool.renderResult)) {
585
622
  const tool = this.#tool;
586
623
  const mergeCallAndResult = Boolean((tool as { mergeCallAndResult?: boolean }).mergeCallAndResult);
587
624
  // Custom tools use Box for flexible component rendering
588
- const inline = Boolean((tool as { inline?: boolean }).inline);
589
- this.#contentBox.setBgFn(inline ? undefined : bgFn);
625
+ this.#contentBox.setBgFn(undefined);
590
626
  this.#contentBox.clear();
591
- let contentBoxHasFramedBlock = false;
592
627
  // Mirror the built-in renderer branch so custom renderers (notably the
593
628
  // task tool, whose live instance routes through here) receive the same
594
629
  // render context — e.g. the `hasResult` flag that suppresses the task
595
630
  // call preview once result lines exist.
596
631
  this.#renderState.renderContext = this.#buildRenderContext();
597
632
 
598
- // Render call component
633
+ // Render call component. The fallback label only stands in for a
634
+ // missing `renderCall`; when the call is intentionally suppressed
635
+ // (mergeCallAndResult once a result exists) we render nothing here so
636
+ // the result component isn't preceded by a redundant tool-name line.
599
637
  const shouldRenderCall = !this.#result || !mergeCallAndResult;
600
- if (shouldRenderCall && tool.renderCall) {
601
- try {
602
- const callComponent = tool.renderCall(this.#getCallArgsForRender(), this.#renderState, theme);
603
- if (callComponent) {
604
- contentBoxHasFramedBlock = addBoxChild(this.#contentBox, callComponent) || contentBoxHasFramedBlock;
638
+ if (shouldRenderCall) {
639
+ if (tool.renderCall) {
640
+ try {
641
+ const callComponent = tool.renderCall(this.#getCallArgsForRender(), this.#renderState, theme);
642
+ if (callComponent) this.#contentBox.addChild(callComponent as Component);
643
+ } catch (err) {
644
+ logger.warn("Tool renderer failed", { tool: this.#toolName, error: String(err) });
645
+ // Fall back to default on error
646
+ this.#contentBox.addChild(new Text(theme.fg("toolTitle", theme.bold(this.#toolLabel)), 0, 0));
605
647
  }
606
- } catch (err) {
607
- logger.warn("Tool renderer failed", { tool: this.#toolName, error: String(err) });
608
- // Fall back to default on error
609
- addBoxChild(this.#contentBox, new Text(theme.fg("toolTitle", theme.bold(this.#toolLabel)), 0, 0));
648
+ } else {
649
+ // No custom renderCall, show tool name
650
+ this.#contentBox.addChild(new Text(theme.fg("toolTitle", theme.bold(this.#toolLabel)), 0, 0));
610
651
  }
611
- } else {
612
- // No custom renderCall, show tool name
613
- addBoxChild(this.#contentBox, new Text(theme.fg("toolTitle", theme.bold(this.#toolLabel)), 0, 0));
614
652
  }
615
653
 
616
654
  // Render result component if we have a result
@@ -632,25 +670,27 @@ export class ToolExecutionComponent extends Container {
632
670
  theme,
633
671
  this.#args,
634
672
  );
635
- if (resultComponent) {
636
- contentBoxHasFramedBlock = addBoxChild(this.#contentBox, resultComponent) || contentBoxHasFramedBlock;
637
- }
673
+ if (resultComponent) this.#contentBox.addChild(resultComponent);
638
674
  } catch (err) {
639
675
  logger.warn("Tool renderer failed", { tool: this.#toolName, error: String(err) });
640
676
  // Fall back to showing raw output on error
641
677
  const output = this.#getTextOutput();
642
678
  if (output) {
643
- addBoxChild(this.#contentBox, new Text(theme.fg("toolOutput", replaceTabs(output)), 0, 0));
679
+ this.#contentBox.addChild(new Text(theme.fg("toolOutput", replaceTabs(output)), 0, 0));
644
680
  }
645
681
  }
646
682
  } else if (this.#result) {
647
683
  // Has result but no custom renderResult
648
684
  const output = this.#getTextOutput();
649
685
  if (output) {
650
- addBoxChild(this.#contentBox, new Text(theme.fg("toolOutput", replaceTabs(output)), 0, 0));
686
+ this.#contentBox.addChild(new Text(theme.fg("toolOutput", replaceTabs(output)), 0, 0));
651
687
  }
652
688
  }
653
- this.#contentBox.setPaddingX(contentBoxHasFramedBlock ? 0 : 1);
689
+ // Custom tools that draw their own frame (task) render flush; plain
690
+ // extension renderers get the padded, state-tinted block back.
691
+ const customFramed = this.#contentBox.children.some(isFramedBlockComponent);
692
+ this.#contentBox.setPaddingX(customFramed ? 0 : 1);
693
+ this.#contentBox.setBgFn(customFramed ? undefined : stateBgFn);
654
694
  } else if (this.#toolName in toolRenderers) {
655
695
  // Built-in tools with renderers
656
696
  const renderer = toolRenderers[this.#toolName];
@@ -669,7 +709,6 @@ export class ToolExecutionComponent extends Container {
669
709
  // Multi-file: render each file as its own Box (identical to separate tool calls)
670
710
  this.#contentBox.setBgFn(undefined);
671
711
  this.#contentBox.clear();
672
- this.#contentBox.setPaddingX(1);
673
712
 
674
713
  const renderContext = this.#buildRenderContext();
675
714
  this.#renderState.renderContext = renderContext;
@@ -681,20 +720,14 @@ export class ToolExecutionComponent extends Container {
681
720
  this.#multiFileBoxes.push(spacer);
682
721
  this.addChild(spacer);
683
722
  }
684
- const fileBgFn = fileResult.isError
685
- ? (text: string) => theme.bg("toolErrorBg", text)
686
- : (text: string) => theme.bg("toolSuccessBg", text);
687
- const fileBox = new Box(1, 1, fileBgFn);
723
+ const fileBox = new Box(0, 0);
688
724
  try {
689
725
  const resultComponent = renderer.renderResult(
690
726
  { content: [], details: fileResult, isError: fileResult.isError },
691
727
  this.#renderState,
692
728
  theme,
693
729
  );
694
- if (resultComponent) {
695
- const fileBoxHasFramedBlock = addBoxChild(fileBox, resultComponent);
696
- fileBox.setPaddingX(fileBoxHasFramedBlock ? 0 : 1);
697
- }
730
+ if (resultComponent) fileBox.addChild(resultComponent);
698
731
  } catch (err) {
699
732
  logger.warn("Tool renderer failed", { tool: this.#toolName, error: String(err) });
700
733
  }
@@ -711,7 +744,7 @@ export class ToolExecutionComponent extends Container {
711
744
  const pendingSpacer = new Spacer(1);
712
745
  this.#multiFileBoxes.push(pendingSpacer);
713
746
  this.addChild(pendingSpacer);
714
- const pendingBox = new Box(1, 1, (text: string) => theme.bg("toolPendingBg", text));
747
+ const pendingBox = new Box(0, 0);
715
748
  const pendingText = renderStatusLine(
716
749
  {
717
750
  icon: "pending",
@@ -727,9 +760,8 @@ export class ToolExecutionComponent extends Container {
727
760
  } else {
728
761
  // Single-file or no result: standard rendering
729
762
  // Inline renderers skip background styling
730
- this.#contentBox.setBgFn(renderer.inline ? undefined : bgFn);
763
+ this.#contentBox.setBgFn(undefined);
731
764
  this.#contentBox.clear();
732
- let contentBoxHasFramedBlock = false;
733
765
 
734
766
  const renderContext = this.#buildRenderContext();
735
767
  this.#renderState.renderContext = renderContext;
@@ -739,14 +771,11 @@ export class ToolExecutionComponent extends Container {
739
771
  // Render call component
740
772
  try {
741
773
  const callComponent = renderer.renderCall(this.#getCallArgsForRender(), this.#renderState, theme);
742
- if (callComponent) {
743
- contentBoxHasFramedBlock =
744
- addBoxChild(this.#contentBox, callComponent) || contentBoxHasFramedBlock;
745
- }
774
+ if (callComponent) this.#contentBox.addChild(callComponent);
746
775
  } catch (err) {
747
776
  logger.warn("Tool renderer failed", { tool: this.#toolName, error: String(err) });
748
777
  // Fall back to default on error
749
- addBoxChild(this.#contentBox, new Text(theme.fg("toolTitle", theme.bold(this.#toolLabel)), 0, 0));
778
+ this.#contentBox.addChild(new Text(theme.fg("toolTitle", theme.bold(this.#toolLabel)), 0, 0));
750
779
  }
751
780
  }
752
781
 
@@ -763,24 +792,20 @@ export class ToolExecutionComponent extends Container {
763
792
  theme,
764
793
  this.#getCallArgsForRender(),
765
794
  );
766
- if (resultComponent) {
767
- contentBoxHasFramedBlock =
768
- addBoxChild(this.#contentBox, resultComponent) || contentBoxHasFramedBlock;
769
- }
795
+ if (resultComponent) this.#contentBox.addChild(resultComponent);
770
796
  } catch (err) {
771
797
  logger.warn("Tool renderer failed", { tool: this.#toolName, error: String(err) });
772
798
  // Fall back to showing raw output on error
773
799
  const output = this.#getTextOutput();
774
800
  if (output) {
775
- addBoxChild(this.#contentBox, new Text(theme.fg("toolOutput", replaceTabs(output)), 0, 0));
801
+ this.#contentBox.addChild(new Text(theme.fg("toolOutput", replaceTabs(output)), 0, 0));
776
802
  }
777
803
  }
778
804
  }
779
- this.#contentBox.setPaddingX(contentBoxHasFramedBlock ? 0 : 1);
780
805
  }
781
806
  } else {
782
807
  // Other built-in tools: use Text directly with caching
783
- this.#contentText.setCustomBgFn(bgFn);
808
+ this.#contentText.setCustomBgFn(stateBgFn);
784
809
  this.#contentText.setText(this.#formatToolExecution());
785
810
  }
786
811