refactor(tui): thinking streams into whole pane (v0.6.5)
Operator: "Why does the thinking scroll a little section at the
bottom of the thinking pane instead of scrolling the whole pane?"
Root cause: v0.6.1's thinking-current Static was docked to the
bottom of the Thinking pane and rendered the last 200 chars of
streaming content. As deltas arrived, the displayed 200-char tail
shifted — old text fell off the left, new text appeared on the
right — visually reading as "a little section scrolling at the
bottom" while the larger thinking-log RichLog above showed only
the previous run's closed content (or nothing on first turn).
## Fix: stream directly into thinking-log
The Static is gone. Thinking deltas now write straight to the
`thinking-log` RichLog (one delta = one line in the scrollable
log). The whole pane scrolls naturally as content arrives —
operator can switch to Ctrl+3 and see streaming content fill
the pane top-to-bottom.
Routing pattern:
First Thinking delta of run:
→ write Rule(title="turn N · thinking #K start") to thinking_log
→ write delta content as a line
→ set thinking_open = True
Subsequent Thinking deltas:
→ write delta content as a line
Non-thinking event (closes the run):
→ write Rule(title="turn N · thinking #K end") to thinking_log
→ reset thinking_open
The Rule(start) at the top of an in-progress run is now the
"thinking is happening" indicator. No more separate live-preview
widget required.
## Trade-off: no markdown re-render
Pre-v0.6.5 closed runs got a Markdown(full_content) render between
the start/end Rules. v0.6.5 drops that — the streamed deltas ARE
the content; re-rendering as Markdown would either need to wait
for run-end (no streaming) OR re-render incrementally per delta
(bad UX). Streaming wins for "live observability" framing.
The downside: if model thinking has Markdown structure (lists,
code), it renders as raw text. Acceptable per operator's "stream
in line" framing.
## Removed widgets
- `Static#thinking-current` (right column / Thinking pane bottom)
- `TuiPresenterState.render` no longer takes a `thinking_widget` param
- `TuiPresenterState.thinking_buffer` field dropped (no accumulation)
- `_stream_turn_worker` no longer queries `#thinking-current`
- `on_mount` no longer hides `#thinking-current`
- DEFAULT_CSS `#thinking-current` block removed
## Contract amendment
INV-022 amended: thinking now streams as raw delta lines, not
Markdown-rendered on close. INV-024 amended: thinking-current
Static removed entirely (was relocated v0.6.1, removed v0.6.5).
Drift-check clean.
## Tests
238/238 GREEN (was 241 — 3 obsolete widget tests deleted:
test_thinking_widget_truncation, test_thinking_widget_visibility_lifecycle,
test_terminal_events_belt_and_braces_widget_cleanup). 5 routing tests
rewritten for the new streaming shape (test_thinking_streams_into_thinking_log,
test_thinking_closes_to_thinking_log, test_multiple_thinking_runs_...,
test_render_exception_fallback, test_cancelled_mid_thinking_closes,
test_left_column_content_only).
ruff clean. Manual injection test confirms routing: Rule(start) +
delta lines write to thinking_log; transcript untouched.
Patch bump (v0.6.4 → v0.6.5) — internal restructure within Thinking
pane; presenter signature narrowed; no caller-visible public API
change (RatatoskrApp + AgentPickerApp surfaces identical).
This commit is contained in:
+84
-193
@@ -11,7 +11,6 @@ from ratatoskr.cli import ParsedArgs
|
||||
from ratatoskr.sse_client import (
|
||||
Cancelled,
|
||||
Done,
|
||||
Error,
|
||||
SseId,
|
||||
Text,
|
||||
Thinking,
|
||||
@@ -116,56 +115,43 @@ SID = SseId(42, 5)
|
||||
class TestTuiPresenterState:
|
||||
"""Tests for the new TuiPresenterState — per issue #12 contract."""
|
||||
|
||||
def test_thinking_coalesce_single_widget_update(self) -> None:
|
||||
"""thinking_coalesce_single_widget_update [happy,tracer]:
|
||||
3 Thinking events → thinking_widget.update called 3 times with cumulative content;
|
||||
RichLog has 0 thinking entries (closure hasn't fired yet).
|
||||
def test_thinking_streams_into_thinking_log(self) -> None:
|
||||
"""thinking_streams_into_thinking_log [happy,tracer, v0.6.5]:
|
||||
3 Thinking deltas → thinking_log gets Rule(start) + 3 delta lines.
|
||||
Transcript untouched; no thinking-current Static involved.
|
||||
"""
|
||||
from rich.rule import Rule
|
||||
|
||||
from ratatoskr.tui import TuiPresenterState
|
||||
|
||||
log = MagicMock()
|
||||
widget = MagicMock()
|
||||
thinking_log = MagicMock()
|
||||
state = TuiPresenterState()
|
||||
state.render(
|
||||
Thinking(sse_id=SID, content="a"),
|
||||
log=log,
|
||||
thinking_widget=widget,
|
||||
tools_log=MagicMock(),
|
||||
debug_log=MagicMock(),
|
||||
current_text=MagicMock(), thinking_log=MagicMock(), raw=False,
|
||||
)
|
||||
state.render(
|
||||
Thinking(sse_id=SID, content="b"),
|
||||
log=log,
|
||||
thinking_widget=widget,
|
||||
tools_log=MagicMock(),
|
||||
debug_log=MagicMock(),
|
||||
current_text=MagicMock(), thinking_log=MagicMock(), raw=False,
|
||||
)
|
||||
state.render(
|
||||
Thinking(sse_id=SID, content="c"),
|
||||
log=log,
|
||||
thinking_widget=widget,
|
||||
tools_log=MagicMock(),
|
||||
debug_log=MagicMock(),
|
||||
current_text=MagicMock(), thinking_log=MagicMock(), raw=False,
|
||||
)
|
||||
# Widget updated 3 times — once per delta — with cumulative content
|
||||
assert widget.update.call_count == 3
|
||||
# Latest call shows the full accumulated content (under 200 chars so no truncation).
|
||||
# v0.5.1 polish: widget text is prefixed with "thinking… " for self-explanation.
|
||||
assert widget.update.call_args_list[-1][0][0] == "thinking… abc"
|
||||
# Widget became visible at first delta
|
||||
assert widget.display is True
|
||||
# No RichLog write yet — closure hasn't fired
|
||||
for chunk in ("a", "b", "c"):
|
||||
state.render(
|
||||
Thinking(sse_id=SID, content=chunk),
|
||||
log=log,
|
||||
tools_log=MagicMock(),
|
||||
debug_log=MagicMock(),
|
||||
current_text=MagicMock(),
|
||||
thinking_log=thinking_log,
|
||||
raw=False,
|
||||
)
|
||||
writes = [c[0][0] for c in thinking_log.write.call_args_list]
|
||||
# 1 Rule(start) + 3 content lines = 4 writes
|
||||
assert len(writes) == 4
|
||||
assert isinstance(writes[0], Rule)
|
||||
assert writes[1] == "a"
|
||||
assert writes[2] == "b"
|
||||
assert writes[3] == "c"
|
||||
# Transcript untouched during thinking streaming.
|
||||
assert log.write.call_count == 0
|
||||
|
||||
def test_thinking_closes_to_thinking_log(self) -> None:
|
||||
"""thinking_closes_to_thinking_log [happy, v0.6.0]: 2x Thinking + WorkerPhase →
|
||||
thinking_log gets Rule(start) + Markdown + Rule(end); debug_log gets worker_phase;
|
||||
transcript and tools_log untouched. Widget cleared+hidden.
|
||||
"""thinking_closes_to_thinking_log [happy, v0.6.5]: 2x Thinking + WorkerPhase →
|
||||
thinking_log gets Rule(start) + 2 delta lines + Rule(end); debug_log gets
|
||||
the worker_phase line; transcript untouched.
|
||||
"""
|
||||
from rich.markdown import Markdown
|
||||
from rich.rule import Rule
|
||||
|
||||
from ratatoskr.tui import TuiPresenterState
|
||||
@@ -173,13 +159,11 @@ class TestTuiPresenterState:
|
||||
log = MagicMock()
|
||||
debug_log = MagicMock()
|
||||
thinking_log = MagicMock()
|
||||
widget = MagicMock()
|
||||
state = TuiPresenterState()
|
||||
for content in ("a", "b"):
|
||||
state.render(
|
||||
Thinking(sse_id=SID, content=content),
|
||||
log=log,
|
||||
thinking_widget=widget,
|
||||
tools_log=MagicMock(),
|
||||
debug_log=debug_log,
|
||||
current_text=MagicMock(),
|
||||
@@ -189,87 +173,32 @@ class TestTuiPresenterState:
|
||||
state.render(
|
||||
WorkerPhase(sse_id=SID, phase="streaming", turn_id=42),
|
||||
log=log,
|
||||
thinking_widget=widget,
|
||||
tools_log=MagicMock(),
|
||||
debug_log=debug_log,
|
||||
current_text=MagicMock(),
|
||||
thinking_log=thinking_log,
|
||||
raw=False,
|
||||
)
|
||||
# v0.6.0: closure writes Rule(start) + Markdown + Rule(end) to thinking_log.
|
||||
thinking_writes = [c[0][0] for c in thinking_log.write.call_args_list]
|
||||
assert any(isinstance(w, Rule) for w in thinking_writes), thinking_writes
|
||||
assert any(isinstance(w, Markdown) for w in thinking_writes), thinking_writes
|
||||
# worker_phase still goes to debug_log; transcript still untouched.
|
||||
assert debug_log.write.called
|
||||
# 1 Rule(start) + 2 delta lines + 1 Rule(end) = 4 writes
|
||||
assert len(thinking_writes) == 4
|
||||
assert isinstance(thinking_writes[0], Rule)
|
||||
assert thinking_writes[1] == "a"
|
||||
assert thinking_writes[2] == "b"
|
||||
assert isinstance(thinking_writes[3], Rule)
|
||||
# worker_phase still goes to debug_log; transcript untouched.
|
||||
assert "· worker_phase:" in _text_of(debug_log.write.call_args_list[-1][0][0])
|
||||
assert not log.write.called
|
||||
# Widget cleared + hidden
|
||||
widget.update.assert_called_with("")
|
||||
assert widget.display is False
|
||||
|
||||
def test_thinking_widget_truncation(self) -> None:
|
||||
"""thinking_widget_truncation [trace]: buffer 500 chars → widget shows "…" + last 200."""
|
||||
from ratatoskr.tui import TuiPresenterState
|
||||
|
||||
log = MagicMock()
|
||||
widget = MagicMock()
|
||||
state = TuiPresenterState()
|
||||
# Push 500 chars across multiple deltas.
|
||||
long = "x" * 500
|
||||
state.render(
|
||||
Thinking(sse_id=SID, content=long),
|
||||
log=log,
|
||||
thinking_widget=widget,
|
||||
tools_log=MagicMock(),
|
||||
debug_log=MagicMock(),
|
||||
current_text=MagicMock(), thinking_log=MagicMock(), raw=False,
|
||||
)
|
||||
last_update = widget.update.call_args_list[-1][0][0]
|
||||
# v0.5.1 polish: widget gets a "thinking… " prefix + ellipsis-truncated tail.
|
||||
assert last_update.startswith("thinking… ")
|
||||
# tail is "…" + last-200 = 201 chars; prefix is 10 chars ("thinking… ")
|
||||
assert len(last_update) == len("thinking… ") + 201
|
||||
assert "…" in last_update
|
||||
|
||||
def test_thinking_widget_visibility_lifecycle(self) -> None:
|
||||
"""thinking_widget_visibility_lifecycle [trace]: hidden at start; visible during thinking;
|
||||
hidden after closing event.
|
||||
"""
|
||||
from ratatoskr.tui import TuiPresenterState
|
||||
|
||||
log = MagicMock()
|
||||
widget = MagicMock()
|
||||
widget.display = False # initial state (composed hidden)
|
||||
state = TuiPresenterState()
|
||||
# First thinking delta → widget visible
|
||||
state.render(
|
||||
Thinking(sse_id=SID, content="x"),
|
||||
log=log,
|
||||
thinking_widget=widget,
|
||||
tools_log=MagicMock(),
|
||||
debug_log=MagicMock(),
|
||||
current_text=MagicMock(), thinking_log=MagicMock(), raw=False,
|
||||
)
|
||||
assert widget.display is True
|
||||
# Closure (WorkerPhase) → widget hidden
|
||||
state.render(
|
||||
WorkerPhase(sse_id=SID, phase="streaming", turn_id=42),
|
||||
log=log,
|
||||
thinking_widget=widget,
|
||||
tools_log=MagicMock(),
|
||||
debug_log=MagicMock(),
|
||||
current_text=MagicMock(), thinking_log=MagicMock(), raw=False,
|
||||
)
|
||||
assert widget.display is False
|
||||
# v0.6.5: thinking-current Static removed; test_thinking_widget_truncation
|
||||
# and test_thinking_widget_visibility_lifecycle deleted (no longer apply).
|
||||
|
||||
def test_multiple_thinking_runs_each_get_thinking_log_section(self) -> None:
|
||||
"""multiple_thinking_runs_each_get_thinking_log_section [scenario, v0.6.0]:
|
||||
Thinking → Text → Thinking → Done → TWO start/end Rule + Markdown sections
|
||||
in thinking_log. Text goes to current_text Static (buffered). Transcript
|
||||
receives [done] label + Markdown body only.
|
||||
"""multiple_thinking_runs_each_get_section [scenario, v0.6.5]:
|
||||
Thinking → Text → Thinking → Done → TWO start/end Rule pairs in
|
||||
thinking_log, each wrapping their delta lines. Text goes to
|
||||
current_text (buffered). Transcript: [done] + Markdown body.
|
||||
"""
|
||||
from rich.markdown import Markdown
|
||||
from rich.rule import Rule
|
||||
|
||||
from ratatoskr.tui import TuiPresenterState
|
||||
@@ -277,7 +206,6 @@ class TestTuiPresenterState:
|
||||
log = MagicMock()
|
||||
thinking_log = MagicMock()
|
||||
current_text = MagicMock()
|
||||
widget = MagicMock()
|
||||
state = TuiPresenterState()
|
||||
for evt in (
|
||||
Thinking(sse_id=SID, content="first"),
|
||||
@@ -285,22 +213,23 @@ class TestTuiPresenterState:
|
||||
Thinking(sse_id=SID, content="second"),
|
||||
):
|
||||
state.render(
|
||||
evt, log=log, thinking_widget=widget,
|
||||
evt, log=log,
|
||||
tools_log=MagicMock(), debug_log=MagicMock(),
|
||||
current_text=current_text, thinking_log=thinking_log, raw=False,
|
||||
)
|
||||
state.render(
|
||||
_make_tui_done(),
|
||||
log=log, thinking_widget=widget,
|
||||
log=log,
|
||||
tools_log=MagicMock(), debug_log=MagicMock(),
|
||||
current_text=current_text, thinking_log=thinking_log, raw=False,
|
||||
)
|
||||
# v0.6.0: thinking_log holds (Rule(start) + Markdown + Rule(end)) x2.
|
||||
# v0.6.5: thinking_log holds 4 Rules (start + end per run) + 2 delta lines.
|
||||
thinking_writes = [c[0][0] for c in thinking_log.write.call_args_list]
|
||||
rules = [w for w in thinking_writes if isinstance(w, Rule)]
|
||||
markdowns = [w for w in thinking_writes if isinstance(w, Markdown)]
|
||||
delta_strs = [w for w in thinking_writes if isinstance(w, str)]
|
||||
assert len(rules) == 4, f"expected 4 Rules (2 start + 2 end), got {len(rules)}"
|
||||
assert len(markdowns) == 2, f"expected 2 Markdown sections, got {len(markdowns)}"
|
||||
assert "first" in delta_strs
|
||||
assert "second" in delta_strs
|
||||
# Text "hi" went to current_text (buffered), not the transcript directly.
|
||||
current_text.update.assert_any_call("hi")
|
||||
# Transcript: [done] label + Markdown(response) (raw=False).
|
||||
@@ -308,22 +237,25 @@ class TestTuiPresenterState:
|
||||
assert any(w.startswith("[done]") for w in log_writes if isinstance(w, str))
|
||||
|
||||
def test_render_exception_fallback(self) -> None:
|
||||
"""render_exception_fallback [adversarial, v0.6.0]:
|
||||
widget.update raises → thinking_log gets the plain-label fallback for
|
||||
Thinking (per v0.6.0 routing — Thinking now routes to thinking_log,
|
||||
not debug_log). render_error line follows. transcript untouched.
|
||||
"""render_exception_fallback [adversarial, v0.6.5]:
|
||||
thinking_log.write raises → catch in presenter, write plain-label
|
||||
fallback + render_error line via INV-009 fallback path (routing
|
||||
preservation: thinking events still route to thinking_log).
|
||||
"""
|
||||
from ratatoskr.tui import TuiPresenterState
|
||||
|
||||
log = MagicMock()
|
||||
thinking_log = MagicMock()
|
||||
widget = MagicMock()
|
||||
widget.update.side_effect = AttributeError("widget gone (msg should NOT leak)")
|
||||
# First call (Rule write) raises; subsequent calls succeed for fallback.
|
||||
thinking_log.write.side_effect = [
|
||||
AttributeError("rule write failed (msg should NOT leak)"),
|
||||
None,
|
||||
None,
|
||||
]
|
||||
state = TuiPresenterState()
|
||||
state.render(
|
||||
Thinking(sse_id=SID, content="x"),
|
||||
log=log,
|
||||
thinking_widget=widget,
|
||||
tools_log=MagicMock(),
|
||||
debug_log=MagicMock(),
|
||||
current_text=MagicMock(),
|
||||
@@ -333,7 +265,7 @@ class TestTuiPresenterState:
|
||||
writes = [c[0][0] for c in thinking_log.write.call_args_list if isinstance(c[0][0], str)]
|
||||
assert any(w.startswith("[thinking]") for w in writes), writes
|
||||
assert any(w == "[render_error] AttributeError" for w in writes), writes
|
||||
assert not any("widget gone" in w for w in writes), writes
|
||||
assert not any("rule write failed" in w for w in writes), writes
|
||||
assert not log.write.called
|
||||
|
||||
def test_state_reset_per_worker(self) -> None:
|
||||
@@ -344,10 +276,11 @@ class TestTuiPresenterState:
|
||||
s1.render(
|
||||
Thinking(sse_id=SID, content="x"),
|
||||
log=MagicMock(),
|
||||
thinking_widget=MagicMock(),
|
||||
tools_log=MagicMock(),
|
||||
debug_log=MagicMock(),
|
||||
current_text=MagicMock(), thinking_log=MagicMock(), raw=False,
|
||||
current_text=MagicMock(),
|
||||
thinking_log=MagicMock(),
|
||||
raw=False,
|
||||
)
|
||||
s2 = TuiPresenterState()
|
||||
assert s1.thinking_open is True
|
||||
@@ -361,32 +294,29 @@ class TestTuiPresenterState:
|
||||
from ratatoskr.tui import TuiPresenterState
|
||||
|
||||
log = MagicMock()
|
||||
debug_log = MagicMock()
|
||||
widget = MagicMock()
|
||||
thinking_log = MagicMock()
|
||||
state = TuiPresenterState()
|
||||
state.render(
|
||||
Thinking(sse_id=SID, content="partial"),
|
||||
log=log,
|
||||
thinking_widget=widget,
|
||||
tools_log=MagicMock(),
|
||||
debug_log=debug_log,
|
||||
current_text=MagicMock(), thinking_log=MagicMock(), raw=False,
|
||||
debug_log=MagicMock(),
|
||||
current_text=MagicMock(), thinking_log=thinking_log, raw=False,
|
||||
)
|
||||
state.render(
|
||||
Cancelled(
|
||||
sse_id=SID, phase="cancelled", turn_id=42, reason="user", partial_message_id=None
|
||||
),
|
||||
log=log,
|
||||
thinking_widget=widget,
|
||||
tools_log=MagicMock(),
|
||||
debug_log=debug_log,
|
||||
current_text=MagicMock(), thinking_log=MagicMock(), raw=False,
|
||||
debug_log=MagicMock(),
|
||||
current_text=MagicMock(), thinking_log=thinking_log, raw=False,
|
||||
)
|
||||
# v0.6.0: closed thinking lands in thinking_log (Markdown body wrapped in
|
||||
# Rule start/end). terminal [cancelled] still in transcript.
|
||||
# v0.6.5: streamed thinking + Rule(end) in thinking_log; [cancelled] in transcript.
|
||||
log_writes = [_text_of(c[0][0]) for c in log.write.call_args_list]
|
||||
assert any(w.startswith("[cancelled]") for w in log_writes)
|
||||
assert widget.display is False
|
||||
# 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.6.0]:
|
||||
@@ -401,12 +331,10 @@ class TestTuiPresenterState:
|
||||
|
||||
log = MagicMock()
|
||||
current_text = MagicMock()
|
||||
widget = MagicMock()
|
||||
state = TuiPresenterState()
|
||||
state.render(
|
||||
Text(sse_id=SID, content="hi"),
|
||||
log=log,
|
||||
thinking_widget=widget,
|
||||
tools_log=MagicMock(),
|
||||
debug_log=MagicMock(),
|
||||
current_text=current_text,
|
||||
@@ -418,7 +346,6 @@ class TestTuiPresenterState:
|
||||
state.render(
|
||||
_make_tui_done(),
|
||||
log=log,
|
||||
thinking_widget=widget,
|
||||
tools_log=MagicMock(),
|
||||
debug_log=MagicMock(),
|
||||
current_text=current_text,
|
||||
@@ -440,12 +367,10 @@ class TestTuiPresenterState:
|
||||
from ratatoskr.tui import TuiPresenterState
|
||||
|
||||
log = MagicMock()
|
||||
widget = MagicMock()
|
||||
state = TuiPresenterState()
|
||||
state.render(
|
||||
Text(sse_id=SID, content="hi"),
|
||||
log=log,
|
||||
thinking_widget=widget,
|
||||
tools_log=MagicMock(),
|
||||
debug_log=MagicMock(),
|
||||
current_text=MagicMock(), thinking_log=MagicMock(), raw=True,
|
||||
@@ -453,7 +378,6 @@ class TestTuiPresenterState:
|
||||
state.render(
|
||||
_make_tui_done(),
|
||||
log=log,
|
||||
thinking_widget=widget,
|
||||
tools_log=MagicMock(),
|
||||
debug_log=MagicMock(),
|
||||
current_text=MagicMock(), thinking_log=MagicMock(), raw=True,
|
||||
@@ -477,7 +401,6 @@ class TestTuiPresenterState:
|
||||
state.render(
|
||||
WorkerPhase(sse_id=SID, phase="streaming", turn_id=42),
|
||||
log=log,
|
||||
thinking_widget=MagicMock(),
|
||||
tools_log=MagicMock(),
|
||||
debug_log=debug_log,
|
||||
current_text=MagicMock(), thinking_log=MagicMock(), raw=False,
|
||||
@@ -494,36 +417,10 @@ class TestTuiPresenterState:
|
||||
assert text.startswith("· worker_phase:")
|
||||
assert "[worker_phase]" not in text
|
||||
|
||||
def test_terminal_events_belt_and_braces_widget_cleanup(self) -> None:
|
||||
"""terminal_events_belt_and_braces_widget_cleanup [trace]:
|
||||
Done / Error / Cancelled MUST clear+hide the thinking widget even when
|
||||
thinking_open is False (Volva F3 fix; POST-005 + STEPS 5-6).
|
||||
"""
|
||||
from ratatoskr.tui import TuiPresenterState
|
||||
|
||||
for terminal in (
|
||||
_make_tui_done(),
|
||||
Error(sse_id=SID, phase="failed", message="boom", error_code="x"),
|
||||
Cancelled(
|
||||
sse_id=SID, phase="cancelled", turn_id=42, reason="r", partial_message_id=None
|
||||
),
|
||||
):
|
||||
log = MagicMock()
|
||||
widget = MagicMock()
|
||||
widget.display = True # pre-set to non-default to detect the clear
|
||||
state = TuiPresenterState()
|
||||
# thinking_open is False (state just constructed).
|
||||
state.render(
|
||||
terminal,
|
||||
log=log,
|
||||
thinking_widget=widget,
|
||||
tools_log=MagicMock(),
|
||||
debug_log=MagicMock(),
|
||||
current_text=MagicMock(), thinking_log=MagicMock(), raw=True,
|
||||
)
|
||||
# Belt-and-braces: widget cleared + hidden on EVERY terminal event.
|
||||
widget.update.assert_called_with("")
|
||||
assert widget.display is False, type(terminal).__name__
|
||||
# v0.6.5: test_terminal_events_belt_and_braces_widget_cleanup deleted.
|
||||
# The thinking-current Static is gone, so there's no widget to clean up
|
||||
# on terminal events. The corresponding Volva F3 invariant is obsoleted
|
||||
# by the streaming-into-thinking_log architecture.
|
||||
|
||||
def test_tool_start_routes_to_tools_log(self) -> None:
|
||||
"""tool_start_routes_to_tools_log [INV-014]: ToolStart writes to tools_log, NOT transcript.
|
||||
@@ -540,7 +437,6 @@ class TestTuiPresenterState:
|
||||
state.render(
|
||||
ToolStart(sse_id=SID, name="read_file", arguments={"path": "/x"}),
|
||||
log=log,
|
||||
thinking_widget=MagicMock(),
|
||||
tools_log=tools_log,
|
||||
debug_log=MagicMock(),
|
||||
current_text=MagicMock(), thinking_log=MagicMock(), raw=False,
|
||||
@@ -561,7 +457,6 @@ class TestTuiPresenterState:
|
||||
state.render(
|
||||
ToolResult(sse_id=SID, name="read_file", result="ok", duration_ms=12),
|
||||
log=log,
|
||||
thinking_widget=MagicMock(),
|
||||
tools_log=tools_log,
|
||||
debug_log=MagicMock(),
|
||||
current_text=MagicMock(), thinking_log=MagicMock(), raw=False,
|
||||
@@ -583,7 +478,6 @@ class TestTuiPresenterState:
|
||||
state.render(
|
||||
Text(sse_id=SID, content="hello"),
|
||||
log=log,
|
||||
thinking_widget=MagicMock(),
|
||||
tools_log=tools_log,
|
||||
debug_log=MagicMock(),
|
||||
current_text=current_text,
|
||||
@@ -606,7 +500,6 @@ class TestTuiPresenterState:
|
||||
state.render(
|
||||
Text(sse_id=SID, content=tok),
|
||||
log=MagicMock(),
|
||||
thinking_widget=MagicMock(),
|
||||
tools_log=MagicMock(),
|
||||
debug_log=MagicMock(),
|
||||
current_text=current_text,
|
||||
@@ -625,7 +518,6 @@ class TestTuiPresenterState:
|
||||
state.render(
|
||||
_make_tui_done(duration_ms=5467),
|
||||
log=log,
|
||||
thinking_widget=MagicMock(),
|
||||
tools_log=MagicMock(),
|
||||
debug_log=MagicMock(),
|
||||
current_text=MagicMock(), thinking_log=MagicMock(), raw=True,
|
||||
@@ -653,7 +545,6 @@ class TestTuiPresenterState:
|
||||
state.render(
|
||||
_make_tui_done(usage=usage),
|
||||
log=log,
|
||||
thinking_widget=MagicMock(),
|
||||
tools_log=MagicMock(),
|
||||
debug_log=MagicMock(),
|
||||
current_text=MagicMock(), thinking_log=MagicMock(), raw=True,
|
||||
@@ -815,27 +706,28 @@ class TestLayoutShape:
|
||||
assert row is not None
|
||||
|
||||
async def test_left_column_content_only(self) -> None:
|
||||
"""left_column_content_only [v0.5.0]: left column = transcript + prompt ONLY.
|
||||
|
||||
thinking-current Static moved to right column so the left column is
|
||||
genuinely content-only (transcript + prompt input).
|
||||
"""left_column_content_only [v0.6.5]: left column = transcript + prompt
|
||||
+ current-text (streaming text Static). thinking-current Static
|
||||
removed entirely as of v0.6.5.
|
||||
"""
|
||||
from textual.containers import Vertical
|
||||
from textual.widgets import Input, RichLog, Static
|
||||
from textual.widgets import Input, RichLog
|
||||
|
||||
app = _resolved_app(_args_new(), session_id="s-new12345", agent_id="mimir")
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
left = app.query_one("#left-column", Vertical)
|
||||
right = app.query_one("#right-column", Vertical)
|
||||
transcript = app.query_one("#transcript", RichLog)
|
||||
prompt = app.query_one("#prompt", Input)
|
||||
thinking = app.query_one("#thinking-current", Static)
|
||||
assert transcript in left.walk_children()
|
||||
assert prompt in left.walk_children()
|
||||
# v0.5.0: thinking-current is now under the right column, NOT left.
|
||||
assert thinking not in left.walk_children()
|
||||
assert thinking in right.walk_children()
|
||||
# v0.6.5: thinking-current Static removed; no longer in DOM at all.
|
||||
from textual.css.query import NoMatches
|
||||
try:
|
||||
app.query_one("#thinking-current")
|
||||
raise AssertionError("thinking-current should not exist in v0.6.5")
|
||||
except NoMatches:
|
||||
pass # expected
|
||||
|
||||
async def test_right_column_has_tabbed_content_with_tools_tab(self) -> None:
|
||||
"""right_column_has_tabbed_content_with_tools_tab: #side-panes + TabPane#tools-tab."""
|
||||
@@ -940,7 +832,6 @@ class TestLayoutShape:
|
||||
state.render(
|
||||
_make_tui_done(),
|
||||
log=log,
|
||||
thinking_widget=app.query_one("#thinking-current"),
|
||||
tools_log=app.query_one("#tools-log", RichLog),
|
||||
debug_log=app.query_one("#debug-log", RichLog),
|
||||
current_text=MagicMock(), thinking_log=MagicMock(), raw=True,
|
||||
|
||||
Reference in New Issue
Block a user