pull into local

This commit is contained in:
Harivansh Rathi 2026-03-11 02:31:07 -04:00
parent dd0c89d8aa
commit d0f85b30cc
50 changed files with 26814 additions and 86 deletions

View file

@ -0,0 +1,22 @@
/**
* ANSI escape code to HTML converter.
*
* Converts terminal ANSI color/style codes to HTML with inline styles.
* Supports:
* - Standard foreground colors (30-37) and bright variants (90-97)
* - Standard background colors (40-47) and bright variants (100-107)
* - 256-color palette (38;5;N and 48;5;N)
* - RGB true color (38;2;R;G;B and 48;2;R;G;B)
* - Text styles: bold (1), dim (2), italic (3), underline (4)
* - Reset (0)
*/
/**
* Convert ANSI-escaped text to HTML with inline styles.
*/
export declare function ansiToHtml(text: string): string;
/**
* Convert array of ANSI-escaped lines to HTML.
* Each line is wrapped in a div element.
*/
export declare function ansiLinesToHtml(lines: string[]): string;
//# sourceMappingURL=ansi-to-html.d.ts.map

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,239 @@
/**
* ANSI escape code to HTML converter.
*
* Converts terminal ANSI color/style codes to HTML with inline styles.
* Supports:
* - Standard foreground colors (30-37) and bright variants (90-97)
* - Standard background colors (40-47) and bright variants (100-107)
* - 256-color palette (38;5;N and 48;5;N)
* - RGB true color (38;2;R;G;B and 48;2;R;G;B)
* - Text styles: bold (1), dim (2), italic (3), underline (4)
* - Reset (0)
*/
// Standard ANSI color palette (0-15)
const ANSI_COLORS = [
"#000000", // 0: black
"#800000", // 1: red
"#008000", // 2: green
"#808000", // 3: yellow
"#000080", // 4: blue
"#800080", // 5: magenta
"#008080", // 6: cyan
"#c0c0c0", // 7: white
"#808080", // 8: bright black
"#ff0000", // 9: bright red
"#00ff00", // 10: bright green
"#ffff00", // 11: bright yellow
"#0000ff", // 12: bright blue
"#ff00ff", // 13: bright magenta
"#00ffff", // 14: bright cyan
"#ffffff", // 15: bright white
];
/**
* Convert 256-color index to hex.
*/
function color256ToHex(index) {
// Standard colors (0-15)
if (index < 16) {
return ANSI_COLORS[index];
}
// Color cube (16-231): 6x6x6 = 216 colors
if (index < 232) {
const cubeIndex = index - 16;
const r = Math.floor(cubeIndex / 36);
const g = Math.floor((cubeIndex % 36) / 6);
const b = cubeIndex % 6;
const toComponent = (n) => (n === 0 ? 0 : 55 + n * 40);
const toHex = (n) => toComponent(n).toString(16).padStart(2, "0");
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
}
// Grayscale (232-255): 24 shades
const gray = 8 + (index - 232) * 10;
const grayHex = gray.toString(16).padStart(2, "0");
return `#${grayHex}${grayHex}${grayHex}`;
}
/**
* Escape HTML special characters.
*/
function escapeHtml(text) {
return text
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#039;");
}
function createEmptyStyle() {
return {
fg: null,
bg: null,
bold: false,
dim: false,
italic: false,
underline: false,
};
}
function styleToInlineCSS(style) {
const parts = [];
if (style.fg) parts.push(`color:${style.fg}`);
if (style.bg) parts.push(`background-color:${style.bg}`);
if (style.bold) parts.push("font-weight:bold");
if (style.dim) parts.push("opacity:0.6");
if (style.italic) parts.push("font-style:italic");
if (style.underline) parts.push("text-decoration:underline");
return parts.join(";");
}
function hasStyle(style) {
return (
style.fg !== null ||
style.bg !== null ||
style.bold ||
style.dim ||
style.italic ||
style.underline
);
}
/**
* Parse ANSI SGR (Select Graphic Rendition) codes and update style.
*/
function applySgrCode(params, style) {
let i = 0;
while (i < params.length) {
const code = params[i];
if (code === 0) {
// Reset all
style.fg = null;
style.bg = null;
style.bold = false;
style.dim = false;
style.italic = false;
style.underline = false;
} else if (code === 1) {
style.bold = true;
} else if (code === 2) {
style.dim = true;
} else if (code === 3) {
style.italic = true;
} else if (code === 4) {
style.underline = true;
} else if (code === 22) {
// Reset bold/dim
style.bold = false;
style.dim = false;
} else if (code === 23) {
style.italic = false;
} else if (code === 24) {
style.underline = false;
} else if (code >= 30 && code <= 37) {
// Standard foreground colors
style.fg = ANSI_COLORS[code - 30];
} else if (code === 38) {
// Extended foreground color
if (params[i + 1] === 5 && params.length > i + 2) {
// 256-color: 38;5;N
style.fg = color256ToHex(params[i + 2]);
i += 2;
} else if (params[i + 1] === 2 && params.length > i + 4) {
// RGB: 38;2;R;G;B
const r = params[i + 2];
const g = params[i + 3];
const b = params[i + 4];
style.fg = `rgb(${r},${g},${b})`;
i += 4;
}
} else if (code === 39) {
// Default foreground
style.fg = null;
} else if (code >= 40 && code <= 47) {
// Standard background colors
style.bg = ANSI_COLORS[code - 40];
} else if (code === 48) {
// Extended background color
if (params[i + 1] === 5 && params.length > i + 2) {
// 256-color: 48;5;N
style.bg = color256ToHex(params[i + 2]);
i += 2;
} else if (params[i + 1] === 2 && params.length > i + 4) {
// RGB: 48;2;R;G;B
const r = params[i + 2];
const g = params[i + 3];
const b = params[i + 4];
style.bg = `rgb(${r},${g},${b})`;
i += 4;
}
} else if (code === 49) {
// Default background
style.bg = null;
} else if (code >= 90 && code <= 97) {
// Bright foreground colors
style.fg = ANSI_COLORS[code - 90 + 8];
} else if (code >= 100 && code <= 107) {
// Bright background colors
style.bg = ANSI_COLORS[code - 100 + 8];
}
// Ignore unrecognized codes
i++;
}
}
// Match ANSI escape sequences: ESC[ followed by params and ending with 'm'
const ANSI_REGEX = /\x1b\[([\d;]*)m/g;
/**
* Convert ANSI-escaped text to HTML with inline styles.
*/
export function ansiToHtml(text) {
const style = createEmptyStyle();
let result = "";
let lastIndex = 0;
let inSpan = false;
// Reset regex state
ANSI_REGEX.lastIndex = 0;
let match = ANSI_REGEX.exec(text);
while (match !== null) {
// Add text before this escape sequence
const beforeText = text.slice(lastIndex, match.index);
if (beforeText) {
result += escapeHtml(beforeText);
}
// Parse SGR parameters
const paramStr = match[1];
const params = paramStr
? paramStr.split(";").map((p) => parseInt(p, 10) || 0)
: [0];
// Close existing span if we have one
if (inSpan) {
result += "</span>";
inSpan = false;
}
// Apply the codes
applySgrCode(params, style);
// Open new span if we have any styling
if (hasStyle(style)) {
result += `<span style="${styleToInlineCSS(style)}">`;
inSpan = true;
}
lastIndex = match.index + match[0].length;
match = ANSI_REGEX.exec(text);
}
// Add remaining text
const remainingText = text.slice(lastIndex);
if (remainingText) {
result += escapeHtml(remainingText);
}
// Close any open span
if (inSpan) {
result += "</span>";
}
return result;
}
/**
* Convert array of ANSI-escaped lines to HTML.
* Each line is wrapped in a div element.
*/
export function ansiLinesToHtml(lines) {
return lines
.map(
(line) => `<div class="ansi-line">${ansiToHtml(line) || "&nbsp;"}</div>`,
)
.join("\n");
}
//# sourceMappingURL=ansi-to-html.js.map

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,46 @@
import type { AgentState } from "@mariozechner/companion-agent-core";
import { SessionManager } from "../session-manager.js";
/**
* Interface for rendering custom tools to HTML.
* Used by agent-session to pre-render extension tool output.
*/
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. */
renderResult(
toolName: string,
result: Array<{
type: string;
text?: string;
data?: string;
mimeType?: string;
}>,
details: unknown,
isError: boolean,
): string | undefined;
}
export interface ExportOptions {
outputPath?: string;
themeName?: string;
/** Optional tool renderer for custom tools */
toolRenderer?: ToolHtmlRenderer;
}
/**
* Export session to HTML using SessionManager and AgentState.
* Used by TUI's /export command.
*/
export declare function exportSessionToHtml(
sm: SessionManager,
state?: AgentState,
options?: ExportOptions | string,
): Promise<string>;
/**
* Export session file to HTML (standalone, without AgentState).
* Used by CLI for exporting arbitrary session files.
*/
export declare function exportFromFile(
inputPath: string,
options?: ExportOptions | string,
): Promise<string>;
//# sourceMappingURL=index.d.ts.map

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,254 @@
import { existsSync, readFileSync, writeFileSync } from "fs";
import { basename, join } from "path";
import { APP_NAME, getExportTemplateDir } from "../../config.js";
import {
getResolvedThemeColors,
getThemeExportColors,
} from "../../modes/interactive/theme/theme.js";
import { SessionManager } from "../session-manager.js";
/** Parse a color string to RGB values. Supports hex (#RRGGBB) and rgb(r,g,b) formats. */
function parseColor(color) {
const hexMatch = color.match(
/^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/,
);
if (hexMatch) {
return {
r: Number.parseInt(hexMatch[1], 16),
g: Number.parseInt(hexMatch[2], 16),
b: Number.parseInt(hexMatch[3], 16),
};
}
const rgbMatch = color.match(
/^rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)$/,
);
if (rgbMatch) {
return {
r: Number.parseInt(rgbMatch[1], 10),
g: Number.parseInt(rgbMatch[2], 10),
b: Number.parseInt(rgbMatch[3], 10),
};
}
return undefined;
}
/** Calculate relative luminance of a color (0-1, higher = lighter). */
function getLuminance(r, g, b) {
const toLinear = (c) => {
const s = c / 255;
return s <= 0.03928 ? s / 12.92 : ((s + 0.055) / 1.055) ** 2.4;
};
return 0.2126 * toLinear(r) + 0.7152 * toLinear(g) + 0.0722 * toLinear(b);
}
/** Adjust color brightness. Factor > 1 lightens, < 1 darkens. */
function adjustBrightness(color, factor) {
const parsed = parseColor(color);
if (!parsed) return color;
const adjust = (c) => Math.min(255, Math.max(0, Math.round(c * factor)));
return `rgb(${adjust(parsed.r)}, ${adjust(parsed.g)}, ${adjust(parsed.b)})`;
}
/** Derive export background colors from a base color (e.g., userMessageBg). */
function deriveExportColors(baseColor) {
const parsed = parseColor(baseColor);
if (!parsed) {
return {
pageBg: "rgb(24, 24, 30)",
cardBg: "rgb(30, 30, 36)",
infoBg: "rgb(60, 55, 40)",
};
}
const luminance = getLuminance(parsed.r, parsed.g, parsed.b);
const isLight = luminance > 0.5;
if (isLight) {
return {
pageBg: adjustBrightness(baseColor, 0.96),
cardBg: baseColor,
infoBg: `rgb(${Math.min(255, parsed.r + 10)}, ${Math.min(255, parsed.g + 5)}, ${Math.max(0, parsed.b - 20)})`,
};
}
return {
pageBg: adjustBrightness(baseColor, 0.7),
cardBg: adjustBrightness(baseColor, 0.85),
infoBg: `rgb(${Math.min(255, parsed.r + 20)}, ${Math.min(255, parsed.g + 15)}, ${parsed.b})`,
};
}
/**
* Generate CSS custom property declarations from theme colors.
*/
function generateThemeVars(themeName) {
const colors = getResolvedThemeColors(themeName);
const lines = [];
for (const [key, value] of Object.entries(colors)) {
lines.push(`--${key}: ${value};`);
}
// Use explicit theme export colors if available, otherwise derive from userMessageBg
const themeExport = getThemeExportColors(themeName);
const userMessageBg = colors.userMessageBg || "#343541";
const derivedColors = deriveExportColors(userMessageBg);
lines.push(`--exportPageBg: ${themeExport.pageBg ?? derivedColors.pageBg};`);
lines.push(`--exportCardBg: ${themeExport.cardBg ?? derivedColors.cardBg};`);
lines.push(`--exportInfoBg: ${themeExport.infoBg ?? derivedColors.infoBg};`);
return lines.join("\n ");
}
/**
* Core HTML generation logic shared by both export functions.
*/
function generateHtml(sessionData, themeName) {
const templateDir = getExportTemplateDir();
const template = readFileSync(join(templateDir, "template.html"), "utf-8");
const templateCss = readFileSync(join(templateDir, "template.css"), "utf-8");
const templateJs = readFileSync(join(templateDir, "template.js"), "utf-8");
const markedJs = readFileSync(
join(templateDir, "vendor", "marked.min.js"),
"utf-8",
);
const hljsJs = readFileSync(
join(templateDir, "vendor", "highlight.min.js"),
"utf-8",
);
const themeVars = generateThemeVars(themeName);
const colors = getResolvedThemeColors(themeName);
const exportColors = deriveExportColors(colors.userMessageBg || "#343541");
const bodyBg = exportColors.pageBg;
const containerBg = exportColors.cardBg;
const infoBg = exportColors.infoBg;
// Base64 encode session data to avoid escaping issues
const sessionDataBase64 = Buffer.from(JSON.stringify(sessionData)).toString(
"base64",
);
// Build the CSS with theme variables injected
const css = templateCss
.replace("{{THEME_VARS}}", themeVars)
.replace("{{BODY_BG}}", bodyBg)
.replace("{{CONTAINER_BG}}", containerBg)
.replace("{{INFO_BG}}", infoBg);
return template
.replace("{{CSS}}", css)
.replace("{{JS}}", templateJs)
.replace("{{SESSION_DATA}}", sessionDataBase64)
.replace("{{MARKED_JS}}", markedJs)
.replace("{{HIGHLIGHT_JS}}", hljsJs);
}
/** Built-in tool names that have custom rendering in template.js */
const BUILTIN_TOOLS = new Set([
"bash",
"read",
"write",
"edit",
"ls",
"find",
"grep",
]);
/**
* Pre-render custom tools to HTML using their TUI renderers.
*/
function preRenderCustomTools(entries, toolRenderer) {
const renderedTools = {};
for (const entry of entries) {
if (entry.type !== "message") continue;
const msg = entry.message;
// Find tool calls in assistant messages
if (msg.role === "assistant" && Array.isArray(msg.content)) {
for (const block of msg.content) {
if (block.type === "toolCall" && !BUILTIN_TOOLS.has(block.name)) {
const callHtml = toolRenderer.renderCall(block.name, block.arguments);
if (callHtml) {
renderedTools[block.id] = { callHtml };
}
}
}
}
// Find tool results
if (msg.role === "toolResult" && msg.toolCallId) {
const toolName = msg.toolName || "";
// 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) {
renderedTools[msg.toolCallId] = {
...existing,
resultHtml,
};
}
}
}
}
return renderedTools;
}
/**
* Export session to HTML using SessionManager and AgentState.
* Used by TUI's /export command.
*/
export async function exportSessionToHtml(sm, state, options) {
const opts =
typeof options === "string" ? { outputPath: options } : options || {};
const sessionFile = sm.getSessionFile();
if (!sessionFile) {
throw new Error("Cannot export in-memory session to HTML");
}
if (!existsSync(sessionFile)) {
throw new Error("Nothing to export yet - start a conversation first");
}
const entries = sm.getEntries();
// Pre-render custom tools if a tool renderer is provided
let renderedTools;
if (opts.toolRenderer) {
renderedTools = preRenderCustomTools(entries, opts.toolRenderer);
// Only include if we actually rendered something
if (Object.keys(renderedTools).length === 0) {
renderedTools = undefined;
}
}
const sessionData = {
header: sm.getHeader(),
entries,
leafId: sm.getLeafId(),
systemPrompt: state?.systemPrompt,
tools: state?.tools?.map((t) => ({
name: t.name,
description: t.description,
parameters: t.parameters,
})),
renderedTools,
};
const html = generateHtml(sessionData, opts.themeName);
let outputPath = opts.outputPath;
if (!outputPath) {
const sessionBasename = basename(sessionFile, ".jsonl");
outputPath = `${APP_NAME}-session-${sessionBasename}.html`;
}
writeFileSync(outputPath, html, "utf8");
return outputPath;
}
/**
* Export session file to HTML (standalone, without AgentState).
* Used by CLI for exporting arbitrary session files.
*/
export async function exportFromFile(inputPath, options) {
const opts =
typeof options === "string" ? { outputPath: options } : options || {};
if (!existsSync(inputPath)) {
throw new Error(`File not found: ${inputPath}`);
}
const sm = SessionManager.open(inputPath);
const sessionData = {
header: sm.getHeader(),
entries: sm.getEntries(),
leafId: sm.getLeafId(),
systemPrompt: undefined,
tools: undefined,
};
const html = generateHtml(sessionData, opts.themeName);
let outputPath = opts.outputPath;
if (!outputPath) {
const inputBasename = basename(inputPath, ".jsonl");
outputPath = `${APP_NAME}-session-${inputBasename}.html`;
}
writeFileSync(outputPath, html, "utf8");
return outputPath;
}
//# sourceMappingURL=index.js.map

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,971 @@
:root {
{{THEME_VARS}}
--body-bg: {{BODY_BG}};
--container-bg: {{CONTAINER_BG}};
--info-bg: {{INFO_BG}};
}
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--line-height: 18px; /* 12px font * 1.5 */
}
body {
font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono', monospace;
font-size: 12px;
line-height: var(--line-height);
color: var(--text);
background: var(--body-bg);
}
#app {
display: flex;
min-height: 100vh;
}
/* Sidebar */
#sidebar {
width: 400px;
background: var(--container-bg);
flex-shrink: 0;
display: flex;
flex-direction: column;
position: sticky;
top: 0;
height: 100vh;
border-right: 1px solid var(--dim);
}
.sidebar-header {
padding: 8px 12px;
flex-shrink: 0;
}
.sidebar-controls {
padding: 8px 8px 4px 8px;
}
.sidebar-search {
width: 100%;
box-sizing: border-box;
padding: 4px 8px;
font-size: 11px;
font-family: inherit;
background: var(--body-bg);
color: var(--text);
border: 1px solid var(--dim);
border-radius: 3px;
}
.sidebar-filters {
display: flex;
padding: 4px 8px 8px 8px;
gap: 4px;
align-items: center;
flex-wrap: wrap;
}
.sidebar-search:focus {
outline: none;
border-color: var(--accent);
}
.sidebar-search::placeholder {
color: var(--muted);
}
.filter-btn {
padding: 3px 8px;
font-size: 10px;
font-family: inherit;
background: transparent;
color: var(--muted);
border: 1px solid var(--dim);
border-radius: 3px;
cursor: pointer;
}
.filter-btn:hover {
color: var(--text);
border-color: var(--text);
}
.filter-btn.active {
background: var(--accent);
color: var(--body-bg);
border-color: var(--accent);
}
.sidebar-close {
display: none;
padding: 3px 8px;
font-size: 12px;
font-family: inherit;
background: transparent;
color: var(--muted);
border: 1px solid var(--dim);
border-radius: 3px;
cursor: pointer;
margin-left: auto;
}
.sidebar-close:hover {
color: var(--text);
border-color: var(--text);
}
.tree-container {
flex: 1;
overflow: auto;
padding: 4px 0;
}
.tree-node {
padding: 0 8px;
cursor: pointer;
display: flex;
align-items: baseline;
font-size: 11px;
line-height: 13px;
white-space: nowrap;
}
.tree-node:hover {
background: var(--selectedBg);
}
.tree-node.active {
background: var(--selectedBg);
}
.tree-node.active .tree-content {
font-weight: bold;
}
.tree-node.in-path {
background: color-mix(in srgb, var(--accent) 10%, transparent);
}
.tree-node:not(.in-path) {
opacity: 0.5;
}
.tree-node:not(.in-path):hover {
opacity: 1;
}
.tree-prefix {
color: var(--muted);
flex-shrink: 0;
font-family: monospace;
white-space: pre;
}
.tree-marker {
color: var(--accent);
flex-shrink: 0;
}
.tree-content {
color: var(--text);
}
.tree-role-user {
color: var(--accent);
}
.tree-role-assistant {
color: var(--success);
}
.tree-role-tool {
color: var(--muted);
}
.tree-muted {
color: var(--muted);
}
.tree-error {
color: var(--error);
}
.tree-compaction {
color: var(--borderAccent);
}
.tree-branch-summary {
color: var(--warning);
}
.tree-custom-message {
color: var(--customMessageLabel);
}
.tree-status {
padding: 4px 12px;
font-size: 10px;
color: var(--muted);
flex-shrink: 0;
}
/* Main content */
#content {
flex: 1;
overflow-y: auto;
padding: var(--line-height) calc(var(--line-height) * 2);
display: flex;
flex-direction: column;
align-items: center;
}
#content > * {
width: 100%;
max-width: 800px;
}
/* Help bar */
.help-bar {
font-size: 11px;
color: var(--warning);
margin-bottom: var(--line-height);
display: flex;
align-items: center;
gap: 12px;
}
.download-json-btn {
font-size: 10px;
padding: 2px 8px;
background: var(--container-bg);
border: 1px solid var(--border);
border-radius: 3px;
color: var(--text);
cursor: pointer;
font-family: inherit;
}
.download-json-btn:hover {
background: var(--hover);
border-color: var(--borderAccent);
}
/* Header */
.header {
background: var(--container-bg);
border-radius: 4px;
padding: var(--line-height);
margin-bottom: var(--line-height);
}
.header h1 {
font-size: 12px;
font-weight: bold;
color: var(--borderAccent);
margin-bottom: var(--line-height);
}
.header-info {
display: flex;
flex-direction: column;
gap: 0;
font-size: 11px;
}
.info-item {
color: var(--dim);
display: flex;
align-items: baseline;
}
.info-label {
font-weight: 600;
margin-right: 8px;
min-width: 100px;
}
.info-value {
color: var(--text);
flex: 1;
}
/* Messages */
#messages {
display: flex;
flex-direction: column;
gap: var(--line-height);
}
.message-timestamp {
font-size: 10px;
color: var(--dim);
opacity: 0.8;
}
.user-message {
background: var(--userMessageBg);
color: var(--userMessageText);
padding: var(--line-height);
border-radius: 4px;
position: relative;
}
.assistant-message {
padding: 0;
position: relative;
}
/* Copy link button - appears on hover */
.copy-link-btn {
position: absolute;
top: 8px;
right: 8px;
width: 28px;
height: 28px;
padding: 6px;
background: var(--container-bg);
border: 1px solid var(--dim);
border-radius: 4px;
color: var(--muted);
cursor: pointer;
opacity: 0;
transition: opacity 0.15s, background 0.15s, color 0.15s;
display: flex;
align-items: center;
justify-content: center;
z-index: 10;
}
.user-message:hover .copy-link-btn,
.assistant-message:hover .copy-link-btn {
opacity: 1;
}
.copy-link-btn:hover {
background: var(--accent);
color: var(--body-bg);
border-color: var(--accent);
}
.copy-link-btn.copied {
background: var(--success, #22c55e);
color: white;
border-color: var(--success, #22c55e);
}
/* Highlight effect for deep-linked messages */
.user-message.highlight,
.assistant-message.highlight {
animation: highlight-pulse 2s ease-out;
}
@keyframes highlight-pulse {
0% {
box-shadow: 0 0 0 3px var(--accent);
}
100% {
box-shadow: 0 0 0 0 transparent;
}
}
.assistant-message > .message-timestamp {
padding-left: var(--line-height);
}
.assistant-text {
padding: var(--line-height);
padding-bottom: 0;
}
.message-timestamp + .assistant-text,
.message-timestamp + .thinking-block {
padding-top: 0;
}
.thinking-block + .assistant-text {
padding-top: 0;
}
.thinking-text {
padding: var(--line-height);
color: var(--thinkingText);
font-style: italic;
white-space: pre-wrap;
}
.message-timestamp + .thinking-block .thinking-text,
.message-timestamp + .thinking-block .thinking-collapsed {
padding-top: 0;
}
.thinking-collapsed {
display: none;
padding: var(--line-height);
color: var(--thinkingText);
font-style: italic;
}
/* Tool execution */
.tool-execution {
padding: var(--line-height);
border-radius: 4px;
}
.tool-execution + .tool-execution {
margin-top: var(--line-height);
}
.assistant-text + .tool-execution {
margin-top: var(--line-height);
}
.tool-execution.pending { background: var(--toolPendingBg); }
.tool-execution.success { background: var(--toolSuccessBg); }
.tool-execution.error { background: var(--toolErrorBg); }
.tool-header, .tool-name {
font-weight: bold;
}
.tool-path {
color: var(--accent);
word-break: break-all;
}
.line-numbers {
color: var(--warning);
}
.line-count {
color: var(--dim);
}
.tool-command {
font-weight: bold;
white-space: pre-wrap;
word-wrap: break-word;
overflow-wrap: break-word;
word-break: break-word;
}
.tool-output {
margin-top: var(--line-height);
color: var(--toolOutput);
word-wrap: break-word;
overflow-wrap: break-word;
word-break: break-word;
font-family: inherit;
overflow-x: auto;
}
.tool-output > div,
.output-preview,
.output-full {
margin: 0;
padding: 0;
line-height: var(--line-height);
}
.tool-output pre {
margin: 0;
padding: 0;
font-family: inherit;
color: inherit;
white-space: pre-wrap;
word-wrap: break-word;
overflow-wrap: break-word;
}
.tool-output code {
padding: 0;
background: none;
color: var(--text);
}
.tool-output.expandable {
cursor: pointer;
}
.tool-output.expandable:hover {
opacity: 0.9;
}
.tool-output.expandable .output-full {
display: none;
}
.tool-output.expandable.expanded .output-preview {
display: none;
}
.tool-output.expandable.expanded .output-full {
display: block;
}
.ansi-line {
white-space: pre-wrap;
}
.tool-images {
}
.tool-image {
max-width: 100%;
max-height: 500px;
border-radius: 4px;
margin: var(--line-height) 0;
}
.expand-hint {
color: var(--toolOutput);
}
/* Diff */
.tool-diff {
font-size: 11px;
overflow-x: auto;
white-space: pre;
}
.diff-added { color: var(--toolDiffAdded); }
.diff-removed { color: var(--toolDiffRemoved); }
.diff-context { color: var(--toolDiffContext); }
/* Model change */
.model-change {
padding: 0 var(--line-height);
color: var(--dim);
font-size: 11px;
}
.model-name {
color: var(--borderAccent);
font-weight: bold;
}
/* Compaction / Branch Summary - matches customMessage colors from TUI */
.compaction {
background: var(--customMessageBg);
border-radius: 4px;
padding: var(--line-height);
cursor: pointer;
}
.compaction-label {
color: var(--customMessageLabel);
font-weight: bold;
}
.compaction-collapsed {
color: var(--customMessageText);
}
.compaction-content {
display: none;
color: var(--customMessageText);
white-space: pre-wrap;
margin-top: var(--line-height);
}
.compaction.expanded .compaction-collapsed {
display: none;
}
.compaction.expanded .compaction-content {
display: block;
}
/* System prompt */
.system-prompt {
background: var(--customMessageBg);
padding: var(--line-height);
border-radius: 4px;
margin-bottom: var(--line-height);
}
.system-prompt.expandable {
cursor: pointer;
}
.system-prompt-header {
font-weight: bold;
color: var(--customMessageLabel);
}
.system-prompt-preview {
color: var(--customMessageText);
white-space: pre-wrap;
word-wrap: break-word;
font-size: 11px;
margin-top: var(--line-height);
}
.system-prompt-expand-hint {
color: var(--muted);
font-style: italic;
margin-top: 4px;
}
.system-prompt-full {
display: none;
color: var(--customMessageText);
white-space: pre-wrap;
word-wrap: break-word;
font-size: 11px;
margin-top: var(--line-height);
}
.system-prompt.expanded .system-prompt-preview,
.system-prompt.expanded .system-prompt-expand-hint {
display: none;
}
.system-prompt.expanded .system-prompt-full {
display: block;
}
.system-prompt.provider-prompt {
border-left: 3px solid var(--warning);
}
.system-prompt-note {
font-size: 10px;
font-style: italic;
color: var(--muted);
margin-top: 4px;
}
/* Tools list */
.tools-list {
background: var(--customMessageBg);
padding: var(--line-height);
border-radius: 4px;
margin-bottom: var(--line-height);
}
.tools-header {
font-weight: bold;
color: var(--customMessageLabel);
margin-bottom: var(--line-height);
}
.tool-item {
font-size: 11px;
}
.tool-item-name {
font-weight: bold;
color: var(--text);
}
.tool-item-desc {
color: var(--dim);
}
.tool-params-hint {
color: var(--muted);
font-style: italic;
}
.tool-item:has(.tool-params-hint) {
cursor: pointer;
}
.tool-params-hint::after {
content: '[click to show parameters]';
}
.tool-item.params-expanded .tool-params-hint::after {
content: '[hide parameters]';
}
.tool-params-content {
display: none;
margin-top: 4px;
margin-left: 12px;
padding-left: 8px;
border-left: 1px solid var(--dim);
}
.tool-item.params-expanded .tool-params-content {
display: block;
}
.tool-param {
margin-bottom: 4px;
font-size: 11px;
}
.tool-param-name {
font-weight: bold;
color: var(--text);
}
.tool-param-type {
color: var(--dim);
font-style: italic;
}
.tool-param-required {
color: var(--warning, #e8a838);
font-size: 10px;
}
.tool-param-optional {
color: var(--dim);
font-size: 10px;
}
.tool-param-desc {
color: var(--dim);
margin-left: 8px;
}
/* Hook/custom messages */
.hook-message {
background: var(--customMessageBg);
color: var(--customMessageText);
padding: var(--line-height);
border-radius: 4px;
}
.hook-type {
color: var(--customMessageLabel);
font-weight: bold;
}
/* Branch summary */
.branch-summary {
background: var(--customMessageBg);
padding: var(--line-height);
border-radius: 4px;
}
.branch-summary-header {
font-weight: bold;
color: var(--borderAccent);
}
/* Error */
.error-text {
color: var(--error);
padding: 0 var(--line-height);
}
.tool-error {
color: var(--error);
}
/* Images */
.message-images {
margin-bottom: 12px;
}
.message-image {
max-width: 100%;
max-height: 400px;
border-radius: 4px;
margin: var(--line-height) 0;
}
/* Markdown content */
.markdown-content h1,
.markdown-content h2,
.markdown-content h3,
.markdown-content h4,
.markdown-content h5,
.markdown-content h6 {
color: var(--mdHeading);
margin: var(--line-height) 0 0 0;
font-weight: bold;
}
.markdown-content h1 { font-size: 1em; }
.markdown-content h2 { font-size: 1em; }
.markdown-content h3 { font-size: 1em; }
.markdown-content h4 { font-size: 1em; }
.markdown-content h5 { font-size: 1em; }
.markdown-content h6 { font-size: 1em; }
.markdown-content p { margin: 0; }
.markdown-content p + p { margin-top: var(--line-height); }
.markdown-content a {
color: var(--mdLink);
text-decoration: underline;
}
.markdown-content code {
background: rgba(128, 128, 128, 0.2);
color: var(--mdCode);
padding: 0 4px;
border-radius: 3px;
font-family: inherit;
}
.markdown-content pre {
background: transparent;
margin: var(--line-height) 0;
overflow-x: auto;
}
.markdown-content pre code {
display: block;
background: none;
color: var(--text);
}
.markdown-content blockquote {
border-left: 3px solid var(--mdQuoteBorder);
padding-left: var(--line-height);
margin: var(--line-height) 0;
color: var(--mdQuote);
font-style: italic;
}
.markdown-content ul,
.markdown-content ol {
margin: var(--line-height) 0;
padding-left: calc(var(--line-height) * 2);
}
.markdown-content li { margin: 0; }
.markdown-content li::marker { color: var(--mdListBullet); }
.markdown-content hr {
border: none;
border-top: 1px solid var(--mdHr);
margin: var(--line-height) 0;
}
.markdown-content table {
border-collapse: collapse;
margin: 0.5em 0;
width: 100%;
}
.markdown-content th,
.markdown-content td {
border: 1px solid var(--mdCodeBlockBorder);
padding: 6px 10px;
text-align: left;
}
.markdown-content th {
background: rgba(128, 128, 128, 0.1);
font-weight: bold;
}
.markdown-content img {
max-width: 100%;
border-radius: 4px;
}
/* Syntax highlighting */
.hljs { background: transparent; color: var(--text); }
.hljs-comment, .hljs-quote { color: var(--syntaxComment); }
.hljs-keyword, .hljs-selector-tag { color: var(--syntaxKeyword); }
.hljs-number, .hljs-literal { color: var(--syntaxNumber); }
.hljs-string, .hljs-doctag { color: var(--syntaxString); }
/* Function names: hljs v11 uses .hljs-title.function_ compound class */
.hljs-function, .hljs-title, .hljs-title.function_, .hljs-section, .hljs-name { color: var(--syntaxFunction); }
/* Types: hljs v11 uses .hljs-title.class_ for class names */
.hljs-type, .hljs-class, .hljs-title.class_, .hljs-built_in { color: var(--syntaxType); }
.hljs-attr, .hljs-variable, .hljs-variable.language_, .hljs-params, .hljs-property { color: var(--syntaxVariable); }
.hljs-meta, .hljs-meta .hljs-keyword, .hljs-meta .hljs-string { color: var(--syntaxKeyword); }
.hljs-operator { color: var(--syntaxOperator); }
.hljs-punctuation { color: var(--syntaxPunctuation); }
.hljs-subst { color: var(--text); }
/* Footer */
.footer {
margin-top: 48px;
padding: 20px;
text-align: center;
color: var(--dim);
font-size: 10px;
}
/* Mobile */
#hamburger {
display: none;
position: fixed;
top: 10px;
left: 10px;
z-index: 100;
padding: 3px 8px;
font-size: 12px;
font-family: inherit;
background: transparent;
color: var(--muted);
border: 1px solid var(--dim);
border-radius: 3px;
cursor: pointer;
}
#hamburger:hover {
color: var(--text);
border-color: var(--text);
}
#sidebar-overlay {
display: none;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 98;
}
@media (max-width: 900px) {
#sidebar {
position: fixed;
left: -400px;
width: 400px;
top: 0;
bottom: 0;
height: 100vh;
z-index: 99;
transition: left 0.3s;
}
#sidebar.open {
left: 0;
}
#sidebar-overlay.open {
display: block;
}
#hamburger {
display: block;
}
.sidebar-close {
display: block;
}
#content {
padding: var(--line-height) 16px;
}
#content > * {
max-width: 100%;
}
}
@media (max-width: 500px) {
#sidebar {
width: 100vw;
left: -100vw;
}
}
@media print {
#sidebar, #sidebar-toggle { display: none !important; }
body { background: white; color: black; }
#content { max-width: none; }
}

View file

@ -0,0 +1,54 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Session Export</title>
<style>
{{CSS}}
</style>
</head>
<body>
<button id="hamburger" title="Open sidebar"><svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor" stroke="none"><circle cx="6" cy="6" r="2.5"/><circle cx="6" cy="18" r="2.5"/><circle cx="18" cy="12" r="2.5"/><rect x="5" y="6" width="2" height="12"/><path d="M6 12h10c1 0 2 0 2-2V8"/></svg></button>
<div id="sidebar-overlay"></div>
<div id="app">
<aside id="sidebar">
<div class="sidebar-header">
<div class="sidebar-controls">
<input type="text" class="sidebar-search" id="tree-search" placeholder="Search...">
</div>
<div class="sidebar-filters">
<button class="filter-btn active" data-filter="default" title="Hide settings entries">Default</button>
<button class="filter-btn" data-filter="no-tools" title="Default minus tool results">No-tools</button>
<button class="filter-btn" data-filter="user-only" title="Only user messages">User</button>
<button class="filter-btn" data-filter="labeled-only" title="Only labeled entries">Labeled</button>
<button class="filter-btn" data-filter="all" title="Show everything">All</button>
<button class="sidebar-close" id="sidebar-close" title="Close"></button>
</div>
</div>
<div class="tree-container" id="tree-container"></div>
<div class="tree-status" id="tree-status"></div>
</aside>
<main id="content">
<div id="header-container"></div>
<div id="messages"></div>
</main>
<div id="image-modal" class="image-modal">
<img id="modal-image" src="" alt="">
</div>
</div>
<script id="session-data" type="application/json">{{SESSION_DATA}}</script>
<!-- Vendored libraries -->
<script>{{MARKED_JS}}</script>
<!-- highlight.js -->
<script>{{HIGHLIGHT_JS}}</script>
<!-- Main application code -->
<script>
{{JS}}
</script>
</body>
</html>

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,42 @@
/**
* Tool HTML renderer for custom tools in HTML export.
*
* Renders custom tool calls and results to HTML by invoking their TUI renderers
* and converting the ANSI output to HTML.
*/
import type { Theme } from "../../modes/interactive/theme/theme.js";
import type { ToolDefinition } from "../extensions/types.js";
export interface ToolHtmlRendererDeps {
/** Function to look up tool definition by name */
getToolDefinition: (name: string) => ToolDefinition | undefined;
/** Theme for styling */
theme: Theme;
/** Terminal width for rendering (default: 100) */
width?: number;
}
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. */
renderResult(
toolName: string,
result: Array<{
type: string;
text?: string;
data?: string;
mimeType?: string;
}>,
details: unknown,
isError: boolean,
): string | undefined;
}
/**
* Create a tool HTML renderer.
*
* The renderer looks up tool definitions and invokes their renderCall/renderResult
* methods, converting the resulting TUI Component output (ANSI) to HTML.
*/
export declare function createToolHtmlRenderer(
deps: ToolHtmlRendererDeps,
): ToolHtmlRenderer;
//# sourceMappingURL=tool-renderer.d.ts.map

View file

@ -0,0 +1 @@
{"version":3,"file":"tool-renderer.d.ts","sourceRoot":"","sources":["../../../src/core/export-html/tool-renderer.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAGH,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,wCAAwC,CAAC;AACpE,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAG7D,MAAM,WAAW,oBAAoB;IACnC,kDAAkD;IAClD,iBAAiB,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,cAAc,GAAG,SAAS,CAAC;IAChE,wBAAwB;IACxB,KAAK,EAAE,KAAK,CAAC;IACb,kDAAkD;IAClD,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAC/B,oFAAoF;IACpF,UAAU,CAAC,QAAQ,EAAE,MAAM,EAAE,IAAI,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS,CAAC;IAChE,sFAAsF;IACtF,YAAY,CACV,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,KAAK,CAAC;QACZ,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC,EACF,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,OAAO,GACf,MAAM,GAAG,SAAS,CAAC;CACvB;AAED;;;;;GAKG;AACH,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,oBAAoB,GACzB,gBAAgB,CAiElB","sourcesContent":["/**\n * Tool HTML renderer for custom tools in HTML export.\n *\n * Renders custom tool calls and results to HTML by invoking their TUI renderers\n * and converting the ANSI output to HTML.\n */\n\nimport type { ImageContent, TextContent } from \"@mariozechner/companion-ai\";\nimport type { Theme } from \"../../modes/interactive/theme/theme.js\";\nimport type { ToolDefinition } from \"../extensions/types.js\";\nimport { ansiLinesToHtml } from \"./ansi-to-html.js\";\n\nexport interface ToolHtmlRendererDeps {\n /** Function to look up tool definition by name */\n getToolDefinition: (name: string) => ToolDefinition | undefined;\n /** Theme for styling */\n theme: Theme;\n /** Terminal width for rendering (default: 100) */\n width?: number;\n}\n\nexport interface ToolHtmlRenderer {\n /** Render a tool call to HTML. Returns undefined if tool has no custom renderer. */\n renderCall(toolName: string, args: unknown): string | undefined;\n /** Render a tool result to HTML. Returns undefined if tool has no custom renderer. */\n renderResult(\n toolName: string,\n result: Array<{\n type: string;\n text?: string;\n data?: string;\n mimeType?: string;\n }>,\n details: unknown,\n isError: boolean,\n ): string | undefined;\n}\n\n/**\n * Create a tool HTML renderer.\n *\n * The renderer looks up tool definitions and invokes their renderCall/renderResult\n * methods, converting the resulting TUI Component output (ANSI) to HTML.\n */\nexport function createToolHtmlRenderer(\n deps: ToolHtmlRendererDeps,\n): ToolHtmlRenderer {\n const { getToolDefinition, theme, width = 100 } = deps;\n\n return {\n renderCall(toolName: string, args: unknown): string | undefined {\n try {\n const toolDef = getToolDefinition(toolName);\n if (!toolDef?.renderCall) {\n return undefined;\n }\n\n const component = toolDef.renderCall(args, theme);\n if (!component) {\n return undefined;\n }\n const lines = component.render(width);\n return ansiLinesToHtml(lines);\n } catch {\n // On error, return undefined to trigger JSON fallback\n return undefined;\n }\n },\n\n renderResult(\n toolName: string,\n result: Array<{\n type: string;\n text?: string;\n data?: string;\n mimeType?: string;\n }>,\n details: unknown,\n isError: boolean,\n ): string | undefined {\n try {\n const toolDef = getToolDefinition(toolName);\n if (!toolDef?.renderResult) {\n return undefined;\n }\n\n // Build AgentToolResult from content array\n // Cast content since session storage uses generic object types\n const agentToolResult = {\n content: result as (TextContent | ImageContent)[],\n details,\n isError,\n };\n\n // Always render expanded, client-side will apply truncation\n const component = toolDef.renderResult(\n agentToolResult,\n { expanded: true, isPartial: false },\n theme,\n );\n if (!component) {\n return undefined;\n }\n const lines = component.render(width);\n return ansiLinesToHtml(lines);\n } catch {\n // On error, return undefined to trigger JSON fallback\n return undefined;\n }\n },\n };\n}\n"]}

View file

@ -0,0 +1,65 @@
/**
* Tool HTML renderer for custom tools in HTML export.
*
* Renders custom tool calls and results to HTML by invoking their TUI renderers
* and converting the ANSI output to HTML.
*/
import { ansiLinesToHtml } from "./ansi-to-html.js";
/**
* Create a tool HTML renderer.
*
* The renderer looks up tool definitions and invokes their renderCall/renderResult
* methods, converting the resulting TUI Component output (ANSI) to HTML.
*/
export function createToolHtmlRenderer(deps) {
const { getToolDefinition, theme, width = 100 } = deps;
return {
renderCall(toolName, args) {
try {
const toolDef = getToolDefinition(toolName);
if (!toolDef?.renderCall) {
return undefined;
}
const component = toolDef.renderCall(args, theme);
if (!component) {
return undefined;
}
const lines = component.render(width);
return ansiLinesToHtml(lines);
} catch {
// On error, return undefined to trigger JSON fallback
return undefined;
}
},
renderResult(toolName, result, details, isError) {
try {
const toolDef = getToolDefinition(toolName);
if (!toolDef?.renderResult) {
return undefined;
}
// Build AgentToolResult from content array
// Cast content since session storage uses generic object types
const agentToolResult = {
content: result,
details,
isError,
};
// Always render expanded, client-side will apply truncation
const component = toolDef.renderResult(
agentToolResult,
{ expanded: true, isPartial: false },
theme,
);
if (!component) {
return undefined;
}
const lines = component.render(width);
return ansiLinesToHtml(lines);
} catch {
// On error, return undefined to trigger JSON fallback
return undefined;
}
},
};
}
//# sourceMappingURL=tool-renderer.js.map

View file

@ -0,0 +1 @@
{"version":3,"file":"tool-renderer.js","sourceRoot":"","sources":["../../../src/core/export-html/tool-renderer.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAKH,OAAO,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAC;AA4BpD;;;;;GAKG;AACH,MAAM,UAAU,sBAAsB,CACpC,IAA0B,EACR;IAClB,MAAM,EAAE,iBAAiB,EAAE,KAAK,EAAE,KAAK,GAAG,GAAG,EAAE,GAAG,IAAI,CAAC;IAEvD,OAAO;QACL,UAAU,CAAC,QAAgB,EAAE,IAAa,EAAsB;YAC9D,IAAI,CAAC;gBACH,MAAM,OAAO,GAAG,iBAAiB,CAAC,QAAQ,CAAC,CAAC;gBAC5C,IAAI,CAAC,OAAO,EAAE,UAAU,EAAE,CAAC;oBACzB,OAAO,SAAS,CAAC;gBACnB,CAAC;gBAED,MAAM,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;gBAClD,IAAI,CAAC,SAAS,EAAE,CAAC;oBACf,OAAO,SAAS,CAAC;gBACnB,CAAC;gBACD,MAAM,KAAK,GAAG,SAAS,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;gBACtC,OAAO,eAAe,CAAC,KAAK,CAAC,CAAC;YAChC,CAAC;YAAC,MAAM,CAAC;gBACP,sDAAsD;gBACtD,OAAO,SAAS,CAAC;YACnB,CAAC;QAAA,CACF;QAED,YAAY,CACV,QAAgB,EAChB,MAKE,EACF,OAAgB,EAChB,OAAgB,EACI;YACpB,IAAI,CAAC;gBACH,MAAM,OAAO,GAAG,iBAAiB,CAAC,QAAQ,CAAC,CAAC;gBAC5C,IAAI,CAAC,OAAO,EAAE,YAAY,EAAE,CAAC;oBAC3B,OAAO,SAAS,CAAC;gBACnB,CAAC;gBAED,2CAA2C;gBAC3C,+DAA+D;gBAC/D,MAAM,eAAe,GAAG;oBACtB,OAAO,EAAE,MAAwC;oBACjD,OAAO;oBACP,OAAO;iBACR,CAAC;gBAEF,4DAA4D;gBAC5D,MAAM,SAAS,GAAG,OAAO,CAAC,YAAY,CACpC,eAAe,EACf,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,EACpC,KAAK,CACN,CAAC;gBACF,IAAI,CAAC,SAAS,EAAE,CAAC;oBACf,OAAO,SAAS,CAAC;gBACnB,CAAC;gBACD,MAAM,KAAK,GAAG,SAAS,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;gBACtC,OAAO,eAAe,CAAC,KAAK,CAAC,CAAC;YAChC,CAAC;YAAC,MAAM,CAAC;gBACP,sDAAsD;gBACtD,OAAO,SAAS,CAAC;YACnB,CAAC;QAAA,CACF;KACF,CAAC;AAAA,CACH","sourcesContent":["/**\n * Tool HTML renderer for custom tools in HTML export.\n *\n * Renders custom tool calls and results to HTML by invoking their TUI renderers\n * and converting the ANSI output to HTML.\n */\n\nimport type { ImageContent, TextContent } from \"@mariozechner/companion-ai\";\nimport type { Theme } from \"../../modes/interactive/theme/theme.js\";\nimport type { ToolDefinition } from \"../extensions/types.js\";\nimport { ansiLinesToHtml } from \"./ansi-to-html.js\";\n\nexport interface ToolHtmlRendererDeps {\n /** Function to look up tool definition by name */\n getToolDefinition: (name: string) => ToolDefinition | undefined;\n /** Theme for styling */\n theme: Theme;\n /** Terminal width for rendering (default: 100) */\n width?: number;\n}\n\nexport interface ToolHtmlRenderer {\n /** Render a tool call to HTML. Returns undefined if tool has no custom renderer. */\n renderCall(toolName: string, args: unknown): string | undefined;\n /** Render a tool result to HTML. Returns undefined if tool has no custom renderer. */\n renderResult(\n toolName: string,\n result: Array<{\n type: string;\n text?: string;\n data?: string;\n mimeType?: string;\n }>,\n details: unknown,\n isError: boolean,\n ): string | undefined;\n}\n\n/**\n * Create a tool HTML renderer.\n *\n * The renderer looks up tool definitions and invokes their renderCall/renderResult\n * methods, converting the resulting TUI Component output (ANSI) to HTML.\n */\nexport function createToolHtmlRenderer(\n deps: ToolHtmlRendererDeps,\n): ToolHtmlRenderer {\n const { getToolDefinition, theme, width = 100 } = deps;\n\n return {\n renderCall(toolName: string, args: unknown): string | undefined {\n try {\n const toolDef = getToolDefinition(toolName);\n if (!toolDef?.renderCall) {\n return undefined;\n }\n\n const component = toolDef.renderCall(args, theme);\n if (!component) {\n return undefined;\n }\n const lines = component.render(width);\n return ansiLinesToHtml(lines);\n } catch {\n // On error, return undefined to trigger JSON fallback\n return undefined;\n }\n },\n\n renderResult(\n toolName: string,\n result: Array<{\n type: string;\n text?: string;\n data?: string;\n mimeType?: string;\n }>,\n details: unknown,\n isError: boolean,\n ): string | undefined {\n try {\n const toolDef = getToolDefinition(toolName);\n if (!toolDef?.renderResult) {\n return undefined;\n }\n\n // Build AgentToolResult from content array\n // Cast content since session storage uses generic object types\n const agentToolResult = {\n content: result as (TextContent | ImageContent)[],\n details,\n isError,\n };\n\n // Always render expanded, client-side will apply truncation\n const component = toolDef.renderResult(\n agentToolResult,\n { expanded: true, isPartial: false },\n theme,\n );\n if (!component) {\n return undefined;\n }\n const lines = component.render(width);\n return ansiLinesToHtml(lines);\n } catch {\n // On error, return undefined to trigger JSON fallback\n return undefined;\n }\n },\n };\n}\n"]}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff