From cd6ed4ce8c84dffbaab92aba58badf4b8a45fa59 Mon Sep 17 00:00:00 2001 From: can1357 Date: Sun, 1 Mar 2026 02:44:52 +0100 Subject: [PATCH] refactor(tools): restructured tool rendering to inline highlighted format - Simplified resolve tool output rendering from boxed layout to inline highlighted format for cleaner display. - Updated resolve tool to parse source tool name from label using colon separator instead of separate metadata field. - Replaced CachedOutputBlock with direct line rendering using padToWidth and inverse styling. - Removed actionBadge helper function and consolidated action/state logic into single rendering path. - Updated 1 test to verify inline rendering behavior and label parsing. --- packages/coding-agent/CHANGELOG.md | 3 +- packages/coding-agent/src/tools/resolve.ts | 70 +++++++------------ .../coding-agent/test/tools/resolve.test.ts | 19 +++-- 3 files changed, 36 insertions(+), 56 deletions(-) diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index ed8b87edb..94a7ba1a4 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -1,7 +1,6 @@ # Changelog ## [Unreleased] - ### Breaking Changes - `ast_grep` parameter `pattern` (string) replaced by `patterns` (string[]) @@ -27,6 +26,8 @@ ### Changed +- Simplified `resolve` tool output rendering to use inline highlighted format instead of boxed layout +- Updated `resolve` tool to parse source tool name from label using colon separator for cleaner display - AST edit tool no longer accepts `preview` parameter; all AST edit calls now return previews by default - AST edit workflow changed: preview is always shown, then use `resolve` tool to apply or discard changes - Agent now suggests calling `resolve` tool after AST edit preview with system reminder diff --git a/packages/coding-agent/src/tools/resolve.ts b/packages/coding-agent/src/tools/resolve.ts index c3ea635da..0928d69ef 100644 --- a/packages/coding-agent/src/tools/resolve.ts +++ b/packages/coding-agent/src/tools/resolve.ts @@ -7,7 +7,7 @@ import { renderPromptTemplate } from "../config/prompt-templates"; import type { RenderResultOptions } from "../extensibility/custom-tools/types"; import type { Theme } from "../modes/theme/theme"; import resolveDescription from "../prompts/tools/resolve.md" with { type: "text" }; -import { CachedOutputBlock, Ellipsis, renderStatusLine, truncateToWidth } from "../tui"; +import { Ellipsis, padToWidth, renderStatusLine, truncateToWidth } from "../tui"; import type { ToolSession } from "."; import { replaceTabs } from "./render-utils"; import { ToolError } from "./tool-errors"; @@ -33,12 +33,6 @@ function resolveReasonPreview(reason?: string): string | undefined { return truncateToWidth(trimmed, 72, Ellipsis.Omit); } -function actionBadge(action: "apply" | "discard", theme: Theme): string { - const color = action === "apply" ? "success" : "warning"; - const left = theme.format.bracketLeft; - const right = theme.format.bracketRight; - return theme.fg(color, `${left}${action}${right}`); -} export class ResolveTool implements AgentTool { readonly name = "resolve"; readonly label = "Resolve"; @@ -119,50 +113,38 @@ export const resolveToolRenderer = { uiTheme: Theme, ): Component { const details = result.details; - const label = details?.label ?? "pending action"; - const reason = details?.reason?.trim() || "No reason provided"; - const textContent = result.content.find(part => part.type === "text")?.text?.trim(); + const label = replaceTabs(details?.label ?? "pending action"); + const reason = replaceTabs(details?.reason?.trim() || "No reason provided"); const action = details?.action ?? "apply"; - const state = result.isError ? "error" : action === "discard" ? "warning" : "success"; - const headerMeta = details?.sourceToolName ? `from ${details.sourceToolName}` : undefined; - const actionLine = `${uiTheme.bold("Action")}: ${actionBadge(action, uiTheme)}`; - const targetLine = `${uiTheme.bold("Target")}: ${uiTheme.fg("accent", replaceTabs(label))}`; - const reasonLine = `${uiTheme.bold("Reason")}: ${replaceTabs(reason)}`; - const fallbackEffect = result.isError - ? "Resolve failed. No pending action was changed." - : action === "discard" - ? `Discarded preview for ${label}.` - : `Applied preview for ${label}.`; - const effectLines = (textContent || fallbackEffect).split("\n").map(line => replaceTabs(line.trimEnd())); - const outputBlock = new CachedOutputBlock(); + const isApply = action === "apply" && !result.isError; + const bgColor = result.isError ? "error" : isApply ? "success" : "warning"; + const icon = isApply ? uiTheme.status.success : uiTheme.status.error; + const verb = isApply ? "Accept" : "Discard"; + const separator = ": "; + const separatorIndex = label.indexOf(separator); + const sourceLabel = separatorIndex > 0 ? label.slice(0, separatorIndex).trim() : undefined; + const summaryLabel = separatorIndex > 0 ? label.slice(separatorIndex + separator.length).trim() : label; + const sourceBadge = sourceLabel + ? uiTheme.bold(`${uiTheme.format.bracketLeft}${sourceLabel}${uiTheme.format.bracketRight}`) + : undefined; + const headerLine = `${icon} ${uiTheme.bold(`${verb}:`)} ${summaryLabel}${sourceBadge ? ` ${sourceBadge}` : ""}`; + const lines = ["", headerLine, "", uiTheme.italic(reason), ""]; return { render(width: number) { - return outputBlock.render( - { - header: `${uiTheme.bold("Resolve")}`, - headerMeta, - state, - width, - sections: [ - { - label: uiTheme.fg("toolTitle", "Decision"), - lines: [actionLine, targetLine, reasonLine], - }, - { - label: uiTheme.fg("toolTitle", result.isError ? "Error" : "Effect"), - lines: effectLines, - }, - ], - }, - uiTheme, - ); - }, - invalidate() { - outputBlock.invalidate(); + const lineWidth = Math.max(3, width); + const innerWidth = Math.max(1, lineWidth - 2); + return lines.map(line => { + const truncated = truncateToWidth(line, innerWidth, Ellipsis.Omit); + const framed = ` ${padToWidth(truncated, innerWidth)} `; + const padded = padToWidth(framed, lineWidth); + return uiTheme.inverse(uiTheme.fg(bgColor, padded)); + }); }, + invalidate() {}, }; }, + inline: true, mergeCallAndResult: true, }; diff --git a/packages/coding-agent/test/tools/resolve.test.ts b/packages/coding-agent/test/tools/resolve.test.ts index a6d2932c2..a7caa7b11 100644 --- a/packages/coding-agent/test/tools/resolve.test.ts +++ b/packages/coding-agent/test/tools/resolve.test.ts @@ -89,7 +89,7 @@ describe("ResolveTool", () => { }); }); -it("renders a boxed decision summary for apply actions", async () => { +it("renders a highlighted apply summary", async () => { const theme = await getThemeByName("dark"); expect(theme).toBeDefined(); const uiTheme = theme!; @@ -99,7 +99,7 @@ it("renders a boxed decision summary for apply actions", async () => { content: [{ type: "text", text: "Applied 2 replacements in 1 file." }], details: { action: "apply", - reason: "Preview matches expected replacements", + reason: "All replacements are correct", sourceToolName: "ast_edit", label: "AST Edit: 2 replacements in 1 file", }, @@ -109,13 +109,10 @@ it("renders a boxed decision summary for apply actions", async () => { ); const rendered = sanitizeText(component.render(90).join("\n")); - expect(rendered).toContain("Resolve"); - expect(rendered).toContain("Decision"); - expect(rendered).toContain("Effect"); - expect(rendered).toContain("Action:"); - expect(rendered).toContain("apply"); - expect(rendered).toContain("Target: AST Edit: 2 replacements in 1 file"); - expect(rendered).toContain("Reason: Preview matches expected replacements"); - expect(rendered).toContain("Applied 2 replacements in 1 file."); - expect(rendered).toContain("┌"); + expect(rendered).toContain("Accept: 2 replacements in 1 file"); + expect(rendered).toContain("AST Edit"); + expect(rendered).toContain("All replacements are correct"); + expect(rendered).not.toContain("Applied 2 replacements in 1 file."); + expect(rendered).not.toContain("Decision"); + expect(rendered).not.toContain("┌"); });