Files
ClaudeDo/docs/superpowers/specs/2026-04-21-stream-formatter-rewrite-design.md

5.2 KiB
Raw Blame History

Stream Formatter Rewrite — Design

Date: 2026-04-21 Scope: src/ClaudeDo.Ui/Helpers/StreamLineFormatter.cs

Problem

StreamLineFormatter converts Claude CLI stream-json lines into human-readable text for the Details pane. The current implementation only recognizes:

  • type=stream_event — dead code (requires --include-partial-messages, which the Worker does not pass)
  • type=result — shown as --- Result --- block
  • type=system with subtype=api_retry

Everything else — notably assistant and user messages that carry the actual conversation and tool activity — falls through to default: return null and is silently dropped. The Details pane is therefore mostly empty during a run, while the raw .log file retains the full JSON.

Goal

Rewrite the formatter so every meaningful message type is rendered as one or more compact text lines suitable for the live log in the Details pane. The public API (FormatLine(string) / FormatFile(string)) and the existing buffer/trim behavior in DetailsIslandViewModel stay the same.

Input format

The Worker invokes the Claude CLI with:

claude -p --output-format stream-json --verbose --dangerously-skip-permissions ...

Each stdout line is one complete SDK message. Top-level shapes relevant to the formatter:

// Session start
{"type":"system","subtype":"init","session_id":"…","model":"claude-…", }

// API retry notification
{"type":"system","subtype":"api_retry", }

// Assistant reply (text + tool calls)
{"type":"assistant","message":{"role":"assistant","content":[
  {"type":"text","text":"…"},
  {"type":"tool_use","id":"toolu_…","name":"Read","input":{"file_path":"…"}}
]}, }

// Tool result fed back to the model
{"type":"user","message":{"role":"user","content":[
  {"tool_use_id":"toolu_…","type":"tool_result","content":"… or [ {type,text} ] …","is_error":false}
]}, "tool_use_result":{…optional rich payload…}, }

// Final result
{"type":"result","result":"…", }

Notes on quirks already observed in captured output:

  • tool_result.content is sometimes a plain string, sometimes an array of {type:"text", text:"…"} blocks. Handle both.
  • The envelope may include tool_use_result.file.numLines / file.filePath for Read-style results.
  • Assistant messages may contain thinking blocks (filtered, not displayed).

Output format

One line per logical event. A trailing \n ends each line so the DetailsIslandViewModel buffer splits cleanly.

Input Output
system / init [session <id8> · <model>]\n
system / api_retry [Retrying API call...]\n
system / other null (filtered)
assistant text block <text>\n (raw)
assistant tool_use block [<ToolLabel>] <arg>\n (see below)
assistant thinking block null (filtered)
user tool_result block → <summary>\n (see below)
result \n--- Result ---\n<text>\n
unrecognized / parse failure raw line (existing behavior for non-JSON)

A single assistant message with N content blocks produces N output lines, concatenated into one return string.

Tool label + arg

Pick the most identifying input field per tool:

Tool name Display
Read, Write, Edit, NotebookEdit [<Tool>] <basename(file_path)>
Bash, PowerShell [Bash] $ <command> — truncate command at 120 chars, append
Grep [Grep] "<pattern>"
Glob [Glob] <pattern>
Task, Agent [Task: <subagent_type>] <description> (description truncated to 120)
WebFetch [WebFetch] <url>
WebSearch [WebSearch] "<query>"
TodoWrite [TodoWrite] (no arg)
fallback [<name>]

Missing or empty input fields → emit the label only, no trailing text.

tool_result summary

For each tool_result block in a user message, in priority order:

  1. is_error == true→ error: <first non-empty line, trimmed, ≤120 chars>
  2. Envelope has tool_use_result.file.numLines→ <N> lines
  3. Content resolves to empty/whitespace string → → ok
  4. Otherwise → → <first non-empty line, ≤120 chars> (append if truncated)

Content resolution: if content is a string, use it; if it's an array, join the text fields of {type:"text"} entries.

Non-goals

  • No changes to DetailsIslandViewModel or the Worker pipeline.
  • No collapsible/rich rendering — tool results stay one-liners.
  • No persistence changes — the raw .log file still contains full JSON for debugging.
  • No unit tests in this change (separate workload).

Out of scope

  • Partial-token streaming (--include-partial-messages). The existing stream_event branch is removed as dead code.
  • Structured output / --json-schema rendering beyond the final result.

Risks / edge cases

  • Unknown tool names — fallback label [<name>] keeps output readable.
  • Malformed JSON inside a valid envelope (e.g. missing message.content) — skip the broken block, emit what we can; never throw.
  • Very long Bash commands or search queries — 120-char truncation with keeps lines reasonable while preserving the prefix.
  • Binary or huge tool_result content — summary rules 24 cap output at a single line; full content stays in the raw log.