${copyBtnHtml}${tsHtml}`;
// Skill invocation (collapsed by default, click to expand)
html += `
[skill] ${escapeHtml(skillBlock.name)}
${escapeHtml(skillBlock.name)} (click to expand)
${safeMarkedParse(skillBlock.content)}
`;
// User message (separate block if present)
if (hasUserContent) {
html += '
';
if (images.length > 0) {
html += '
';
for (const img of images) {
html += `
};base64,${escapeHtml(img.data || '')})
`;
}
html += '
';
}
if (skillBlock.userMessage) {
html += `
${safeMarkedParse(skillBlock.userMessage)}
`;
}
html += '
';
}
html += '
';
return html;
}
// No skill block - normal user message
let html = `${copyBtnHtml}${tsHtml}`;
if (Array.isArray(content)) {
const images = content.filter(c => c.type === 'image');
if (images.length > 0) {
html += '
';
for (const img of images) {
html += `
};base64,${escapeHtml(img.data || '')})
`;
}
html += '
';
}
}
if (text.trim()) {
html += `
${safeMarkedParse(text)}
`;
}
html += '
';
return html;
}
if (msg.role === 'assistant') {
let html = `${copyBtnHtml}${tsHtml}`;
for (const block of msg.content) {
if (block.type === 'text' && block.text.trim()) {
html += `
${safeMarkedParse(block.text)}
`;
} else if (block.type === 'thinking' && block.thinking.trim()) {
html += `
${escapeHtml(block.thinking)}
Thinking ...
`;
}
}
for (const block of msg.content) {
if (block.type === 'toolCall') {
html += renderToolCall(block);
}
}
if (msg.stopReason === 'aborted') {
html += '
Aborted
';
} else if (msg.stopReason === 'error') {
html += `
Error: ${escapeHtml(msg.errorMessage || 'Unknown error')}
`;
}
html += '
';
return html;
}
if (msg.role === 'bashExecution') {
const isError = msg.cancelled || (msg.exitCode !== 0 && msg.exitCode !== null);
let html = `${tsHtml}`;
html += `
$ ${escapeHtml(msg.command)}
`;
if (msg.output) html += formatExpandableOutput(msg.output, 10);
if (msg.cancelled) {
html += '
(cancelled)
';
} else if (msg.exitCode !== 0 && msg.exitCode !== null) {
html += `
(exit ${msg.exitCode})
`;
}
html += '
';
return html;
}
if (msg.role === 'toolResult') return '';
}
if (entry.type === 'model_change') {
return `
[compaction]
Compacted from ${entry.tokensBefore.toLocaleString()} tokens
Compacted from ${entry.tokensBefore.toLocaleString()} tokens\n\n${escapeHtml(entry.summary)}
`;
}
if (entry.type === 'branch_summary') {
return `${tsHtml}
${safeMarkedParse(entry.summary)}
`;
}
if (entry.type === 'custom_message' && entry.display) {
return `${tsHtml}
[${escapeHtml(entry.customType)}]
${safeMarkedParse(typeof entry.content === 'string' ? entry.content : JSON.stringify(entry.content))}
`;
}
return '';
}
// ============================================================
// HEADER / STATS
// ============================================================
function computeStats(entryList) {
let userMessages = 0, assistantMessages = 0, toolResults = 0;
let customMessages = 0, compactions = 0, branchSummaries = 0, toolCalls = 0;
const tokens = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
const cost = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
const models = new Set();
for (const entry of entryList) {
if (entry.type === 'message') {
const msg = entry.message;
if (msg.role === 'user') userMessages++;
if (msg.role === 'assistant') {
assistantMessages++;
if (msg.model) models.add(msg.provider ? `${msg.provider}/${msg.model}` : msg.model);
if (msg.usage) {
tokens.input += msg.usage.input || 0;
tokens.output += msg.usage.output || 0;
tokens.cacheRead += msg.usage.cacheRead || 0;
tokens.cacheWrite += msg.usage.cacheWrite || 0;
if (msg.usage.cost) {
cost.input += msg.usage.cost.input || 0;
cost.output += msg.usage.cost.output || 0;
cost.cacheRead += msg.usage.cost.cacheRead || 0;
cost.cacheWrite += msg.usage.cost.cacheWrite || 0;
}
}
toolCalls += msg.content.filter(c => c.type === 'toolCall').length;
}
if (msg.role === 'toolResult') toolResults++;
} else if (entry.type === 'compaction') {
compactions++;
} else if (entry.type === 'branch_summary') {
branchSummaries++;
} else if (entry.type === 'custom_message') {
customMessages++;
}
}
return { userMessages, assistantMessages, toolResults, customMessages, compactions, branchSummaries, toolCalls, tokens, cost, models: Array.from(models) };
}
const globalStats = computeStats(entries);
function renderHeader() {
const totalCost = globalStats.cost.input + globalStats.cost.output + globalStats.cost.cacheRead + globalStats.cost.cacheWrite;
const tokenParts = [];
if (globalStats.tokens.input) tokenParts.push(`↑${formatTokens(globalStats.tokens.input)}`);
if (globalStats.tokens.output) tokenParts.push(`↓${formatTokens(globalStats.tokens.output)}`);
if (globalStats.tokens.cacheRead) tokenParts.push(`R${formatTokens(globalStats.tokens.cacheRead)}`);
if (globalStats.tokens.cacheWrite) tokenParts.push(`W${formatTokens(globalStats.tokens.cacheWrite)}`);
const msgParts = [];
if (globalStats.userMessages) msgParts.push(`${globalStats.userMessages} user`);
if (globalStats.assistantMessages) msgParts.push(`${globalStats.assistantMessages} assistant`);
if (globalStats.toolResults) msgParts.push(`${globalStats.toolResults} tool results`);
if (globalStats.customMessages) msgParts.push(`${globalStats.customMessages} custom`);
if (globalStats.compactions) msgParts.push(`${globalStats.compactions} compactions`);
if (globalStats.branchSummaries) msgParts.push(`${globalStats.branchSummaries} branch summaries`);
let html = `
`;
// Render system prompt (user's base prompt, applies to all providers)
if (systemPrompt) {
const lines = systemPrompt.split('\n');
const previewLines = 10;
if (lines.length > previewLines) {
const preview = lines.slice(0, previewLines).join('\n');
const remaining = lines.length - previewLines;
html += `