import type { AgentState } from "@mariozechner/pi-agent";
import type { AssistantMessage, Message, ToolResultMessage, UserMessage } from "@mariozechner/pi-ai";
import { readFileSync, writeFileSync } from "fs";
import { homedir } from "os";
import { basename, dirname, join } from "path";
import { fileURLToPath } from "url";
import type { SessionManager } from "./session-manager.js";
// Get version from package.json
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const packageJson = JSON.parse(readFileSync(join(__dirname, "../package.json"), "utf-8"));
const VERSION = packageJson.version;
/**
* TUI Color scheme (matching exact RGB values from TUI components)
*/
const COLORS = {
// Backgrounds
userMessageBg: "rgb(52, 53, 65)", // Dark slate
toolPendingBg: "rgb(40, 40, 50)", // Dark blue-gray
toolSuccessBg: "rgb(40, 50, 40)", // Dark green
toolErrorBg: "rgb(60, 40, 40)", // Dark red
bodyBg: "rgb(24, 24, 30)", // Very dark background
containerBg: "rgb(30, 30, 36)", // Slightly lighter container
// Text colors (matching chalk colors)
text: "rgb(229, 229, 231)", // Light gray (close to white)
textDim: "rgb(161, 161, 170)", // Dimmed gray
cyan: "rgb(103, 232, 249)", // Cyan for paths
green: "rgb(34, 197, 94)", // Green for success
red: "rgb(239, 68, 68)", // Red for errors
yellow: "rgb(234, 179, 8)", // Yellow for warnings
italic: "rgb(161, 161, 170)", // Gray italic for thinking
};
/**
* Escape HTML special characters
*/
function escapeHtml(text: string): string {
return text
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
/**
* Shorten path with tilde notation
*/
function shortenPath(path: string): string {
const home = homedir();
if (path.startsWith(home)) {
return "~" + path.slice(home.length);
}
return path;
}
/**
* Replace tabs with 3 spaces
*/
function replaceTabs(text: string): string {
return text.replace(/\t/g, " ");
}
/**
* Format tool execution matching TUI ToolExecutionComponent
*/
function formatToolExecution(
toolName: string,
args: any,
result?: ToolResultMessage,
): { html: string; bgColor: string } {
let html = "";
const isError = result?.isError || false;
const bgColor = result ? (isError ? COLORS.toolErrorBg : COLORS.toolSuccessBg) : COLORS.toolPendingBg;
// Get text output from result
const getTextOutput = (): string => {
if (!result) return "";
const textBlocks = result.content.filter((c) => c.type === "text");
return textBlocks.map((c: any) => c.text).join("\n");
};
// Format based on tool type (matching TUI logic exactly)
if (toolName === "bash") {
const command = args?.command || "";
html = `
$ ${escapeHtml(command || "...")}
`;
if (result) {
const output = getTextOutput().trim();
if (output) {
const lines = output.split("\n");
const maxLines = 5;
const displayLines = lines.slice(0, maxLines);
const remaining = lines.length - maxLines;
if (remaining > 0) {
// Truncated output - make it expandable
html += '";
} else {
// Short output - show all
html += '";
}
}
}
} else if (toolName === "read") {
const path = shortenPath(args?.file_path || args?.path || "");
html = ``;
if (result) {
const output = getTextOutput();
const lines = output.split("\n");
const maxLines = 10;
const displayLines = lines.slice(0, maxLines);
const remaining = lines.length - maxLines;
if (remaining > 0) {
// Truncated output - make it expandable
html += '";
} else {
// Short output - show all
html += '";
}
}
} else if (toolName === "write") {
const path = shortenPath(args?.file_path || args?.path || "");
const fileContent = args?.content || "";
const lines = fileContent ? fileContent.split("\n") : [];
const totalLines = lines.length;
html = `";
if (fileContent) {
const maxLines = 10;
const displayLines = lines.slice(0, maxLines);
const remaining = lines.length - maxLines;
if (remaining > 0) {
// Truncated output - make it expandable
html += '";
} else {
// Short output - show all
html += '";
}
}
if (result) {
const output = getTextOutput().trim();
if (output) {
html += ``;
}
}
} else if (toolName === "edit") {
const path = shortenPath(args?.file_path || args?.path || "");
html = ``;
// Show diff if available from result.details.diff
if (result?.details?.diff) {
const diffLines = result.details.diff.split("\n");
html += '";
}
if (result) {
const output = getTextOutput().trim();
if (output) {
html += ``;
}
}
} else {
// Generic tool
html = ``;
html += ``;
if (result) {
const output = getTextOutput();
if (output) {
html += ``;
}
}
}
return { html, bgColor };
}
/**
* Format a message as HTML (matching TUI component styling)
*/
function formatMessage(message: Message, toolResultsMap: Map): string {
let html = "";
if (message.role === "user") {
const userMsg = message as UserMessage;
let textContent = "";
if (typeof userMsg.content === "string") {
textContent = userMsg.content;
} else {
const textBlocks = userMsg.content.filter((c) => c.type === "text");
textContent = textBlocks.map((c: any) => c.text).join("");
}
if (textContent.trim()) {
html += `${escapeHtml(textContent).replace(/\n/g, "
")}
`;
}
} else if (message.role === "assistant") {
const assistantMsg = message as AssistantMessage;
// Render text and thinking content
for (const content of assistantMsg.content) {
if (content.type === "text" && content.text.trim()) {
html += `${escapeHtml(content.text.trim()).replace(/\n/g, "
")}
`;
} else if (content.type === "thinking" && content.thinking.trim()) {
html += `${escapeHtml(content.thinking.trim()).replace(/\n/g, "
")}
`;
}
}
// Render tool calls with their results
for (const content of assistantMsg.content) {
if (content.type === "toolCall") {
const toolResult = toolResultsMap.get(content.id);
const { html: toolHtml, bgColor } = formatToolExecution(content.name, content.arguments, toolResult);
html += `${toolHtml}
`;
}
}
// Show error/abort status if no tool calls
const hasToolCalls = assistantMsg.content.some((c) => c.type === "toolCall");
if (!hasToolCalls) {
if (assistantMsg.stopReason === "aborted") {
html += 'Aborted
';
} else if (assistantMsg.stopReason === "error") {
const errorMsg = assistantMsg.errorMessage || "Unknown error";
html += `Error: ${escapeHtml(errorMsg)}
`;
}
}
}
return html;
}
/**
* Export session to a self-contained HTML file matching TUI visual style
*/
export function exportSessionToHtml(sessionManager: SessionManager, state: AgentState, outputPath?: string): string {
const sessionFile = sessionManager.getSessionFile();
const timestamp = new Date().toISOString();
// Use session filename + .html if no output path provided
if (!outputPath) {
const sessionBasename = basename(sessionFile, ".jsonl");
outputPath = `${sessionBasename}.html`;
}
// Read and parse session data
const sessionContent = readFileSync(sessionFile, "utf8");
const lines = sessionContent.trim().split("\n");
let sessionHeader: any = null;
const messages: Message[] = [];
const toolResultsMap = new Map();
for (const line of lines) {
try {
const entry = JSON.parse(line);
if (entry.type === "session") {
sessionHeader = entry;
} else if (entry.type === "message") {
messages.push(entry.message);
// Build map of tool call ID to result
if (entry.message.role === "toolResult") {
toolResultsMap.set(entry.message.toolCallId, entry.message);
}
}
} catch {
// Skip malformed lines
}
}
// Generate messages HTML
let messagesHtml = "";
for (const message of messages) {
if (message.role !== "toolResult") {
// Skip toolResult messages as they're rendered with their tool calls
messagesHtml += formatMessage(message, toolResultsMap);
}
}
// Generate HTML (matching TUI aesthetic)
const html = `
Session Export - ${basename(sessionFile)}
${escapeHtml(sessionHeader?.systemPrompt || state.systemPrompt)}
${messagesHtml}
`;
// Write HTML file
writeFileSync(outputPath, html, "utf8");
return outputPath;
}