@oh-my-pi/pi-coding-agent 17.0.0 → 17.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (202) hide show
  1. package/CHANGELOG.md +128 -0
  2. package/dist/cli.js +4006 -3966
  3. package/dist/types/advisor/advise-tool.d.ts +5 -2
  4. package/dist/types/advisor/config.d.ts +14 -6
  5. package/dist/types/advisor/runtime.d.ts +20 -11
  6. package/dist/types/autolearn/controller.d.ts +1 -0
  7. package/dist/types/cli-commands.d.ts +14 -1
  8. package/dist/types/config/model-discovery.d.ts +17 -0
  9. package/dist/types/config/model-registry.d.ts +4 -0
  10. package/dist/types/config/model-resolver.d.ts +6 -2
  11. package/dist/types/config/models-config-schema.d.ts +10 -0
  12. package/dist/types/config/models-config.d.ts +6 -0
  13. package/dist/types/config/settings-schema.d.ts +32 -7
  14. package/dist/types/config/settings.d.ts +50 -0
  15. package/dist/types/cursor.d.ts +11 -0
  16. package/dist/types/discovery/helpers.d.ts +5 -2
  17. package/dist/types/discovery/substitute-plugin-root.d.ts +20 -7
  18. package/dist/types/exec/bash-executor.d.ts +2 -0
  19. package/dist/types/extensibility/extensions/managed-timers.d.ts +15 -0
  20. package/dist/types/extensibility/extensions/runner.d.ts +7 -0
  21. package/dist/types/extensibility/extensions/types.d.ts +18 -0
  22. package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +40 -0
  23. package/dist/types/extensibility/shared-events.d.ts +6 -0
  24. package/dist/types/extensibility/utils.d.ts +2 -2
  25. package/dist/types/internal-urls/agent-protocol.d.ts +1 -0
  26. package/dist/types/lib/xai-http.d.ts +11 -0
  27. package/dist/types/mcp/transports/stdio.d.ts +13 -1
  28. package/dist/types/modes/acp/acp-agent.d.ts +3 -0
  29. package/dist/types/modes/acp/acp-mode.d.ts +6 -2
  30. package/dist/types/modes/components/advisor-config.d.ts +8 -1
  31. package/dist/types/modes/components/assistant-message.d.ts +2 -0
  32. package/dist/types/modes/components/custom-editor-plugin-ctor.test.d.ts +1 -0
  33. package/dist/types/modes/components/custom-editor.d.ts +23 -1
  34. package/dist/types/modes/components/hook-editor.d.ts +16 -2
  35. package/dist/types/modes/components/login-dialog.test.d.ts +1 -0
  36. package/dist/types/modes/components/model-hub.d.ts +5 -2
  37. package/dist/types/modes/components/session-selector.d.ts +4 -0
  38. package/dist/types/modes/components/tool-execution.d.ts +8 -1
  39. package/dist/types/modes/components/tool-execution.test.d.ts +1 -0
  40. package/dist/types/modes/controllers/command-controller.d.ts +8 -0
  41. package/dist/types/modes/controllers/selector-controller.d.ts +3 -1
  42. package/dist/types/modes/interactive-mode.d.ts +6 -0
  43. package/dist/types/modes/noninteractive-dispose.test.d.ts +1 -0
  44. package/dist/types/modes/print-mode.d.ts +1 -1
  45. package/dist/types/modes/types.d.ts +10 -0
  46. package/dist/types/modes/utils/transcript-render-helpers.d.ts +2 -2
  47. package/dist/types/modes/warp-events.d.ts +24 -0
  48. package/dist/types/modes/warp-events.test.d.ts +1 -0
  49. package/dist/types/plan-mode/model-transition.d.ts +47 -0
  50. package/dist/types/plan-mode/model-transition.test.d.ts +1 -0
  51. package/dist/types/registry/agent-lifecycle.d.ts +26 -1
  52. package/dist/types/sdk.d.ts +13 -2
  53. package/dist/types/session/agent-session.d.ts +43 -11
  54. package/dist/types/session/session-history-format.d.ts +10 -0
  55. package/dist/types/session/session-manager.d.ts +14 -0
  56. package/dist/types/session/streaming-output.d.ts +2 -0
  57. package/dist/types/slash-commands/helpers/active-oauth-account.d.ts +9 -0
  58. package/dist/types/slash-commands/types.d.ts +4 -4
  59. package/dist/types/system-prompt.d.ts +1 -1
  60. package/dist/types/task/label.d.ts +1 -1
  61. package/dist/types/telemetry-export.d.ts +34 -9
  62. package/dist/types/tools/approval.d.ts +8 -0
  63. package/dist/types/tools/bash.d.ts +2 -0
  64. package/dist/types/tools/browser/registry.d.ts +7 -3
  65. package/dist/types/tools/browser/tab-supervisor.d.ts +2 -0
  66. package/dist/types/tools/essential-tools.d.ts +29 -0
  67. package/dist/types/tools/image-gen.d.ts +2 -1
  68. package/dist/types/tools/index.d.ts +4 -1
  69. package/dist/types/tools/manage-skill.d.ts +2 -0
  70. package/dist/types/tools/xdev.d.ts +11 -2
  71. package/dist/types/utils/title-generator.d.ts +2 -1
  72. package/dist/types/web/search/index.d.ts +2 -0
  73. package/dist/types/web/search/providers/base.d.ts +3 -0
  74. package/dist/types/web/search/providers/kimi.d.ts +4 -1
  75. package/dist/types/web/search/types.d.ts +1 -1
  76. package/package.json +21 -16
  77. package/src/advisor/__tests__/advisor.test.ts +1358 -86
  78. package/src/advisor/__tests__/config.test.ts +58 -2
  79. package/src/advisor/advise-tool.ts +7 -3
  80. package/src/advisor/config.ts +76 -24
  81. package/src/advisor/runtime.ts +445 -92
  82. package/src/autolearn/controller.ts +23 -28
  83. package/src/cli/bench-cli.ts +4 -1
  84. package/src/cli/grep-cli.ts +2 -1
  85. package/src/cli-commands.ts +57 -23
  86. package/src/cli.ts +5 -1
  87. package/src/config/model-discovery.ts +81 -21
  88. package/src/config/model-registry.ts +34 -6
  89. package/src/config/model-resolver.ts +57 -12
  90. package/src/config/models-config-schema.ts +1 -0
  91. package/src/config/settings-schema.ts +43 -6
  92. package/src/config/settings.ts +405 -25
  93. package/src/cursor.ts +20 -3
  94. package/src/debug/report-bundle.ts +40 -4
  95. package/src/discovery/codex.ts +14 -4
  96. package/src/discovery/helpers.ts +28 -5
  97. package/src/discovery/substitute-plugin-root.ts +23 -7
  98. package/src/exec/bash-executor.ts +14 -5
  99. package/src/export/html/template.js +2 -0
  100. package/src/extensibility/custom-tools/loader.ts +3 -3
  101. package/src/extensibility/custom-tools/wrapper.ts +2 -1
  102. package/src/extensibility/extensions/loader.ts +3 -3
  103. package/src/extensibility/extensions/managed-timers.ts +83 -0
  104. package/src/extensibility/extensions/runner.ts +26 -0
  105. package/src/extensibility/extensions/types.ts +18 -0
  106. package/src/extensibility/extensions/wrapper.ts +2 -1
  107. package/src/extensibility/hooks/loader.ts +3 -3
  108. package/src/extensibility/legacy-pi-coding-agent-shim.ts +96 -1
  109. package/src/extensibility/plugins/legacy-pi-compat.ts +225 -22
  110. package/src/extensibility/plugins/manager.ts +2 -2
  111. package/src/extensibility/shared-events.ts +6 -0
  112. package/src/extensibility/utils.ts +91 -25
  113. package/src/internal-urls/__tests__/agent-protocol-nested.test.ts +73 -0
  114. package/src/internal-urls/agent-protocol.ts +73 -39
  115. package/src/irc/bus.ts +22 -3
  116. package/src/launch/broker.ts +3 -2
  117. package/src/launch/client.ts +2 -2
  118. package/src/launch/presence.ts +2 -2
  119. package/src/lib/xai-http.ts +28 -2
  120. package/src/lsp/client.ts +1 -1
  121. package/src/main.ts +11 -8
  122. package/src/mcp/manager.ts +9 -3
  123. package/src/mcp/transports/stdio.ts +103 -23
  124. package/src/modes/acp/acp-agent.ts +28 -9
  125. package/src/modes/acp/acp-event-mapper.ts +8 -0
  126. package/src/modes/acp/acp-mode.ts +18 -4
  127. package/src/modes/components/advisor-config.ts +65 -3
  128. package/src/modes/components/ask-dialog.ts +1 -1
  129. package/src/modes/components/assistant-message.ts +35 -14
  130. package/src/modes/components/chat-transcript-builder.ts +2 -0
  131. package/src/modes/components/custom-editor-plugin-ctor.test.ts +36 -0
  132. package/src/modes/components/custom-editor.ts +62 -1
  133. package/src/modes/components/hook-editor.ts +35 -5
  134. package/src/modes/components/login-dialog.test.ts +56 -0
  135. package/src/modes/components/login-dialog.ts +7 -3
  136. package/src/modes/components/model-hub.ts +138 -42
  137. package/src/modes/components/plan-review-overlay.ts +51 -10
  138. package/src/modes/components/session-selector.ts +10 -0
  139. package/src/modes/components/status-line/component.test.ts +1 -0
  140. package/src/modes/components/status-line/component.ts +2 -4
  141. package/src/modes/components/status-line/segments.ts +21 -6
  142. package/src/modes/components/tool-execution.test.ts +101 -0
  143. package/src/modes/components/tool-execution.ts +117 -9
  144. package/src/modes/components/transcript-container.ts +30 -2
  145. package/src/modes/controllers/command-controller-shared.ts +1 -1
  146. package/src/modes/controllers/command-controller.ts +168 -48
  147. package/src/modes/controllers/event-controller.ts +6 -0
  148. package/src/modes/controllers/extension-ui-controller.ts +4 -22
  149. package/src/modes/controllers/input-controller.ts +12 -12
  150. package/src/modes/controllers/selector-controller.ts +208 -46
  151. package/src/modes/interactive-mode.ts +205 -65
  152. package/src/modes/noninteractive-dispose.test.ts +60 -0
  153. package/src/modes/print-mode.ts +10 -5
  154. package/src/modes/rpc/host-tools.ts +2 -1
  155. package/src/modes/rpc/rpc-mode.ts +31 -7
  156. package/src/modes/theme/theme.ts +2 -2
  157. package/src/modes/types.ts +10 -0
  158. package/src/modes/utils/interactive-context-helpers.ts +3 -1
  159. package/src/modes/utils/transcript-render-helpers.ts +3 -2
  160. package/src/modes/warp-events.test.ts +794 -0
  161. package/src/modes/warp-events.ts +232 -0
  162. package/src/plan-mode/model-transition.test.ts +60 -0
  163. package/src/plan-mode/model-transition.ts +51 -0
  164. package/src/prompts/system/system-prompt.md +1 -1
  165. package/src/registry/agent-lifecycle.ts +133 -18
  166. package/src/sdk.ts +227 -44
  167. package/src/session/agent-session.ts +1364 -382
  168. package/src/session/session-history-format.ts +20 -5
  169. package/src/session/session-listing.ts +6 -5
  170. package/src/session/session-loader.ts +9 -0
  171. package/src/session/session-manager.ts +48 -0
  172. package/src/session/session-persistence.ts +11 -0
  173. package/src/session/streaming-output.ts +3 -1
  174. package/src/slash-commands/builtin-registry.ts +9 -0
  175. package/src/slash-commands/helpers/active-oauth-account.ts +16 -0
  176. package/src/slash-commands/types.ts +4 -4
  177. package/src/system-prompt.ts +2 -2
  178. package/src/task/executor.ts +1 -1
  179. package/src/task/label.ts +2 -0
  180. package/src/telemetry-export.ts +453 -97
  181. package/src/tools/approval.ts +11 -0
  182. package/src/tools/bash.ts +71 -38
  183. package/src/tools/browser/registry.ts +9 -2
  184. package/src/tools/browser/tab-supervisor.ts +65 -10
  185. package/src/tools/browser.ts +4 -3
  186. package/src/tools/essential-tools.ts +45 -0
  187. package/src/tools/eval.ts +3 -0
  188. package/src/tools/gh.ts +169 -2
  189. package/src/tools/image-gen.ts +624 -513
  190. package/src/tools/index.ts +10 -6
  191. package/src/tools/manage-skill.ts +5 -3
  192. package/src/tools/output-meta.ts +7 -0
  193. package/src/tools/path-utils.ts +8 -5
  194. package/src/tools/read.ts +48 -3
  195. package/src/tools/write.ts +22 -4
  196. package/src/tools/xdev.ts +14 -3
  197. package/src/utils/title-generator.ts +15 -4
  198. package/src/web/search/index.ts +21 -6
  199. package/src/web/search/providers/base.ts +3 -0
  200. package/src/web/search/providers/kimi.ts +18 -12
  201. package/src/web/search/providers/xai.ts +40 -9
  202. package/src/web/search/types.ts +6 -1
@@ -129,9 +129,9 @@ const modelSegment: StatusLineSegment = {
129
129
 
130
130
  // Fast-mode icon and thinking-level suffix trail the model name and are
131
131
  // colored together with it as `statusLineModel`. The advisor "++" badge
132
- // sits between the name and that tail in `accent`, so it reads as a
133
- // distinct marker. theme.fg resets only the fg, so the spans are
134
- // concatenated (not nested) to keep each color intact.
132
+ // sits between the name and that tail, so it reads as a distinct marker.
133
+ // theme.fg resets only the fg, so the spans are concatenated (not
134
+ // nested) to keep each color intact.
135
135
  let tail = "";
136
136
  if (ctx.session.isFastModeActive() && theme.icon.fast) {
137
137
  tail += ` ${theme.icon.fast}`;
@@ -141,10 +141,25 @@ const modelSegment: StatusLineSegment = {
141
141
  }
142
142
 
143
143
  // `statusLineModel` is aliased to `accent` in many themes, so the badge
144
- // uses `success` to stay visibly distinct from the model name color.
144
+ // uses status colors to stay visibly distinct from the model name color.
145
145
  let content = theme.fg("statusLineModel", withIcon(modelIcon, modelName));
146
- if (ctx.session.isAdvisorActive()) {
147
- content += theme.fg("success", "++");
146
+ // Advisor "++" badge, colored by the worst status in the roster:
147
+ // success = all running, warning = quota-exhausted, error = failed,
148
+ // dim = everything paused/no-model. Per-advisor detail lives in
149
+ // `/advisor status`.
150
+ // Optional chaining: lightweight session doubles (test mocks) that don't
151
+ // implement getAdvisorStatusOverview skip the badge instead of crashing.
152
+ const advisorStats = ctx.session.getAdvisorStatusOverview?.();
153
+ if (advisorStats?.configured && advisorStats.advisors.length > 0) {
154
+ const statuses = advisorStats.advisors.map(a => a.status);
155
+ const badgeColor = statuses.includes("error")
156
+ ? "error"
157
+ : statuses.includes("quota_exhausted")
158
+ ? "warning"
159
+ : statuses.includes("running")
160
+ ? "success"
161
+ : "dim";
162
+ content += theme.fg(badgeColor, "++");
148
163
  }
149
164
  if (tail) {
150
165
  content += theme.fg("statusLineModel", tail);
@@ -0,0 +1,101 @@
1
+ import { beforeAll, describe, expect, it } from "bun:test";
2
+ import type { AgentTool } from "@oh-my-pi/pi-agent-core";
3
+ import { type Component, Text } from "@oh-my-pi/pi-tui";
4
+ import { Settings } from "../../config/settings";
5
+ import { getThemeByName, setThemeInstance, theme } from "../theme/theme";
6
+ import { ToolExecutionComponent, type ToolExecutionUi } from "./tool-execution";
7
+
8
+ class BoldTypeErrorComponent implements Component {
9
+ render(_width: number): readonly string[] {
10
+ throw new TypeError("th.bold is not a function");
11
+ }
12
+ }
13
+
14
+ function visibleText(lines: readonly string[]): string {
15
+ let text = lines.join("\n");
16
+ text = text.replace(/\x1b\]8;[^\x1b\x07]*(?:\x07|\x1b\\)/g, "");
17
+ text = text.replace(/\x1b\[[0-9;]*m/g, "");
18
+ return text;
19
+ }
20
+
21
+ describe("ToolExecutionComponent custom renderer failures", () => {
22
+ beforeAll(async () => {
23
+ await Settings.init({ inMemory: true });
24
+ const loaded = await getThemeByName("dark");
25
+ if (!loaded) throw new Error("theme unavailable");
26
+ setThemeInstance(loaded);
27
+ });
28
+
29
+ it("falls back to the custom tool label when a renderCall child component throws during render", () => {
30
+ const tool: AgentTool = {
31
+ name: "graphify_graph",
32
+ label: "Graphify Graph",
33
+ description: "renders a graph",
34
+ parameters: { type: "object", additionalProperties: true },
35
+ renderCall() {
36
+ return new BoldTypeErrorComponent();
37
+ },
38
+ async execute() {
39
+ return { content: [{ type: "text", text: "ok" }] };
40
+ },
41
+ };
42
+ const ui: ToolExecutionUi = {
43
+ requestRender() {},
44
+ requestComponentRender(_component: Component) {},
45
+ resetDisplay() {},
46
+ };
47
+ const component = new ToolExecutionComponent(
48
+ "graphify_graph",
49
+ {},
50
+ { showImages: false },
51
+ tool,
52
+ ui,
53
+ process.cwd(),
54
+ );
55
+ let text = "";
56
+
57
+ expect(() => {
58
+ text = visibleText(component.render(80));
59
+ }).not.toThrow();
60
+ expect(text).toContain("Graphify Graph");
61
+ });
62
+
63
+ it("preserves raw result text when a renderResult child component throws during render", () => {
64
+ const rawResultText = "raw result survives child renderer failure";
65
+ const tool: AgentTool = {
66
+ name: "crashy_result_renderer",
67
+ label: "Crashy Result Renderer",
68
+ description: "renders result output",
69
+ parameters: { type: "object", additionalProperties: true },
70
+ renderCall() {
71
+ return new Text(theme.fg("toolTitle", theme.bold("Crashy Result Renderer")), 0, 0);
72
+ },
73
+ renderResult() {
74
+ return new BoldTypeErrorComponent();
75
+ },
76
+ async execute() {
77
+ return { content: [{ type: "text", text: rawResultText }] };
78
+ },
79
+ };
80
+ const ui: ToolExecutionUi = {
81
+ requestRender() {},
82
+ requestComponentRender(_component: Component) {},
83
+ resetDisplay() {},
84
+ };
85
+ const component = new ToolExecutionComponent(
86
+ "crashy_result_renderer",
87
+ {},
88
+ { showImages: false },
89
+ tool,
90
+ ui,
91
+ process.cwd(),
92
+ );
93
+ component.updateResult({ content: [{ type: "text", text: rawResultText }] }, false);
94
+ let text = "";
95
+
96
+ expect(() => {
97
+ text = visibleText(component.render(80));
98
+ }).not.toThrow();
99
+ expect(text).toContain(rawResultText);
100
+ });
101
+ });
@@ -40,7 +40,7 @@ import {
40
40
  } from "../../tools/render-utils";
41
41
  import { type FirstResultViewportRepaint, toolRenderers } from "../../tools/renderers";
42
42
  import { TODO_STRIKE_TOTAL_FRAMES, type TodoToolDetails } from "../../tools/todo";
43
- import { isFramedBlockComponent, renderStatusLine, WidthAwareText } from "../../tui";
43
+ import { isFramedBlockComponent, markFramedBlockComponent, renderStatusLine, WidthAwareText } from "../../tui";
44
44
  import { sanitizeWithOptionalSixelPassthrough } from "../../utils/sixel";
45
45
  import { renderDiff } from "./diff";
46
46
 
@@ -148,6 +148,68 @@ function getArgsWithStreamedTextInput(args: unknown): unknown {
148
148
  return input === undefined ? args : { ...record, input };
149
149
  }
150
150
 
151
+ type ToolRendererStage = "call" | "result";
152
+
153
+ class SafeToolRendererComponent implements Component {
154
+ #toolName: string;
155
+ #stage: ToolRendererStage;
156
+ #component: Component;
157
+ #fallback: () => Component | undefined;
158
+ #warned = false;
159
+ readonly wantsKeyRelease: boolean | undefined;
160
+
161
+ constructor(
162
+ toolName: string,
163
+ stage: ToolRendererStage,
164
+ component: Component,
165
+ fallback: () => Component | undefined,
166
+ ) {
167
+ this.#toolName = toolName;
168
+ this.#stage = stage;
169
+ this.#component = component;
170
+ this.#fallback = fallback;
171
+ this.wantsKeyRelease = component.wantsKeyRelease;
172
+ if (isFramedBlockComponent(component)) {
173
+ markFramedBlockComponent(this);
174
+ }
175
+ }
176
+
177
+ render(width: number): readonly string[] {
178
+ try {
179
+ return this.#component.render(width);
180
+ } catch (err) {
181
+ if (!this.#warned) {
182
+ this.#warned = true;
183
+ logger.warn("Tool renderer failed", { tool: this.#toolName, stage: this.#stage, error: String(err) });
184
+ }
185
+ return this.#fallback()?.render(width) ?? [];
186
+ }
187
+ }
188
+
189
+ handleInput(data: string): void {
190
+ const handleInput = this.#component.handleInput;
191
+ if (handleInput === undefined) return;
192
+ handleInput.call(this.#component, data);
193
+ }
194
+
195
+ invalidate(): void {
196
+ const invalidate = this.#component.invalidate;
197
+ if (invalidate === undefined) return;
198
+ invalidate.call(this.#component);
199
+ }
200
+
201
+ setIgnoreTight(ignore: boolean): void {
202
+ const setIgnoreTight = this.#component.setIgnoreTight;
203
+ if (setIgnoreTight === undefined) return;
204
+ setIgnoreTight.call(this.#component, ignore);
205
+ }
206
+
207
+ dispose(): void {
208
+ const dispose = this.#component.dispose;
209
+ if (dispose === undefined) return;
210
+ dispose.call(this.#component);
211
+ }
212
+ }
151
213
  /**
152
214
  * Transcript-side probe telling a block whether it is still inside the live
153
215
  * (repaintable) region. Implemented by `TranscriptContainer`; injected rather
@@ -157,6 +219,14 @@ export interface TranscriptLiveRegionProbe {
157
219
  isBlockInLiveRegion(component: Component): boolean;
158
220
  }
159
221
 
222
+ /** Minimal TUI surface ToolExecutionComponent uses to schedule repaints and share image budget. */
223
+ export interface ToolExecutionUi {
224
+ requestRender(): void;
225
+ requestComponentRender(component: Component): void;
226
+ resetDisplay(): void;
227
+ imageBudget?: TUI["imageBudget"];
228
+ }
229
+
160
230
  export interface ToolExecutionOptions {
161
231
  snapshots?: SnapshotStore;
162
232
  showImages?: boolean; // default: true (only used if terminal supports images)
@@ -240,7 +310,7 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
240
310
  // forcing the common image-free result to re-shape on every resize tick.
241
311
  #renderedImageCount = 0;
242
312
  #tool?: AgentTool;
243
- #ui: TUI;
313
+ #ui: ToolExecutionUi;
244
314
  #cwd: string;
245
315
  #result?: {
246
316
  content: Array<{ type: string; text?: string; data?: string; mimeType?: string }>;
@@ -308,7 +378,7 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
308
378
  args: any,
309
379
  options: ToolExecutionOptions = {},
310
380
  tool: AgentTool | undefined,
311
- ui: TUI,
381
+ ui: ToolExecutionUi,
312
382
  cwd: string = getProjectDir(),
313
383
  _toolCallId?: string,
314
384
  ) {
@@ -902,8 +972,17 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
902
972
  if (tool.renderCall) {
903
973
  try {
904
974
  const callArgs = this.#getCallArgsForRender();
905
- const callComponent = tool.renderCall(callArgs, this.#renderState, theme);
906
- if (callComponent) this.#contentBox.addChild(callComponent as Component);
975
+ const callComponent = tool.renderCall(callArgs, this.#renderState, theme) as Component | undefined;
976
+ if (callComponent) {
977
+ this.#contentBox.addChild(
978
+ new SafeToolRendererComponent(
979
+ this.#toolName,
980
+ "call",
981
+ callComponent,
982
+ () => new Text(theme.fg("toolTitle", theme.bold(this.#toolLabel)), 0, 0),
983
+ ),
984
+ );
985
+ }
907
986
  } catch (err) {
908
987
  logger.warn("Tool renderer failed", { tool: this.#toolName, error: String(err) });
909
988
  // Fall back to default on error
@@ -934,7 +1013,15 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
934
1013
  theme,
935
1014
  this.#args,
936
1015
  );
937
- if (resultComponent) this.#contentBox.addChild(resultComponent);
1016
+ if (resultComponent) {
1017
+ this.#contentBox.addChild(
1018
+ new SafeToolRendererComponent(this.#toolName, "result", resultComponent, () => {
1019
+ const output = this.#getTextOutput();
1020
+ if (!output) return undefined;
1021
+ return new Text(theme.fg("toolOutput", replaceTabs(output)), 0, 0);
1022
+ }),
1023
+ );
1024
+ }
938
1025
  } catch (err) {
939
1026
  logger.warn("Tool renderer failed", { tool: this.#toolName, error: String(err) });
940
1027
  // Fall back to showing raw output on error
@@ -991,7 +1078,11 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
991
1078
  this.#renderState,
992
1079
  theme,
993
1080
  );
994
- if (resultComponent) fileBox.addChild(resultComponent);
1081
+ if (resultComponent) {
1082
+ fileBox.addChild(
1083
+ new SafeToolRendererComponent(this.#toolName, "result", resultComponent, () => undefined),
1084
+ );
1085
+ }
995
1086
  } catch (err) {
996
1087
  logger.warn("Tool renderer failed", { tool: this.#toolName, error: String(err) });
997
1088
  }
@@ -1038,7 +1129,16 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
1038
1129
  try {
1039
1130
  const callArgs = this.#getCallArgsForRender();
1040
1131
  const callComponent = renderer.renderCall(callArgs, this.#renderState, theme);
1041
- if (callComponent) this.#contentBox.addChild(callComponent);
1132
+ if (callComponent) {
1133
+ this.#contentBox.addChild(
1134
+ new SafeToolRendererComponent(
1135
+ this.#toolName,
1136
+ "call",
1137
+ callComponent,
1138
+ () => new Text(theme.fg("toolTitle", theme.bold(this.#toolLabel)), 0, 0),
1139
+ ),
1140
+ );
1141
+ }
1042
1142
  } catch (err) {
1043
1143
  logger.warn("Tool renderer failed", { tool: this.#toolName, error: String(err) });
1044
1144
  // Fall back to default on error
@@ -1059,7 +1159,15 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac
1059
1159
  theme,
1060
1160
  this.#getCallArgsForRender(),
1061
1161
  );
1062
- if (resultComponent) this.#contentBox.addChild(resultComponent);
1162
+ if (resultComponent) {
1163
+ this.#contentBox.addChild(
1164
+ new SafeToolRendererComponent(this.#toolName, "result", resultComponent, () => {
1165
+ const output = this.#getTextOutput();
1166
+ if (!output) return undefined;
1167
+ return new Text(theme.fg("toolOutput", replaceTabs(output)), 0, 0);
1168
+ }),
1169
+ );
1170
+ }
1063
1171
  } catch (err) {
1064
1172
  logger.warn("Tool renderer failed", { tool: this.#toolName, error: String(err) });
1065
1173
  // Fall back to showing raw output on error
@@ -79,6 +79,10 @@ function sealCommittedSnapshot(child: Component): void {
79
79
  if (block.isDisplaceableBlock?.()) block.seal?.();
80
80
  }
81
81
 
82
+ function setBlockCommittedRows(child: Component, rows: number): void {
83
+ (child as Component & Partial<NativeScrollbackCommittedRows>).setNativeScrollbackCommittedRows?.(rows);
84
+ }
85
+
82
86
  // A "plain blank" row is empty or whitespace-only with no ANSI bytes. It marks
83
87
  // separation padding (a `Spacer`, or a no-background `paddingY` row) as opposed
84
88
  // to a background-colored padding row, whose escape sequences contain `\S` and
@@ -208,11 +212,35 @@ export class TranscriptContainer
208
212
  this.#replayPending = false;
209
213
  }
210
214
 
211
- setNativeScrollbackCommittedRows(rows: number): void {
215
+ override setNativeScrollbackCommittedRows(rows: number): void {
212
216
  this.#committedRows = Number.isFinite(rows) ? Math.max(0, Math.trunc(rows)) : 0;
217
+ for (let i = this.#compactedChildStart; i < this.children.length; i++) {
218
+ const child = this.children[i]!;
219
+ const segment = this.#segments[i];
220
+ if (segment === undefined || segment.component !== child) continue;
221
+ const committedContribution = Math.min(
222
+ segment.contribution.length,
223
+ Math.max(0, this.#committedRows - segment.startRow - segment.sep),
224
+ );
225
+ if (committedContribution === 0) {
226
+ setBlockCommittedRows(child, 0);
227
+ continue;
228
+ }
229
+ // Transcript assembly strips plain blank edges from each block. Map the
230
+ // committed contribution back into the child's raw render coordinates so
231
+ // nested containers can split the prefix against their exact child rows.
232
+ let leadingTrimmedRows = 0;
233
+ while (leadingTrimmedRows < segment.rawRef.length && isPlainBlank(segment.rawRef[leadingTrimmedRows]!)) {
234
+ leadingTrimmedRows++;
235
+ }
236
+ setBlockCommittedRows(child, Math.min(segment.rawRef.length, leadingTrimmedRows + committedContribution));
237
+ }
213
238
  }
214
239
 
215
- prepareNativeScrollbackReplay(): void {
240
+ override prepareNativeScrollbackReplay(): void {
241
+ // Replay retires the old terminal tape, so descendants may discard layout
242
+ // locks whose only purpose was keeping that immutable history byte-stable.
243
+ super.prepareNativeScrollbackReplay();
216
244
  if (this.#compactedChildStart === 0) return;
217
245
  this.#compactedChildStart = 0;
218
246
  this.#replayPending = true;
@@ -105,5 +105,5 @@ export function showCommandMessage(ctx: InteractiveModeContext, text: string): v
105
105
  block.addChild(new DynamicBorder());
106
106
  block.addChild(new Text(text, 1, 1));
107
107
  block.addChild(new DynamicBorder());
108
- ctx.present(block);
108
+ ctx.presentCommandOutput(block);
109
109
  }