fix(coding-agent): custom tool collapsed/expanded rendering in HTML export (#1934)
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user