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:
@@ -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
|
||||
|
||||
@@ -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("┌");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user