fix(coding-agent): custom tool collapsed/expanded rendering in HTML export (#1934)

This commit is contained in:
Aliou Diallo
2026-03-07 19:01:33 +01:00
committed by GitHub
parent b279d03d33
commit e77e51e7ba
4 changed files with 48 additions and 28 deletions

View File

@@ -14,19 +14,20 @@ import { SessionManager } from "../session-manager.js";
export interface ToolHtmlRenderer {
/** Render a tool call to HTML. Returns undefined if tool has no custom renderer. */
renderCall(toolName: string, args: unknown): string | undefined;
/** Render a tool result to HTML. Returns undefined if tool has no custom renderer. */
/** Render a tool result to HTML. Returns collapsed/expanded or undefined if tool has no custom renderer. */
renderResult(
toolName: string,
result: Array<{ type: string; text?: string; data?: string; mimeType?: string }>,
details: unknown,
isError: boolean,
): string | undefined;
): { collapsed?: string; expanded?: string } | undefined;
}
/** Pre-rendered HTML for a custom tool call and result */
interface RenderedToolHtml {
callHtml?: string;
resultHtml?: string;
resultHtmlCollapsed?: string;
resultHtmlExpanded?: string;
}
export interface ExportOptions {
@@ -204,11 +205,12 @@ function preRenderCustomTools(
// Only render if we have a pre-rendered call OR it's not a built-in tool
const existing = renderedTools[msg.toolCallId];
if (existing || !BUILTIN_TOOLS.has(toolName)) {
const resultHtml = toolRenderer.renderResult(toolName, msg.content, msg.details, msg.isError || false);
if (resultHtml) {
const rendered = toolRenderer.renderResult(toolName, msg.content, msg.details, msg.isError || false);
if (rendered) {
renderedTools[msg.toolCallId] = {
...existing,
resultHtml,
resultHtmlCollapsed: rendered.collapsed,
resultHtmlExpanded: rendered.expanded,
};
}
}

View File

@@ -966,7 +966,7 @@
default: {
// Check for pre-rendered custom tool HTML
const rendered = renderedTools?.[call.id];
if (rendered?.callHtml || rendered?.resultHtml) {
if (rendered?.callHtml || rendered?.resultHtmlCollapsed || rendered?.resultHtmlExpanded) {
// Custom tool with pre-rendered HTML from TUI renderer
if (rendered.callHtml) {
html += `<div class="tool-header ansi-rendered">${rendered.callHtml}</div>`;
@@ -974,20 +974,17 @@
html += `<div class="tool-header"><span class="tool-name">${escapeHtml(name)}</span></div>`;
}
if (rendered.resultHtml) {
// Apply same truncation as built-in tools (10 lines)
const lines = rendered.resultHtml.split('\n');
if (lines.length > 10) {
const preview = lines.slice(0, 10).join('\n');
html += `<div class="tool-output expandable ansi-rendered" onclick="this.classList.toggle('expanded')">
<div class="output-preview">${preview}<div class="expand-hint">... (${lines.length - 10} more lines)</div></div>
<div class="output-full">${rendered.resultHtml}</div>
</div>`;
} else {
html += `<div class="tool-output ansi-rendered">${rendered.resultHtml}</div>`;
}
if (rendered.resultHtmlCollapsed && rendered.resultHtmlExpanded && rendered.resultHtmlCollapsed !== rendered.resultHtmlExpanded) {
// Both collapsed and expanded differ - render expandable section
html += `<div class="tool-output expandable ansi-rendered" onclick="this.classList.toggle('expanded')">
<div class="output-preview">${rendered.resultHtmlCollapsed}</div>
<div class="output-full">${rendered.resultHtmlExpanded}</div>
</div>`;
} else if (rendered.resultHtmlExpanded) {
// Only expanded exists (or collapsed is identical) - show directly
html += `<div class="tool-output ansi-rendered">${rendered.resultHtmlExpanded}</div>`;
} else if (result) {
// Fallback to JSON for result if no pre-rendered HTML
// No pre-rendered result HTML - fallback to JSON
const output = getResultText();
if (output) html += formatExpandableOutput(output, 10);
}

View File

@@ -22,13 +22,13 @@ export interface ToolHtmlRendererDeps {
export interface ToolHtmlRenderer {
/** Render a tool call to HTML. Returns undefined if tool has no custom renderer. */
renderCall(toolName: string, args: unknown): string | undefined;
/** Render a tool result to HTML. Returns undefined if tool has no custom renderer. */
/** Render a tool result to collapsed/expanded HTML. Returns undefined if tool has no custom renderer. */
renderResult(
toolName: string,
result: Array<{ type: string; text?: string; data?: string; mimeType?: string }>,
details: unknown,
isError: boolean,
): string | undefined;
): { collapsed?: string; expanded?: string } | undefined;
}
/**
@@ -65,7 +65,7 @@ export function createToolHtmlRenderer(deps: ToolHtmlRendererDeps): ToolHtmlRend
result: Array<{ type: string; text?: string; data?: string; mimeType?: string }>,
details: unknown,
isError: boolean,
): string | undefined {
): { collapsed?: string; expanded?: string } | undefined {
try {
const toolDef = getToolDefinition(toolName);
if (!toolDef?.renderResult) {
@@ -80,13 +80,31 @@ export function createToolHtmlRenderer(deps: ToolHtmlRendererDeps): ToolHtmlRend
isError,
};
// Always render expanded, client-side will apply truncation
const component = toolDef.renderResult(agentToolResult, { expanded: true, isPartial: false }, theme);
if (!component) {
// Render collapsed
const collapsedComponent = toolDef.renderResult(
agentToolResult,
{ expanded: false, isPartial: false },
theme,
);
const collapsed = collapsedComponent ? ansiLinesToHtml(collapsedComponent.render(width)) : undefined;
// Render expanded
const expandedComponent = toolDef.renderResult(
agentToolResult,
{ expanded: true, isPartial: false },
theme,
);
const expanded = expandedComponent ? ansiLinesToHtml(expandedComponent.render(width)) : undefined;
// Return collapsed only if it exists and differs from expanded
if (!expanded) {
return undefined;
}
const lines = component.render(width);
return ansiLinesToHtml(lines);
return {
...(collapsed && collapsed !== expanded ? { collapsed } : {}),
expanded,
};
} catch {
// On error, return undefined to trigger JSON fallback
return undefined;