• v0.7.1 9918c10acf

    fix(tui): coalesce thinking deltas on `\n` (v0.7.1)

    vh released this 2026-05-24 20:39:55 -07:00 | 250 commits to main since this release

    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.

    Downloads