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] ## [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 ## [0.57.0] - 2026-03-07
### New Features ### New Features

View File

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

View File

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

View File

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