yoke/loop/plan.md

5.2 KiB

Richer Stdout Display

Deliverable: Improve the NDJSON stream filter so the operator can follow Claude's full reasoning. Currently assistant text is truncated to 3 lines / 120 chars and many event types are silently dropped. No new crates — pure std only.


Context

The stream filter in src/stream.rs reads Claude's NDJSON output and formats it as ANSI-colored terminal output. Current problems:

  1. Assistant text is hard-capped at 3 lines and 120 chars per line. This cuts off Claude's thinking and explanations — the operator has no idea what it's doing.

  2. The stream_event wrapper type (the majority of NDJSON lines) is silently dropped. These carry content_block_delta events with partial text and input JSON, which are useful for streaming progress.

  3. Tool results are invisible — the user event type (which carries tool_result content) is never matched.

Goal: Show Claude's full text output, stream partial text deltas as they arrive, and show a brief summary of tool results.


Implementation Stages

Stage A: Remove text truncation, show full assistant output

Objective: Stop cutting off assistant text so the operator sees everything.

src/stream.rs — in the Some("assistant") branch, the text display path:

Current code truncates: for text_line in trimmed.lines().take(3) { let truncated = if text_line.len() > 120 { &text_line[..120] } else { text_line }; println!("{} {}{}", DIM, truncated, RESET); }

Change to print all lines with no length limit: for text_line in trimmed.lines() { println!("{} {}{}", DIM, text_line, RESET); }

Verify: cargo check -p yoke passes.

Stage B: Display streaming text deltas

Objective: Show content_block_delta events so text streams to the terminal as Claude thinks, rather than appearing only in the final assistant message.

src/stream.rs — add a Some("stream_event") match arm. These lines wrap inner events in a {"type":"stream_event","event":{...}} envelope.

The inner event types to handle:

content_block_delta with "text_delta": The delta has {"type":"text_delta","text":"..."}. Extract the text field from the inner delta and print it inline (no newline — use print! not println!) so streaming text accumulates naturally: print!("{}{}{}", DIM, text, RESET); Flush stdout after each delta.

content_block_delta with "input_json_delta": These are partial tool input being streamed. Skip these silently — the complete tool call will be shown when the full assistant event arrives.

content_block_start: If it contains "tool_use", print nothing (the full assistant event will show the tool call). If it contains "text", print a newline to start a fresh line for streaming text.

content_block_stop: Print a newline to terminate any streaming text on the current line.

message_start, message_delta, message_stop: Skip silently — these are bookkeeping.

To detect the inner event type, use extract_str on the line for the "type" field inside "event". Since the line has a top-level "type":"stream_event" and an inner "type":"content_block_delta" (etc.), and extract_str now retries past value matches, you can search for the inner type by looking for specific strings:

  • line.contains(""content_block_delta"") → delta handling
  • line.contains(""content_block_start"") → start handling
  • line.contains(""content_block_stop"") → stop handling
  • Otherwise → skip

For text_delta extraction: search for "text_delta" in the line, then extract the "text" field. Since the line may have multiple "text" keys (the delta type and the actual text content), extract_str's retry logic will handle this.

Important: flush stdout after each print! call so streaming text appears immediately: use std::io::stdout; stdout().flush().ok();

Verify: cargo check -p yoke passes.

Stage C: Show tool result summaries

Objective: When Claude reads a file or runs a command, show a brief summary of the tool result so the operator knows what happened.

src/stream.rs — add a Some("user") match arm. User events carry tool results in the format: {"type":"user","message":{"role":"user","content":[{"tool_use_id":"...","type":"tool_result","content":"..."}]}}

For tool results:

  • Extract the tool_use_id (not critical but nice)
  • Check if the line contains "tool_result"
  • Show a brief dim summary: the first 120 chars of the content, or just "[tool result]" if content can't be extracted
  • Format: println!("{} ← result ({}b){}", DIM, content_len, RESET) where content_len is the approximate length of the result content

Keep it simple — just indicate a result came back and roughly how big it was. The exact content is less important than knowing it happened.

Verify: cargo check -p yoke passes.


Files to modify

File Change
src/stream.rs Remove truncation, add stream_event + user event handling

Constraints

No external crates. Edition 2024. Pure std only. Only modify src/stream.rs.


Success Criteria

  1. cargo check -p yoke passes
  2. Assistant text displays in full — no line count or character limit
  3. Streaming text deltas appear as Claude thinks
  4. Tool results show a brief acknowledgment line