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

@@ -2,6 +2,9 @@
## [Unreleased]
### Fixed
- Fixed custom tool collapsed/expanded rendering in HTML exports. Custom tools that define different collapsed vs expanded displays now render correctly in exported HTML, with expandable sections when both states differ and direct display when only expanded exists ([#1934](https://github.com/badlogic/pi-mono/pull/1934) by [@aliou](https://github.com/aliou))
## [0.57.0] - 2026-03-07
### New Features

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;