yoke/loop/plan.md

145 lines
5.2 KiB
Markdown

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