fix(tui): coalesce thinking deltas on \n (v0.7.1)
Operator: "thinking tokens seem to be split by token — each on a
newline, is that correct? We don't want that."
Root cause: v0.6.5 wrote each Thinking SSE delta as its own
`thinking_log.write(event.content)` call. Worldtree emits Thinking
events at token granularity (per-token or per-few-tokens), so EACH
token became its own RichLog line — visually choppy, one short
fragment per visual row. Wrong UX.
## Fix: coalesce-on-newline
Thinking deltas accumulate in `TuiPresenterState.thinking_chunk_buffer`
(new str field). On each Thinking event:
1. Append delta content to buffer.
2. Flush every COMPLETE line (chars before each `\n`) as one
thinking_log.write(line) call.
3. Leave the post-final-`\n` tail in the buffer for the next delta.
On any non-thinking event (run close):
1. Flush remaining buffer tail (if any) as one final line.
2. Write Rule(end).
Empty lines (blank paragraph separators in the model's `\n\n` flow)
are skipped — they'd render as no-content RichLog entries which
just add vertical noise. Natural paragraph breaks become single
visible lines; multi-paragraph thinking renders top-to-bottom.
## Verified live (tier-3 smoke against personal Worldtree)
Defined a `thinky-smoke` agent via `python -m ratatoskr.tier3 define`,
asked "What is 12 times 13?". Thinking pane rendered with natural
paragraph chunks:
── turn N · thinking #1 start ──
Thinking Process:
1. **Analyze the Request:** The user wants to know the result of $12 \times 13$.
2. **Calculate:**
* Method 1: Standard multiplication.
$$12 \times 10 = 120$$
$$12 \times 3 = 36$$
$$120 + 36 = 156$$
* Method 2: $(10 + 2)(10 + 3) = 100 + 30 + 20 + 6 = 156$.
── turn N · thinking #1 end ──
Each line = one natural paragraph or list item. No per-token fragments.
## Edge cases noted
- Long-running thinking with NO `\n` at all stays buffered until run
close → operator sees nothing until close. Possible follow-up: add
a length-threshold flush (e.g., > 500 chars → flush at the last
space). For now this is acceptable; thinking content typically has
`\n` breaks every few sentences.
- Empty deltas (`""`) are ignored implicitly — no buffer growth, no
flush.
- `\n` at the very start of a delta flushes whatever was buffered
before, then leaves the empty post-`\n` tail (empty string) in the
buffer, which doesn't show up as an empty line because of the
`if line:` guard.
## Contract amendment
docs/contracts/issues/13.contract.md INV-022 amended for v0.7.1
coalesce semantics. Drift-check clean.
## Tests
265/265 GREEN; ruff clean. Two updated tests:
- `test_thinking_streams_into_thinking_log` → renamed
`test_thinking_coalesces_until_newline`: 3 token-shaped deltas
with no `\n` → only Rule(start) writes, buffer holds accumulated.
- NEW `test_thinking_flushes_on_newline`: delta carrying `\n` →
Rule(start) + accumulated line + clear buffer.
- `test_thinking_closes_to_thinking_log`: 2 deltas "a", "b" +
close → Rule(start) + tail-flush "ab" + Rule(end) = 3 writes
(was 4 with per-delta).
Patch bump (v0.7.0 → v0.7.1) — internal presenter routing change;
no public-API or layout change.
This commit is contained in:
@@ -161,7 +161,7 @@ New `Static(id="pane-name")` widget alongside the existing `identity` + `hint` w
|
||||
- **INV-019** *(amended v0.6.0)*: Three TabPanes in the right column: `Tools` (id `tools-tab`, contains `#tools-log`) + `Debug` (id `debug-tab`, contains `#debug-log`) + `Thinking` (id `thinking-tab`, contains `#thinking-log`). Ctrl+1/Ctrl+2/Ctrl+3 activate respective tabs. `pane-name` Static reflects active tab name dynamically.
|
||||
- **INV-020** *(amended v0.6.0)*: Render-exception fallback (INV-009) preserves routing per event class: `ToolStart` / `ToolResult` → `tools_log`; `Thinking` → `thinking_log`; `WorkerPhase` / `TextBoundary` → `debug_log`; everything else → `log`.
|
||||
- **INV-021** *(new v0.6.0)*: `Text` events do NOT route to `log` per-delta. They accumulate into `TuiPresenterState.text_buffer` and update a single `current_text` Static (docked above the prompt). On terminal event (`Done`/`Error`/`Cancelled`), `current_text` is cleared and (raw mode) accumulated text or (non-raw) post-Done `Markdown(response)` is written to `log`. The pre-v0.6.0 per-token RichLog spam is retired.
|
||||
- **INV-022** *(amended v0.6.5)*: Thinking deltas stream DIRECTLY into `thinking_log` (one delta = one RichLog line). The first delta of a run writes `Rule(title=f"turn N · thinking #K start")`; subsequent deltas write their raw content as lines; the run closes on the next non-thinking event with `Rule(title=f"turn N · thinking #K end")`. Pre-v0.6.5 markdown re-render dropped — the streamed deltas ARE the content; the whole pane scrolls naturally as content arrives.
|
||||
- **INV-022** *(amended v0.7.1)*: Thinking deltas COALESCE on `\n` boundaries before writing to `thinking_log`. The first delta of a run writes `Rule(title=f"turn N · thinking #K start")`; subsequent deltas accumulate in `TuiPresenterState.thinking_chunk_buffer`; whenever the buffer contains `\n`, the leading line(s) flush as RichLog entries (one entry per natural paragraph). The run closes on the next non-thinking event: any tail in the buffer flushes as a final line, then `Rule(title=f"turn N · thinking #K end")`. Pre-v0.7.1 per-delta-per-line caused token-spam (Worldtree emits thinking at token granularity); coalescing produces one log line per natural paragraph, not per token.
|
||||
- **INV-023** *(new v0.6.0)*: Turn-ID header `Rule(title=f"turn N")` is written to all four log panes (`log`, `tools_log`, `debug_log`, `thinking_log`) by `_stream_turn_worker` on the first event of each turn — enables cross-pane visual correlation during multi-turn debugging.
|
||||
- **INV-024** *(amended v0.6.5)*: `thinking-current` Static REMOVED. v0.6.1 placed it inside the Thinking pane (docked bottom); operators reported the bottom-docked Static "scrolling a little section at the bottom" (its 200-char tail acting as a scroll-window) instead of letting the whole pane scroll. v0.6.5 deletes the Static entirely and streams Thinking deltas directly into `thinking_log` (the scrollable RichLog) — the whole pane scrolls naturally as content arrives. The Rule(start) at the first delta of a run is now the live "thinking is happening" indicator.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user