Clean Chat Rendering
Before this, read:
- The structured-stream pivot — the structured JSONL that clean rendering reads from
- The bridge: tunnels, /key, resume — the bridge’s transcript endpoint provides the parsed events
The structured stream pivot gave the cockpit structured events instead of raw ANSI bytes. But “structured” doesn’t mean “readable.” A raw stream-json feed has compaction summaries, meta turns, slash-command echoes, <command-name> tags, and isVisibleInTranscriptOnly flags — Claude Code’s own internal plumbing mixed in with the actual conversation. Rendering all of it produces garbage. Rendering only what belongs produces a clean chat interface.
This article is about building the parser that knows the difference.
What’s in a Claude Code JSONL
Section titled “What’s in a Claude Code JSONL”Every Claude Code session writes its turns to ~/.claude/projects/<enc-cwd>/<session_id>.jsonl. Each line is a complete JSON object. The types you care about for a chat renderer:
{"type": "user", "message": {"content": [{"type": "text", "text": "..."}]}}{"type": "assistant", "message": {"content": [{"type": "text", "text": "..."}, {"type": "tool_use", "id": "...", "name": "Read", "input": {...}}]}}{"type": "tool_result", "tool_use_id": "...", "content": [...]}{"type": "result", "total_cost_usd": 0.085, "stop_reason": "end_turn"}The types you need to actively filter:
{"type": "user", "isCompactSummary": true} // compaction artifact{"type": "assistant", "isVisibleInTranscriptOnly": true} // meta-only{"type": "user", "isMeta": true} // system meta{"type": "user", "message": {"content": [{"type": "text", "text": "<command-name>..."}]}}The compaction summary (isCompactSummary: true) is Claude Code’s own mechanism for summarizing a long context window into a shorter representation. It’s useful internally — it’s what lets Claude Code resume a session without replaying the full transcript. It’s not a conversation turn. Rendering it produces a wall of summary text in the middle of the conversation that has nothing to do with what the user asked.
The isVisibleInTranscriptOnly flag marks turns that are part of Claude Code’s internal visible-state machinery — meta-events that exist for Claude Code’s own context management, not for human reading.
The <command-name> pattern appears when the user runs a slash command (like /compact or /status). Claude Code echoes the command as a user turn in the transcript. A chat renderer that doesn’t filter this shows the raw slash-command string as a user message.
The parser
Section titled “The parser”The bridge’s transcript.py module handles the parsing. 11 event types, with explicit drop rules:
SKIP_USER_TURNS = { lambda t: t.get("isCompactSummary"), # compaction lambda t: t.get("isMeta"), # meta turns lambda t: any( isinstance(c, dict) and c.get("type") == "text" and c.get("text", "").startswith("<command-name>") for c in (t.get("message") or {}).get("content", []) ), # slash-command echoes}
SKIP_ASSISTANT_TURNS = { lambda t: t.get("isVisibleInTranscriptOnly"), # meta-only lambda t: t.get("isMeta"), # meta}The result of each parsed turn maps to a rendering type:
user_text→ user message bubble (white, right-aligned)assistant_text→ assistant prose bubble (darker, left-aligned, markdown-rendered)tool_use→ collapsible tool card (name, input parameters, collapsed by default)tool_result→ paired to itstool_usebytool_use_id(success/failure indicator, content collapsed)cost→ cost chip appended to the assistant turn that triggered it
The compaction leak fix
Section titled “The compaction leak fix”On June 8, 2026, the CHANGELOG records: “Fixed compaction-dump leak in cockpit chat: bridge/transcript.py now drops /compact summary turns (isCompactSummary/isVisibleInTranscriptOnly), meta caveats (isMeta), and <command-name> slash-echoes — Claude Code structural flags the frontend PLUMBING_PATTERNS can’t catch.”
This was root-caused from a different angle: the frontend’s existing pattern list (PLUMBING_PATTERNS) was supposed to filter structural noise before rendering, but it was a list of string patterns applied to rendered text. The isCompactSummary flag is on the raw JSON object — by the time the text had been extracted and compared to the pattern list, the compaction summary had already been included as a user message.
The fix moved the filtering to the parser, before the text is extracted. The parser operates on raw JSONL objects; it can check isCompactSummary directly without parsing the content. That’s the right abstraction level.
The regression tests (+4, total 18 passing after the fix) were built specifically to catch this: a test that parses a JSONL fixture containing a /compact summary turn and asserts that it does not appear in the rendered output. If the filter ever regresses, the test catches it before deployment.
The clean⇄raw toggle
Section titled “The clean⇄raw toggle”Not everything in the cockpit renders better as chat bubbles. The interactive TUI is genuinely useful for:
- Navigating the
/modelselection menu - Watching real-time streaming output as it happens (the bubble renderer polls; the xterm renders stream-native)
- Debugging why an agent is stuck (raw output shows the exact state of the TUI, including partial renders)
The toggle keeps the SessionTerminal (xterm.js) component mounted in the DOM even when the clean view is active. This matters for two reasons:
-
No reconnect overhead. Switching from clean to raw doesn’t respawn the terminal — it shows the already-running xterm. Switching back to clean doesn’t kill the terminal — it hides it and shows the parsed view. The PTY session is not affected by which display mode you’re in.
-
The /model menu stays answerable. AskUserQuestion cards in the clean transcript route their answers through the
/modelor/inputendpoints. But the raw interactive TUI’s/modelselection menu is a PTY interaction — it requires the xterm to be alive to receive the keyboard input. If the xterm were killed when clean mode is active, the menu would be unanswerable. Keeping it mounted preserves the full PTY interaction surface.
The 2026-06-01 clean-render CHANGELOG entry (COCKPIT ARC COMPLETE) specifically notes: “SessionTerminal untouched so /model menu never unanswerable.”
Why this matters for building your own
Section titled “Why this matters for building your own”The pattern here generalizes. If you’re building a web surface for any CLI-based agent:
- Don’t render what you receive. Parse it first. Every structured output format has internal plumbing that looks like application output but isn’t.
- Filter at the schema level, not the text level. Flags and types on JSON objects are more reliable than string patterns on extracted text.
- Write regression tests for the specific artifacts you filter. Compaction summaries, slash-command echoes, meta turns — each gets its own test case that fails if the filter regresses.
- Keep both rendering modes available. The structured rendering is more readable; the raw rendering is more complete. The right trade-off depends on what the user is trying to do.
The cockpit went from showing raw terminal garbage in a browser xterm to showing clean conversation bubbles with tool cards. The parser is about 200 lines. The investment was worth it.
Next: The org chart from ground truth — how the org chart went from a hand-curated YAML file that was always stale to a live graph built from filesystem composition.