style(tui): UI polish pass (v0.5.1)
Cosmetic refinements on top of v0.5.0's content-only main pane. No
behavior change; ships as a patch bump.
## Color signal — terminal labels tinted per outcome
The transcript's [done]/[error]/[cancelled] labels were plain
foreground (Australis #a9bcc3 white), which made them slow to scan
against the surrounding assistant text. Now tinted per outcome:
- [done] → Aurora green (#16B866 / $success)
- [error] → Dawn red (#ff491a / $error)
- [cancelled] → Dawn yellow (#e1c631 / $warning)
The post-Done Rule() separator is also tinted to Australis dark-60
(#86929d) so the streamed-text → markdown-body boundary reads as
chrome, not a content artifact.
## Empty-state placeholders
Tools and Debug panes were stark-empty before any turn fired — easy
to misread as "the pane is broken." Now show placeholder lines on
mount in Sea dark-50 italic:
Tools tab: (no tool events yet — start a turn that uses tools)
Debug tab: (waiting for telemetry — start a turn)
The placeholders scroll off naturally as real events fill the panes.
## Live thinking widget self-explains
The thinking-current Static at the top of the right column used to
just display raw thinking content with no context — an operator
glancing at the screen mid-stream might not realize they were
looking at LLM chain-of-thought. Now prefixed with "thinking… " so
the widget self-identifies.
## Spacing + chrome
- Transcript / tools-log / debug-log: 1-cell horizontal padding so
content doesn't hug the column border.
- thinking-current: italic text-style on top of the dark-60 color,
so the live-preview band is visually distinct from solid-colored
log content.
- Active tab in TabbedContent: Aurora bright-cyan label + bold
text-style, so the eye lands on the currently selected pane.
- Input placeholder text: tinted to Sea dark-50 so it reads as
placeholder, not content.
## Test impact
3 new tests added (test_done_label_styled_success,
test_empty_state_placeholders_present, plus the polish hits
test_thinking_widget_truncation / test_thinking_coalesce updated for
the "thinking… " prefix). 4 existing tests that checked
`isinstance(w, str) and w.startswith("[done]")` updated to use the
_text_of helper (terminal labels are now RichText, not str).
_spy_writes helper widened to accept positional args after Textual's
internal deferred-render path started passing them positionally
post-Resize.
241/241 GREEN; ruff clean. Live smoke against personal Worldtree
confirmed: Done line renders in Aurora green #16B866 verbatim;
both placeholder lines appear in dark-50; thinking widget shows
"thinking… <content>" during a turn.
Patch bump (v0.5.0 → v0.5.1) per SemVer etiquette: purely cosmetic;
no signature change; no caller-visible behavioral shift.
This commit is contained in:
+75
-14
@@ -61,13 +61,18 @@ def _args_existing(session_id: str = "s-1existing", **overrides) -> ParsedArgs:
|
||||
|
||||
|
||||
def _spy_writes(monkeypatch) -> list:
|
||||
"""Patch RichLog.write to record every arg into a list (returned)."""
|
||||
"""Patch RichLog.write to record every arg into a list (returned).
|
||||
|
||||
Accepts *args/**kwargs so Textual's internal deferred-render path
|
||||
(which calls write positionally with width/expand/shrink/scroll_end)
|
||||
still works after a write-during-mount + Resize sequence.
|
||||
"""
|
||||
writes: list = []
|
||||
original = RichLog.write
|
||||
|
||||
def spy(self, content, **kw):
|
||||
def spy(self, content, *args, **kw):
|
||||
writes.append(content)
|
||||
return original(self, content, **kw)
|
||||
return original(self, content, *args, **kw)
|
||||
|
||||
monkeypatch.setattr(RichLog, "write", spy)
|
||||
return writes
|
||||
@@ -147,8 +152,9 @@ class TestTuiPresenterState:
|
||||
)
|
||||
# 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)
|
||||
assert widget.update.call_args_list[-1][0][0] == "abc"
|
||||
# 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
|
||||
@@ -218,9 +224,11 @@ class TestTuiPresenterState:
|
||||
raw=False,
|
||||
)
|
||||
last_update = widget.update.call_args_list[-1][0][0]
|
||||
# …-prefix + last-200 = 201 chars
|
||||
assert last_update.startswith("…")
|
||||
assert len(last_update) == 201
|
||||
# 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;
|
||||
@@ -426,8 +434,8 @@ class TestTuiPresenterState:
|
||||
writes = [c[0][0] for c in log.write.call_args_list]
|
||||
# Text stream wrote "hi" with no prefix.
|
||||
assert "hi" in writes
|
||||
# [done] label wrote.
|
||||
assert any(isinstance(w, str) and w.startswith("[done]") for w in writes)
|
||||
# v0.5.1: [done] label is now RichText (Aurora green); plain content test via _text_of.
|
||||
assert any(_text_of(w).startswith("[done]") for w in writes)
|
||||
# Rule + Markdown render present (post-Done body re-render per issue #4 INV-005).
|
||||
assert any(isinstance(w, Rule) for w in writes)
|
||||
assert any(isinstance(w, Markdown) for w in writes)
|
||||
@@ -623,9 +631,9 @@ class TestTuiPresenterState:
|
||||
raw=True,
|
||||
)
|
||||
done_line = next(
|
||||
c[0][0]
|
||||
_text_of(c[0][0])
|
||||
for c in log.write.call_args_list
|
||||
if isinstance(c[0][0], str) and c[0][0].startswith("[done]")
|
||||
if _text_of(c[0][0]).startswith("[done]")
|
||||
)
|
||||
assert "duration=5.5s" in done_line
|
||||
assert "duration_ms=5467" not in done_line
|
||||
@@ -651,9 +659,9 @@ class TestTuiPresenterState:
|
||||
raw=True,
|
||||
)
|
||||
done_line = next(
|
||||
c[0][0]
|
||||
_text_of(c[0][0])
|
||||
for c in log.write.call_args_list
|
||||
if isinstance(c[0][0], str) and c[0][0].startswith("[done]")
|
||||
if _text_of(c[0][0]).startswith("[done]")
|
||||
)
|
||||
assert "usage 6756 in → 126 out (6882 total, 0 cached)" in done_line
|
||||
|
||||
@@ -914,6 +922,59 @@ class TestLayoutShape:
|
||||
await pilot.pause()
|
||||
assert app.query_one("#side-panes", TabbedContent).active == "debug-tab"
|
||||
|
||||
async def test_done_label_styled_success(self) -> None:
|
||||
"""done_label_styled_success [v0.5.1]: [done] label renders in Aurora green."""
|
||||
from rich.text import Text as RichText
|
||||
from textual.widgets import RichLog
|
||||
|
||||
app = _resolved_app(_args_new(), session_id="s-new12345", agent_id="mimir")
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
# Probe the presenter directly — write a Done via state.render.
|
||||
from ratatoskr.tui import TuiPresenterState
|
||||
log = app.query_one("#transcript", RichLog)
|
||||
state = TuiPresenterState()
|
||||
seen: list = []
|
||||
orig = log.write
|
||||
log.write = lambda c, *a, **kw: (seen.append(c), orig(c, *a, **kw))[1]
|
||||
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),
|
||||
raw=True,
|
||||
)
|
||||
done = next(
|
||||
c for c in seen
|
||||
if isinstance(c, RichText) and _text_of(c).startswith("[done]")
|
||||
)
|
||||
assert done.style == "#16B866" # Aurora green
|
||||
|
||||
async def test_empty_state_placeholders_present(self) -> None:
|
||||
"""empty_state_placeholders_present [v0.5.1]: tools-log + debug-log show
|
||||
placeholder lines before any turn fires."""
|
||||
from textual.widgets import RichLog
|
||||
|
||||
app = _resolved_app(_args_new(), session_id="s-new12345", agent_id="mimir")
|
||||
async with app.run_test() as pilot:
|
||||
await pilot.pause()
|
||||
# Activate Debug tab so its content actually renders.
|
||||
from textual.widgets import TabbedContent
|
||||
tabbed = app.query_one("#side-panes", TabbedContent)
|
||||
tabbed.active = "debug-tab"
|
||||
await pilot.pause()
|
||||
tabbed.active = "tools-tab"
|
||||
await pilot.pause()
|
||||
tools_log = app.query_one("#tools-log", RichLog)
|
||||
debug_log = app.query_one("#debug-log", RichLog)
|
||||
tools_text = " ".join(str(line) for line in tools_log.lines)
|
||||
tabbed.active = "debug-tab"
|
||||
await pilot.pause()
|
||||
debug_text = " ".join(str(line) for line in debug_log.lines)
|
||||
assert "no tool events" in tools_text
|
||||
assert "waiting for telemetry" in debug_text
|
||||
|
||||
async def test_pane_name_updates_on_tab_switch(self) -> None:
|
||||
"""pane_name_updates_on_tab_switch [v0.5.0]: pane-name reflects active tab.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user