fix(coding-agent): tighten HTML export tool spacing
This commit is contained in:
@@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
- Fixed HTML export whitespace around extension-rendered tool output and expandable output hints.
|
||||||
- Fixed bash executor temp output streams leaking file descriptors when output was truncated by line count ([#3786](https://github.com/badlogic/pi-mono/issues/3786))
|
- Fixed bash executor temp output streams leaking file descriptors when output was truncated by line count ([#3786](https://github.com/badlogic/pi-mono/issues/3786))
|
||||||
- Fixed extension `pi.setSessionName()` updates to refresh the interactive terminal title immediately ([#3686](https://github.com/badlogic/pi-mono/issues/3686))
|
- Fixed extension `pi.setSessionName()` updates to refresh the interactive terminal title immediately ([#3686](https://github.com/badlogic/pi-mono/issues/3686))
|
||||||
- Fixed `/tree` cancellation via `session_before_tree` leaving the session stuck in compaction state ([#3688](https://github.com/badlogic/pi-mono/issues/3688))
|
- Fixed `/tree` cancellation via `session_before_tree` leaving the session stuck in compaction state ([#3688](https://github.com/badlogic/pi-mono/issues/3688))
|
||||||
|
|||||||
@@ -254,5 +254,5 @@ export function ansiToHtml(text: string): string {
|
|||||||
* Each line is wrapped in a div element.
|
* Each line is wrapped in a div element.
|
||||||
*/
|
*/
|
||||||
export function ansiLinesToHtml(lines: string[]): string {
|
export function ansiLinesToHtml(lines: string[]): string {
|
||||||
return lines.map((line) => `<div class="ansi-line">${ansiToHtml(line) || " "}</div>`).join("\n");
|
return lines.map((line) => `<div class="ansi-line">${ansiToHtml(line) || " "}</div>`).join("");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -505,11 +505,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.tool-output > div,
|
.tool-output > div,
|
||||||
.output-preview,
|
.output-preview > div,
|
||||||
.output-full {
|
.output-full > div {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
line-height: var(--line-height);
|
line-height: var(--line-height);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tool-output > div:not(.output-preview):not(.output-full),
|
||||||
|
.output-preview > div:not(.expand-hint),
|
||||||
|
.output-full > div:not(.expand-hint) {
|
||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -41,6 +41,20 @@ export interface ToolHtmlRenderer {
|
|||||||
* The renderer looks up tool definitions and invokes their renderCall/renderResult
|
* The renderer looks up tool definitions and invokes their renderCall/renderResult
|
||||||
* methods, converting the resulting TUI Component output (ANSI) to HTML.
|
* methods, converting the resulting TUI Component output (ANSI) to HTML.
|
||||||
*/
|
*/
|
||||||
|
const ANSI_ESCAPE_REGEX = /\x1b\[[\d;]*m/g;
|
||||||
|
|
||||||
|
function isBlankRenderedLine(line: string): boolean {
|
||||||
|
return line.replace(ANSI_ESCAPE_REGEX, "").trim().length === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function trimRenderedResultLines(lines: string[]): string[] {
|
||||||
|
let start = 0;
|
||||||
|
let end = lines.length;
|
||||||
|
while (start < end && isBlankRenderedLine(lines[start])) start++;
|
||||||
|
while (end > start && isBlankRenderedLine(lines[end - 1])) end--;
|
||||||
|
return lines.slice(start, end);
|
||||||
|
}
|
||||||
|
|
||||||
export function createToolHtmlRenderer(deps: ToolHtmlRendererDeps): ToolHtmlRenderer {
|
export function createToolHtmlRenderer(deps: ToolHtmlRendererDeps): ToolHtmlRenderer {
|
||||||
const { getToolDefinition, theme, cwd, width = 100 } = deps;
|
const { getToolDefinition, theme, cwd, width = 100 } = deps;
|
||||||
|
|
||||||
@@ -133,7 +147,7 @@ export function createToolHtmlRenderer(deps: ToolHtmlRendererDeps): ToolHtmlRend
|
|||||||
createRenderContext(toolCallId, renderedResultComponents.get(toolCallId), false, false, isError),
|
createRenderContext(toolCallId, renderedResultComponents.get(toolCallId), false, false, isError),
|
||||||
);
|
);
|
||||||
renderedResultComponents.set(toolCallId, collapsedComponent);
|
renderedResultComponents.set(toolCallId, collapsedComponent);
|
||||||
const collapsed = ansiLinesToHtml(collapsedComponent.render(width));
|
const collapsed = ansiLinesToHtml(trimRenderedResultLines(collapsedComponent.render(width)));
|
||||||
|
|
||||||
// Render expanded
|
// Render expanded
|
||||||
const expandedComponent = toolDef.renderResult(
|
const expandedComponent = toolDef.renderResult(
|
||||||
@@ -143,7 +157,7 @@ export function createToolHtmlRenderer(deps: ToolHtmlRendererDeps): ToolHtmlRend
|
|||||||
createRenderContext(toolCallId, renderedResultComponents.get(toolCallId), true, false, isError),
|
createRenderContext(toolCallId, renderedResultComponents.get(toolCallId), true, false, isError),
|
||||||
);
|
);
|
||||||
renderedResultComponents.set(toolCallId, expandedComponent);
|
renderedResultComponents.set(toolCallId, expandedComponent);
|
||||||
const expanded = ansiLinesToHtml(expandedComponent.render(width));
|
const expanded = ansiLinesToHtml(trimRenderedResultLines(expandedComponent.render(width)));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...(collapsed && collapsed !== expanded ? { collapsed } : {}),
|
...(collapsed && collapsed !== expanded ? { collapsed } : {}),
|
||||||
|
|||||||
@@ -1,12 +1,41 @@
|
|||||||
|
import type { Component } from "@mariozechner/pi-tui";
|
||||||
import { readFileSync } from "fs";
|
import { readFileSync } from "fs";
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { ansiLinesToHtml } from "../src/core/export-html/ansi-to-html.js";
|
||||||
|
import { createToolHtmlRenderer } from "../src/core/export-html/tool-renderer.js";
|
||||||
|
import type { ToolDefinition } from "../src/core/extensions/types.js";
|
||||||
|
import type { Theme } from "../src/modes/interactive/theme/theme.js";
|
||||||
|
|
||||||
describe("export HTML tool output whitespace", () => {
|
describe("export HTML tool output whitespace", () => {
|
||||||
it("preserves whitespace for plain-text tool output containers", () => {
|
it("preserves whitespace for plain-text tool output lines without preserving template whitespace", () => {
|
||||||
const css = readFileSync(new URL("../src/core/export-html/template.css", import.meta.url), "utf-8");
|
const css = readFileSync(new URL("../src/core/export-html/template.css", import.meta.url), "utf-8");
|
||||||
|
|
||||||
expect(css).toMatch(
|
expect(css).toMatch(
|
||||||
/\.tool-output > div,\s*\.output-preview,\s*\.output-full\s*\{[\s\S]*?white-space:\s*pre-wrap;/,
|
/\.output-preview > div:not\(\.expand-hint\),\s*\.output-full > div:not\(\.expand-hint\) \{[\s\S]*?white-space:\s*pre-wrap;/,
|
||||||
|
);
|
||||||
|
expect(css).not.toMatch(/\.output-preview,\s*\.output-full\s*\{[\s\S]*?white-space:\s*pre-wrap;/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not insert source whitespace between ANSI-rendered lines", () => {
|
||||||
|
expect(ansiLinesToHtml(["one", "two"])).toBe('<div class="ansi-line">one</div><div class="ansi-line">two</div>');
|
||||||
|
});
|
||||||
|
|
||||||
|
it("trims TUI spacing lines from custom tool result HTML", () => {
|
||||||
|
const component: Component = { render: () => ["", "\u001b[31mone\u001b[0m", "two", ""], invalidate: () => {} };
|
||||||
|
const tool = {
|
||||||
|
name: "custom",
|
||||||
|
label: "custom",
|
||||||
|
description: "custom",
|
||||||
|
renderResult: () => component,
|
||||||
|
} as unknown as ToolDefinition;
|
||||||
|
const renderer = createToolHtmlRenderer({
|
||||||
|
getToolDefinition: () => tool,
|
||||||
|
theme: {} as Theme,
|
||||||
|
cwd: "/tmp",
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(renderer.renderResult("id", "custom", [], undefined, false)?.expanded).toBe(
|
||||||
|
'<div class="ansi-line"><span style="color:#800000">one</span></div><div class="ansi-line">two</div>',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user