diff --git a/src/server/services/html-exporter.ts b/src/server/services/html-exporter.ts
index 5246a38..89bc85b 100644
--- a/src/server/services/html-exporter.ts
+++ b/src/server/services/html-exporter.ts
@@ -4,6 +4,7 @@ import { markedHighlight } from "marked-highlight";
import type { ExportRequest, ParsedMessage } from "../../shared/types.js";
import { CATEGORY_LABELS } from "../../shared/types.js";
import { redactMessage } from "../../shared/sensitive-redactor.js";
+import { escapeHtml } from "../../shared/escape-html.js";
marked.use(
markedHighlight({
@@ -11,11 +12,31 @@ marked.use(
if (lang && hljs.getLanguage(lang)) {
return hljs.highlight(code, { language: lang }).value;
}
- return hljs.highlightAuto(code).value;
+ // Plain-text fallback: highlightAuto tries every grammar (~6.7ms/block)
+ // vs explicit highlight (~0.04ms). With thousands of unlabeled blocks
+ // this dominates export time (50+ seconds). Escaping is sufficient.
+ return escapeHtml(code);
},
})
);
+// Categories whose content is structured data (JSON, logs, snapshots) — not markdown.
+// Rendered as preformatted text to avoid the cost of markdown parsing on large blobs.
+const PREFORMATTED_CATEGORIES = new Set(["hook_progress", "tool_result", "file_snapshot"]);
+
+// Category dot/border colors matching the client-side design
+const CATEGORY_STYLES: Record = {
+ user_message: { dot: "#58a6ff", border: "#1f3a5f", text: "#58a6ff" },
+ assistant_text: { dot: "#3fb950", border: "#1a4d2e", text: "#3fb950" },
+ thinking: { dot: "#bc8cff", border: "#3b2d6b", text: "#bc8cff" },
+ tool_call: { dot: "#d29922", border: "#4d3a15", text: "#d29922" },
+ tool_result: { dot: "#8b8cf8", border: "#2d2d60", text: "#8b8cf8" },
+ system_message: { dot: "#8b949e", border: "#30363d", text: "#8b949e" },
+ hook_progress: { dot: "#484f58", border: "#21262d", text: "#484f58" },
+ file_snapshot: { dot: "#f778ba", border: "#5c2242", text: "#f778ba" },
+ summary: { dot: "#2dd4bf", border: "#1a4d45", text: "#2dd4bf" },
+};
+
export async function generateExportHtml(
req: ExportRequest
): Promise {
@@ -40,9 +61,7 @@ export async function generateExportHtml(
continue;
}
if (lastWasRedacted) {
- messageHtmlParts.push(
- '··· content redacted ···
'
- );
+ messageHtmlParts.push(renderRedactedDivider());
lastWasRedacted = false;
}
const msgToRender = autoRedactEnabled ? redactMessage(msg) : msg;
@@ -71,9 +90,18 @@ ${hljsCss}
@@ -84,26 +112,55 @@ ${hljsCss}