# 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 <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 2–4 cap output at a
  single line; full content stays in the raw log.
