Make session export a lightweight chat log#1544
Conversation
…cit mode
Review pass over the Codex implementation (in place of codex-debate):
- marked passes raw HTML tokens through verbatim by default, so an
assistant/user message containing `<img src=x onerror=…>` or `<script>`
became stored XSS in any browser that opened the shared export. Override
the `html` renderer to escape raw HTML to its literal text — also the
faithful rendering of a transcript. Covered by a new test.
- Drop the `{ mode: "chat" }` default on `transcriptToHtml`: every caller
already passes mode explicitly, so the default only masked a missing
argument — a fail-fast / no-fallbacks violation.
Move TranscriptHtmlMode to kolu-transcript-core (the low layer both packages already depend on); kolu-common derives TranscriptHtmlModeSchema via z.enum over the core values, and transcript-html imports the type instead of re-declaring it. Agreed by the lowy ⇄ hickey lens debate (finding lowy-1, raised by lowy). Not pushed or merged.
exportSessionAsHtml/exportSessionHtml/exportMode now carry TranscriptHtmlMode[]; a single-element array opens in a tab and a multi-element array downloads each over the same loop. The synthetic "both" member and every `=== "both"` discrimination disappear, keeping the render-mode contract clean and separating the orchestration axis. Agreed by the lowy ⇄ hickey lens debate (finding lowy-2, raised by lowy). Not pushed or merged.
openExport and downloadExport no longer duplicate the Blob + createObjectURL + revoke-after-60s lifecycle; that resource management lives once in withBlobUrl, leaving the two functions as one-line delivery strategies (open-tab-with-fallback vs. force-download). Agreed by the lowy ⇄ hickey lens debate (finding lowy-3, raised by lowy). Not pushed or merged.
Add MODE_LABEL: Record<TranscriptHtmlMode, string> beside the mode enum in kolu-transcript-core and read it everywhere a single mode needs a label — the renderer's meta line and document title, and the single-mode export success toast — so "Chat log" / "Full transcript" can no longer drift across sites. The count-based loading/multi toasts stay length conditionals. Agreed by the lowy ⇄ hickey lens debate (finding hickey-4, raised by hickey). Not pushed or merged.
Add `close: () => setOpen(false)` to the Disclosure triplet and call exportSessionDialog.close() from the Cancel button and exportMode, instead of reaching past the abstraction with the magic `onOpenChange(false)` literal. Agreed by the lowy ⇄ hickey lens debate (finding hickey-5, raised by hickey). Not pushed or merged.
…eshold /simplify pass: the nav HTML and its runtime script are one feature with one gate (more than one human message). Compute hasPromptJump once instead of repeating the >= 2 threshold in promptJumpHtml's internal guard and the script-inclusion line.
- Make the export-modes parameter a non-empty tuple ([Mode, ...Mode[]]) end to end, so "at least one mode" is a compile-time guarantee and the unreachable runtime guard drops out. - renderEvents takes humanTotal as a parameter instead of recomputing the O(n) human-message scan. - Inline the single-use fetchHtml wrapper. - Narrow renderDetailEvent to the detail event union; drop the dead user/assistant branches. - firstLine() computed once in the patch summary; remove the unreachable `?? String(value)` in prettyJson (JSON.stringify never returns null here).
⚖️ Lowy ⇄ Hickey lens debate✅ Consensus after 2 round(s) · lowy + hickey · base Independent findings: lowy=4, hickey=6 Applied (8)
Agreed — no change (2)
|
EvidenceRendered from the real Chat log (
|
The export rework drops marked's old pierre/shiki/preact siblings from packages/transcript-html, changing pnpm-lock.yaml and the vendored pnpm store. Update the fetchPnpmDeps FOD hash to match (both platforms compute the same store). Fixes ci::pnpm-hash-fresh and the cascading nix builds.
🧪 CI metrics — leased pool boxThe x86_64-linux lane ran on
Pool status (8 boxes)
Posted by |



Session export now starts from the thing people actually want to read: the conversation. The export action opens a small picker for Chat log, Full transcript, or Both, with Chat log producing a plain back-and-forth HTML file instead of carrying hidden tool payloads or a rendered mini-app.
What changed
The export uses a conversation-ledger design with hard role colours, visible borders, dark code blocks, and fixed ↑/↓ controls for jumping between human prompts. Those controls are the only runtime script, emitted only when there is more than one human prompt.
The RPC now requires an explicit mode, so there is no silent default export shape. The renderer still consumes the existing vendor-neutral
TranscriptEventstream; it just renders it as a small self-contained document instead of a second app.Project fit
chatorfull; old implicit HTML export calls now fail schema validation.Checks run
nix develop -c pnpm --filter kolu-common test:unit -- transcript.test.tsnix develop -c pnpm --filter kolu-server test:unit -- exportTranscriptHtml.test.tsnix develop -c pnpm --filter kolu-transcript-html typechecknix develop -c pnpm --filter kolu-client typechecknix build .#pnpmDepsnix develop -c just fmtnix develop -c just checknix develop -c just atlas::check-synchuman-1, next →human-2, previous →human-1Review gauntlet (resumed
/be)Codex did the implementation, so the codex-debate slot was replaced by an independent Claude review pass up front, then the rest of the gauntlet ran in order:
markedpassed raw HTML tokens through verbatim, so a message containing<img onerror=…>/<script>became stored XSS in any browser that opened the shared export. Overrode thehtmlrenderer to escape it (verified against the installed marked; test added). Also removed thetranscriptToHtmldefaultmode(a fail-fast / no-fallbacks fix). [57245ce]kolu-transcript-core, modeled "both" as amodesarray instead of a widened mode value, factored the blob/URL lifecycle into one helper, oneMODE_LABELtable, named the dialog close intent.hasPromptJumpthreshold.humanMessageCountscan, narrowedrenderDetailEvent, removed dead branches.Evidence: rendered chat-log + full-transcript documents (desktop + phone) posted below. CI: green on both platforms — also fixed a stale
pnpmDepsFOD hash the trimmed dependency set surfaced. [334b06c]Generated by
/beon Codex (modelgpt-5), review gauntlet resumed on Claude (Opus 4.8).