perf(coding-agent): optimized tool rendering with caching, improved tool grouping, and enhanced string preview formatting

- Implemented caching for tool output block rendering to avoid redundant computations on re-renders.
- Enhanced read tool grouping to prevent coalescing across narrative boundaries (text/thinking blocks between tool calls).
- Improved string preview formatting to detect multi-line strings and display line counts with ellipsis indicators.
- Refactored tool execution component to use reactive render state tracking for better state management.
- Enhanced error handling in tool renderers with logging for failures.
- Fixed truncation indicator to use ellipsis character instead of verbose suffix.
This commit is contained in:
can1357
2026-02-06 02:47:59 +01:00
parent 9c0828ead5
commit 43473e3b0e
25 changed files with 917 additions and 557 deletions
+20 -14
View File
@@ -832,22 +832,20 @@ export const pythonToolRenderer = {
uiTheme: Theme,
): Component {
const ui = new ToolUIKit(uiTheme);
const { renderContext } = options;
const details = result.details;
const expanded = renderContext?.expanded ?? options.expanded;
const previewLines = renderContext?.previewLines ?? PYTHON_DEFAULT_PREVIEW_LINES;
const output = renderContext?.output ?? (result.content?.find(c => c.type === "text")?.text ?? "").trimEnd();
const output =
options.renderContext?.output ?? (result.content?.find(c => c.type === "text")?.text ?? "").trimEnd();
const jsonOutputs = details?.jsonOutputs ?? [];
const jsonLines = jsonOutputs.flatMap((value, index) => {
const header = `JSON output ${index + 1}`;
const treeLines = renderJsonTree(value, uiTheme, expanded);
const treeLines = renderJsonTree(value, uiTheme, options.renderContext?.expanded ?? options.expanded);
return [header, ...treeLines];
});
const truncation = details?.meta?.truncation;
const timeoutSeconds = renderContext?.timeout;
const timeoutSeconds = options.renderContext?.timeout;
const timeoutLine =
typeof timeoutSeconds === "number"
? uiTheme.fg("dim", ui.wrapBrackets(`Timeout: ${timeoutSeconds}s`))
@@ -875,13 +873,12 @@ export const pythonToolRenderer = {
// Cache state following Box pattern
let cached: { key: string; width: number; result: string[] } | undefined;
const buildCacheKey = (spinnerFrame: number | undefined): string => {
return `${expanded}|${previewLines}|${spinnerFrame}`;
};
return {
render: (width: number): string[] => {
const key = buildCacheKey(options.spinnerFrame);
// Read mutable state at render time
const expanded = options.renderContext?.expanded ?? options.expanded;
const previewLines = options.renderContext?.previewLines ?? PYTHON_DEFAULT_PREVIEW_LINES;
const key = `${expanded}|${previewLines}|${options.spinnerFrame}`;
if (cached && cached.key === key && cached.width === width) {
return cached.result;
}
@@ -951,7 +948,11 @@ export const pythonToolRenderer = {
const combinedOutput = [displayOutput, ...jsonLines].filter(Boolean).join("\n");
const statusEvents = details?.statusEvents ?? [];
const statusLines = renderStatusEvents(statusEvents, uiTheme, expanded);
const statusLines = renderStatusEvents(
statusEvents,
uiTheme,
options.renderContext?.expanded ?? options.expanded,
);
if (!combinedOutput && statusLines.length === 0) {
const lines = [timeoutLine, warningLine].filter(Boolean) as string[];
@@ -965,7 +966,7 @@ export const pythonToolRenderer = {
return new Text(lines.join("\n"), 0, 0);
}
if (expanded) {
if (options.renderContext?.expanded ?? options.expanded) {
const styledOutput = combinedOutput
.split("\n")
.map(line => uiTheme.fg("toolOutput", line))
@@ -988,14 +989,18 @@ export const pythonToolRenderer = {
let cachedWidth: number | undefined;
let cachedLines: string[] | undefined;
let cachedSkipped: number | undefined;
let cachedPreviewLines: number | undefined;
return {
render: (width: number): string[] => {
if (cachedLines === undefined || cachedWidth !== width) {
// Read mutable state at render time
const previewLines = options.renderContext?.previewLines ?? PYTHON_DEFAULT_PREVIEW_LINES;
if (cachedLines === undefined || cachedWidth !== width || cachedPreviewLines !== previewLines) {
const result = truncateToVisualLines(textContent, previewLines, width);
cachedLines = result.visualLines;
cachedSkipped = result.skippedCount;
cachedWidth = width;
cachedPreviewLines = previewLines;
}
const outputLines: string[] = [];
if (cachedSkipped && cachedSkipped > 0) {
@@ -1025,6 +1030,7 @@ export const pythonToolRenderer = {
cachedWidth = undefined;
cachedLines = undefined;
cachedSkipped = undefined;
cachedPreviewLines = undefined;
},
};
},