Architecture chapter 09

html/exporter

The trace exporter renders one persisted lash session — or a tree of sessions joined by continue_as handoffs and spawn_agent subagents — into a single self-contained HTML document. The visual language matches the rest of the docs (warm-iron / fast-amber). The forensic affordance is two-edge-aware: handoffs are linear seams in the same view; subagents are drill-in cards into their own full view.

The two-edge model

Both continue_as and spawn_agent create new sessions on disk with parent_session_id set, but they are semantically and visually different — flattening them under one shape is what made the previous exporter feel wrong. Lash's runtime distinguishes them as SessionRelation::Handoff and SessionRelation::Child; the exporter mirrors that distinction in the rendered surface.

Edge Tool Cardinality Lifecycle Visual treatment
Handoff continue_as 1 → 1 terminal Parent ends; successor takes over with seed and task. Inline divider. The successor's entries continue in the same view, behind a sodium-bracketed seam that names the seam.
Subagent spawn_agent 1 → N blocking Parent waits for the child to submit, then resumes. Drill-in card. Click into the subagent's own full view (same hero, same controls). Breadcrumb tracks the path back.

Inline handoff seam

Continue_as is linear, so the natural reading is one transcript that crosses session boundaries. The seam is a full-width band with sodium hairlines top and bottom, the task in display type, the seed broken into projected vs global entries, and a hairline-divided stat strip — the four pieces of state the reader needs to understand what was carried forward and what was left behind.

↪ continued as Synthesise the audit into a ranked compliance plan.
s1·a47e s4·e51a

Take the auth-file inventory and the migration summary; produce a single ranked list of compliance touchpoints. Each item gets a one-line rationale and an effort estimate. Stop when the list is < 12 items.

seed · 4 entries
problemprojected auth_filesglobal · 4 paths migration_summaryglobal · 1.4k findingsglobal · 12 entries
parent ctx78% seed2.8 kb turns8 reasoncontinue_as

Subagent drill-in card

Spawn_agent is blocking but its result is a separate unit of work. Inlining the child transcript inside the parent's tool entry would force a width-cascade and lose the subagent's full hero / controls / minimap. Instead, the exporter renders the spawn_agent call as a click-through card and gives the subagent its own view. The lichen accent keeps the child visually subordinate to the sodium handoff seam.

▾ subagent · spawn_agent db_migrations · explore

Summarise migrations/ schema changes since v3. Flag any change that touches identity, session, or token columns.

ok turns5 spawned1 · migration_v5 tokens7.8k
s3·d23f 22.7s

Live demo

An end-to-end mockup with three views (rootdb_migrationsmigration_v5) backed by synthetic but realistic content. Click any drill-in card or breadcrumb step to navigate. Browser back works through history.pushState. Resize the iframe to see responsive behavior.

open trace-export-edges.html ↗ · scroll inside the frame to see the inlined handoff successor

Implementation map

The renderer is additive — single-session export still works as it always did. Multi-session detection is automatic.

  1. Tree discovery

    Given a root .db path and a trace JSONL, tree::load_tree_from_paths scans the sessions directory, keeps every store reachable through parent_session_id chains, partitions LlmPromptSnapshots by context.session_id, and classifies each child by inspecting the parent's chronological for the matching tool call: spawn_agentNodeRelation::Subagent, continue_asNodeRelation::Handoff, otherwise fall back to Subagent (handoff is rare and continue_as always emits the successor's session id). No schema change to the on-disk store.

  2. View topology

    A view is a chain of sessions joined by handoffs; the root and every subagent are heads of their own views. html::write_view walks the chain and writes one <section class="view"> per head, with successor entries inlined behind handoff dividers and spawn_agent calls converted into drill-in cards that target the subagent's view.

  3. View switching

    Each view is rendered up-front and toggled by a tiny IIFE that reads window.__lashTraceTree (a serialized list of {id, label, sid, parent}, one entry per view head — root and each subagent — not one per session; id and parent are view IDs, sid is the head session's short id). Click handlers on [data-go] elements (drill-in cards, breadcrumb segments, lineage pills) call history.pushState and swap the active .view. The browser back button does the right thing through popstate.

  4. Visual discipline

    Sodium is reserved for the active edge — handoff seams, the breadcrumb's current step, the user's intent. Lichen marks the subagent affordance — drill rails, drill arrows, child status pills. Filter chips read as quiet chalk by default and gain a sodium strikethrough only when the user has actively filtered something off, so the controls strip never floods the page with accent. Ash structural lines do everything else.

Source map

Single self-contained HTML; tokens, fonts, CSS, and JS all inlined.

lash-export/src/tree.rs

Tree discovery and edge classification. Returns a LoadedSessionTree with one LoadedSessionNode per discovered session; each carries its own chronological projection, partitioned LLM prompts, and the list of subagent edges anchored by parent call_id.

lash-export/src/html.rs

render_tree is the multi-view entry point. write_view, write_view_hero, write_lineage, render_handoff_divider, and render_drill_card own the new visual elements; the existing message/tool/RLM-step renderers are reused unchanged.

lash-export/src/html_assets/style.css

Tokens are appended at the bottom under "multi-view: breadcrumb, lineage, drill, handoff". The base trace stylesheet stays intact for single-session exports; multi-view rules only activate when the page contains .view sections.

lash-export/src/html_assets/script.js

The view-switcher IIFE at the bottom reads window.__lashTraceTree, wires [data-go] click handlers, renders the breadcrumb, and listens for popstate. It exits cleanly if the tree global is absent (single-session export).