# 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: ```jsonc // 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 · ]\n` | | `system` / `api_retry` | `[Retrying API call...]\n` | | `system` / other | `null` (filtered) | | `assistant` text block | `\n` (raw) | | `assistant` tool_use block | `[] \n` (see below) | | `assistant` thinking block | `null` (filtered) | | `user` tool_result block | `→ \n` (see below) | | `result` | `\n--- Result ---\n\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` | `[] ` | | `Bash`, `PowerShell` | `[Bash] $ ` — truncate command at 120 chars, append `…` | | `Grep` | `[Grep] ""` | | `Glob` | `[Glob] ` | | `Task`, `Agent` | `[Task: ] ` (description truncated to 120) | | `WebFetch` | `[WebFetch] ` | | `WebSearch` | `[WebSearch] ""` | | `TodoWrite` | `[TodoWrite]` (no arg) | | fallback | `[]` | 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: ` 2. Envelope has `tool_use_result.file.numLines` → `→ lines` 3. Content resolves to empty/whitespace string → `→ ok` 4. Otherwise → `→ ` (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 `[]` 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 2–4 cap output at a single line; full content stays in the raw log.