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.
This commit is contained in:
can1357
2026-03-01 02:44:52 +01:00
parent 29d8768c70
commit cd6ed4ce8c
3 changed files with 36 additions and 56 deletions
+2 -1
View File
@@ -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
+26 -44
View File
@@ -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<typeof resolveSchema, ResolveToolDetails> {
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,
};
@@ -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("┌");
});