From 9e5163c296cb72280dc6ab6378482165dc21d651 Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Thu, 1 Jan 2026 17:12:44 +0100 Subject: [PATCH] Fix export-html header stats and UX MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Header h1 font size now 12px (matches body) - Token/cost stats now computed for all entries, not just current branch - Token display matches footer format: ↑input ↓output Rcache Wcache - Cost display uses 3 decimal places like footer - Scroll to target uses requestAnimationFrame for DOM readiness - Initial load and Escape scroll to bottom, tree clicks scroll to target --- .../src/core/export-html/template.html | 73 ++++++++++++------- 1 file changed, 46 insertions(+), 27 deletions(-) diff --git a/packages/coding-agent/src/core/export-html/template.html b/packages/coding-agent/src/core/export-html/template.html index c604582d..2fae6139 100644 --- a/packages/coding-agent/src/core/export-html/template.html +++ b/packages/coding-agent/src/core/export-html/template.html @@ -235,7 +235,7 @@ } .header h1 { - font-size: 14px; + font-size: 12px; font-weight: bold; color: var(--borderAccent); margin-bottom: var(--line-height); @@ -1287,6 +1287,15 @@ return div.innerHTML; } + // Format token counts (matches footer.ts) + function formatTokens(count) { + if (count < 1000) return count.toString(); + if (count < 10000) return (count / 1000).toFixed(1) + 'k'; + if (count < 1000000) return Math.round(count / 1000) + 'k'; + if (count < 10000000) return (count / 1000000).toFixed(1) + 'M'; + return Math.round(count / 1000000) + 'M'; + } + // Configure marked marked.setOptions({ breaks: true, @@ -1666,8 +1675,8 @@ return ''; } - // Compute stats for current path - function computeStats(path) { + // Compute stats for a list of entries + function computeStats(entryList) { let userMessages = 0; let assistantMessages = 0; let toolCalls = 0; @@ -1675,7 +1684,7 @@ const cost = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }; const models = new Set(); - for (const entry of path) { + for (const entry of entryList) { if (entry.type === 'message') { const msg = entry.message; if (msg.role === 'user') userMessages++; @@ -1704,22 +1713,32 @@ return { userMessages, assistantMessages, toolCalls, tokens, cost, models: Array.from(models) }; } + // Compute global stats once for all entries + const globalStats = computeStats(entries); + // Render header function renderHeader(path) { - const stats = computeStats(path); - const totalTokens = stats.tokens.input + stats.tokens.output + stats.tokens.cacheRead + stats.tokens.cacheWrite; - const totalCost = stats.cost.input + stats.cost.output + stats.cost.cacheRead + stats.cost.cacheWrite; + const pathStats = computeStats(path); + const totalCost = globalStats.cost.input + globalStats.cost.output + globalStats.cost.cacheRead + globalStats.cost.cacheWrite; + + // Format tokens like footer: ↑input ↓output Rcache_read Wcache_write (global stats) + 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 tokensDisplay = tokenParts.join(' ') || '0'; let html = `

Session: ${escapeHtml(header?.id || 'unknown')}

Date:${header?.timestamp ? new Date(header.timestamp).toLocaleString() : 'unknown'}
-
Models:${stats.models.join(', ') || 'unknown'}
-
Messages:${stats.userMessages} user, ${stats.assistantMessages} assistant
-
Tool Calls:${stats.toolCalls}
-
Tokens:${totalTokens.toLocaleString()}
-
Cost:$${totalCost.toFixed(4)}
+
Models:${globalStats.models.join(', ') || 'unknown'}
+
Messages:${globalStats.userMessages} user, ${globalStats.assistantMessages} assistant
+
Tool Calls:${globalStats.toolCalls}
+
Tokens:${tokensDisplay}
+
Cost:$${totalCost.toFixed(3)}
`; @@ -1745,7 +1764,8 @@ } // Navigate to entry - function navigateTo(targetId, scrollToTarget = true) { + // scrollMode: 'target' = scroll to target element, 'bottom' = scroll to bottom, 'none' = no scroll + function navigateTo(targetId, scrollMode = 'target') { currentPath = getPath(targetId); renderTree(); @@ -1755,19 +1775,18 @@ headerContainer.innerHTML = renderHeader(currentPath); messagesContainer.innerHTML = currentPath.map(renderEntry).join(''); - // Scroll to target element - if (scrollToTarget) { - const targetEl = document.getElementById(`entry-${targetId}`); - if (targetEl) { - // Use 'end' to ensure we scroll far enough to see the target - targetEl.scrollIntoView({ behavior: 'smooth', block: 'end' }); - // Brief highlight - targetEl.style.outline = '2px solid var(--accent)'; - setTimeout(() => { targetEl.style.outline = ''; }, 1500); + // Use requestAnimationFrame to wait for DOM render before scrolling + requestAnimationFrame(() => { + const content = document.getElementById('content'); + if (scrollMode === 'bottom') { + content.scrollTop = content.scrollHeight; + } else if (scrollMode === 'target') { + const targetEl = document.getElementById(`entry-${targetId}`); + if (targetEl) { + targetEl.scrollIntoView({ block: 'center' }); + } } - } else { - document.getElementById('content').scrollTop = 0; - } + }); } // Initialize @@ -1852,7 +1871,7 @@ if (e.key === 'Escape') { searchInput.value = ''; searchQuery = ''; - navigateTo(leafId); + navigateTo(leafId, 'bottom'); } // Toggle thinking blocks on Ctrl+T if (e.ctrlKey && e.key === 't') { @@ -1867,7 +1886,7 @@ }); // Initial render - navigateTo(leafId); + navigateTo(leafId, 'bottom'); })();