diff --git a/packages/coding-agent/src/edit/renderer.ts b/packages/coding-agent/src/edit/renderer.ts index e8fab3e0c..5aeb6e63d 100644 --- a/packages/coding-agent/src/edit/renderer.ts +++ b/packages/coding-agent/src/edit/renderer.ts @@ -283,7 +283,7 @@ function renderEditHeader( width: number, uiTheme: Theme, options: { - icon: "pending" | "success" | "error"; + icon?: "pending" | "success" | "error"; iconOverride?: string; op?: Operation; rawPath: string; @@ -614,14 +614,12 @@ export const editToolRenderer = { } const callPreviewCaches: RenderedStringCache[] = []; return framedBlock(uiTheme, width => { - // Static pending icon, never the animated glyph: the header is the - // head row of the framed block, and native-scrollback commits are - // prefix-only — an animating head row would pin the commit boundary - // at the top and keep a tall expanded preview from scroll-appending - // mid-stream. The liveness cue rides the trailing "(preview)" / + // No status icon on the head row: it's the head of the framed block, + // and native-scrollback commits are prefix-only — an animated glyph + // would pin the commit boundary at the top, and the pending hourglass + // just adds noise. The liveness cue rides the trailing "(preview)" / // "(streaming)" line instead. const header = renderEditHeader(width, uiTheme, { - icon: "pending", op, rawPath, rename, @@ -796,20 +794,18 @@ function renderMultiFileResult( if (allLines.length > 0) allLines.push(""); const spinnerFrame = options.spinnerFrame; const spinner = spinnerFrame !== undefined ? formatStatusIcon("running", uiTheme, spinnerFrame) : ""; + // Spinner while actively rendering, otherwise no icon — never the + // pending hourglass on the head row. allLines.push( renderStatusLine( { - icon: "pending", + iconOverride: spinner, title: "Edit", description: uiTheme.fg("dim", `${remaining} more file${remaining > 1 ? "s" : ""} pending…`), }, uiTheme, ), ); - if (spinner) { - // Replace the pending icon with spinner on the last line - allLines[allLines.length - 1] = allLines[allLines.length - 1].replace(/^(?:\x1b\[[^m]*m)*./u, spinner); - } } cached = { key, lines: allLines }; diff --git a/packages/coding-agent/src/modes/components/tool-execution.ts b/packages/coding-agent/src/modes/components/tool-execution.ts index 22326b605..a5d1937f8 100644 --- a/packages/coding-agent/src/modes/components/tool-execution.ts +++ b/packages/coding-agent/src/modes/components/tool-execution.ts @@ -31,7 +31,13 @@ import { JSON_TREE_SCALAR_LEN_EXPANDED, renderJsonTreeLines, } from "../../tools/json-tree"; -import { formatExpandHint, replaceTabs, resolveImageOptions, truncateToWidth } from "../../tools/render-utils"; +import { + formatExpandHint, + formatStatusIcon, + replaceTabs, + resolveImageOptions, + truncateToWidth, +} from "../../tools/render-utils"; import { toolRenderers } from "../../tools/renderers"; import { TODO_STRIKE_TOTAL_FRAMES } from "../../tools/todo"; import { isFramedBlockComponent, renderStatusLine, WidthAwareText } from "../../tui"; @@ -865,9 +871,11 @@ export class ToolExecutionComponent extends Container implements NativeScrollbac this.#multiFileBoxes.push(pendingSpacer); this.addChild(pendingSpacer); const pendingBox = new Box(0, 0); + const spinner = + this.#spinnerFrame !== undefined ? formatStatusIcon("running", theme, this.#spinnerFrame) : ""; const pendingText = renderStatusLine( { - icon: "pending", + iconOverride: spinner, title: "Edit", description: theme.fg("dim", `${remaining} more file${remaining > 1 ? "s" : ""} pending…`), }, diff --git a/packages/coding-agent/src/tools/write.ts b/packages/coding-agent/src/tools/write.ts index ade8a4b46..775ed92b5 100644 --- a/packages/coding-agent/src/tools/write.ts +++ b/packages/coding-agent/src/tools/write.ts @@ -1068,14 +1068,12 @@ export const writeToolRenderer = { const lang = getLanguageFromPath(rawPath) ?? "text"; const langIcon = uiTheme.fg("muted", uiTheme.getLangIcon(lang)); const pathDisplay = filePath ? uiTheme.fg("accent", filePath) : uiTheme.fg("toolOutput", "…"); - // Static pending icon, never the animated glyph: the header is the head - // row of the framed block, and native-scrollback commits are prefix-only - // — an animating head row would pin the commit boundary at the top and - // keep a tall expanded preview from scroll-appending mid-stream. The - // liveness cue rides the trailing "(streaming)" line instead. + // No status icon on the head row: it's the head of the framed block, and + // native-scrollback commits are prefix-only — an animated glyph would pin + // the commit boundary at the top, and the pending hourglass just adds + // noise. The liveness cue rides the trailing "(streaming)" line instead. const header = renderStatusLine( { - icon: "pending", title: "Write", description: `${langIcon} ${pathDisplay}`, },