fix(tui): drop post-Done Markdown body re-render (v0.8.2)
Operator: "first turn double prints agent's turn." Root cause: v0.8.1 wrote both the streamed Text lines AND the post- Done `Markdown(event.response)` body into the transcript. Same content rendered twice — once as plain streaming, once as a full markdown re-render. The v0.8.1 commit message documented this as "some duplication is acceptable" but the live UX read as a bug. ## Fix Drop the post-Done `Rule + Markdown(response)` writes in non-raw mode. The streamed text IS the response; whatever the model emitted flows into the transcript line-by-line via coalesce-on-newline. Markdown formatting (bold, lists, code blocks) renders as plain text — a known regression from v0.8.1's polished output but the right tradeoff vs the duplication bug. ## What this loses temporarily Pre-v0.8.2 (after Done): [done] turn_id=... ─── ─── (Rule separator) ─── **Bold text** rendered bold, `code` highlighted, lists as bullets, etc. v0.8.2 (after Done): [done] turn_id=... ─── **Bold text** as plain asterisks, `code` as backticks, lists as plain dashes ## v0.9.0 plan Restore markdown rendering via LIVE rendering during the stream (not post-Done re-render). Replace `RichLog#transcript` with a `VerticalScroll` container that mounts a fresh `Markdown` widget per turn; Text deltas update the widget; markdown renders as content arrives. No duplication, no snap, full formatting. Operator-confirmed direction (2026-05-25 AskUserQuestion). ## Tests 287/287 GREEN; ruff clean. Two tests updated for the new shape: - test_done_renders_markdown_after_label → renamed test_done_flushes_tail_and_writes_label; asserts NO Markdown, NO Rule (post-Done) in the writes. - test_happy_text_done_renders_markdown → renamed test_happy_text_done_no_double_print; asserts NO Markdown in the spy. Patch bump (v0.8.1 → v0.8.2): bug fix; no public API change.
This commit is contained in:
+22
-18
@@ -333,11 +333,12 @@ class TestTuiPresenterState:
|
||||
# thinking_log got at least Rule(start) + "partial" delta + Rule(end)
|
||||
assert thinking_log.write.call_count >= 3
|
||||
|
||||
def test_done_renders_markdown_after_label(self) -> None:
|
||||
"""done_renders_markdown_after_label [happy, v0.8.1]:
|
||||
def test_done_flushes_tail_and_writes_label(self) -> None:
|
||||
"""done_flushes_tail_and_writes_label [happy, v0.8.2]:
|
||||
Text("hi") buffers in text_chunk_buffer (no `\\n`). Done flushes
|
||||
"hi" as a tail line in transcript, then writes [done] + Rule +
|
||||
Markdown body (non-raw).
|
||||
"hi" tail to transcript, then writes [done] label. v0.8.2 drops
|
||||
the post-Done Markdown body re-render — streamed text is the
|
||||
canonical content (no double-print).
|
||||
"""
|
||||
from rich.markdown import Markdown
|
||||
from rich.rule import Rule
|
||||
@@ -354,7 +355,6 @@ class TestTuiPresenterState:
|
||||
thinking_log=MagicMock(),
|
||||
raw=False,
|
||||
)
|
||||
# v0.8.1: Text "hi" stays buffered (no `\n` yet) — no log write yet.
|
||||
assert not log.write.called
|
||||
assert state.text_chunk_buffer == "hi"
|
||||
state.render(
|
||||
@@ -365,12 +365,13 @@ class TestTuiPresenterState:
|
||||
thinking_log=MagicMock(),
|
||||
raw=False,
|
||||
)
|
||||
# On Done: tail flush + [done] + Rule + Markdown body.
|
||||
# On Done: tail flush "hi" + [done] label. No Markdown, no Rule.
|
||||
writes = [c[0][0] for c in log.write.call_args_list]
|
||||
assert "hi" in writes
|
||||
assert any(_text_of(w).startswith("[done]") for w in writes)
|
||||
assert any(isinstance(w, Rule) for w in writes)
|
||||
assert any(isinstance(w, Markdown) for w in writes)
|
||||
# v0.8.2: no post-Done re-render — no duplicate content.
|
||||
assert not any(isinstance(w, Markdown) for w in writes)
|
||||
assert not any(isinstance(w, Rule) for w in writes)
|
||||
assert state.text_chunk_buffer == ""
|
||||
|
||||
def test_raw_flag_skips_markdown(self) -> None:
|
||||
@@ -1059,11 +1060,12 @@ async def _submit_and_wait(app: RatatoskrApp, pilot, content: str) -> None:
|
||||
|
||||
class TestStreamTurnWorker:
|
||||
@respx.mock
|
||||
async def test_happy_text_done_renders_markdown(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""happy_text_done_renders_markdown [happy,tracer, v0.8.1]:
|
||||
Text("hello") buffers in text_chunk_buffer (no `\\n`); on Done,
|
||||
flushes "hello" tail to transcript, then [done] label, then Rule
|
||||
+ Markdown body.
|
||||
async def test_happy_text_done_no_double_print(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""happy_text_done_no_double_print [happy,tracer, v0.8.2]:
|
||||
Text("hello") buffers; on Done, "hello" flushes as tail to transcript
|
||||
+ [done] label. v0.8.2 drops the post-Done Markdown body re-render
|
||||
(was double-printing the response — streamed text + Markdown twice).
|
||||
Only the turn-header Rule remains in the transcript.
|
||||
"""
|
||||
stream = _sse_chunk("42:1", {"type": "text", "content": "hello"}) + _sse_chunk(
|
||||
"42:2", _DONE_BODY
|
||||
@@ -1080,14 +1082,16 @@ class TestStreamTurnWorker:
|
||||
await _submit_and_wait(app, pilot, "hi")
|
||||
assert app.state == "idle"
|
||||
from rich.markdown import Markdown
|
||||
from rich.rule import Rule
|
||||
|
||||
# v0.8.1: "hello" appears in transcript as a tail-flush on Done.
|
||||
# "hello" appears as a tail-flush; [done] label fires.
|
||||
assert any(w == "hello" for w in writes)
|
||||
assert any("[done]" in str(w) for w in writes)
|
||||
# Post-Done: Markdown body + Rule + turn-header Rule all present.
|
||||
assert any(isinstance(w, Markdown) for w in writes)
|
||||
assert any(isinstance(w, Rule) for w in writes)
|
||||
# v0.8.2: NO Markdown body re-render (was the duplicate).
|
||||
assert not any(isinstance(w, Markdown) for w in writes)
|
||||
# The turn-header Rule is written to all 4 panes; we still expect
|
||||
# SOME Rules in the spy (one per pane), but NOT the post-Done
|
||||
# separator Rule that pre-v0.8.2 wrote.
|
||||
# We rely on _spy_writes counting turn-header Rules only.
|
||||
|
||||
@respx.mock
|
||||
async def test_raw_flag_skips_markdown_render(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
|
||||
Reference in New Issue
Block a user