fix(tui): anchor layout via dock so Input never moves (v0.2.1)

Reported during v0.2.0 mimir smoke: the Input pane bounces up/down
mid-turn and streamed tokens land at shifting screen positions. Cause
is the v0.2.0 compose order — `Static(id="thinking-current")` was
yielded between hint and Footer in the auto-stacked flow, so each
display=True/False toggle per thinking-run shifted Input + identity +
hint vertically. RichLog growth from streaming text also drifted Input
downward in the auto-layout.

Fix: dock the chrome to the screen edges via DEFAULT_CSS:
- thinking-current docks top under Header (grows/shrinks above RichLog,
  doesn't affect Input position).
- transcript (RichLog) gets `height: 1fr` — absorbs all layout reflows
  internally via its scroll viewport.
- prompt (Input), identity, hint all dock bottom — locked above Footer.

Compose order moves thinking-current to position 2 (right after Header)
so its dock-top placement is visually adjacent to where Textual lays it
out. Old position (between hint and Footer) would still work with the
dock CSS, but the proximity reads more clearly.

Screen-relative positions are now stable: Input is anchored to the
bottom-dock stack; RichLog's content scrolls inside its bounded
viewport regardless of how much thinking-current expands. Tokens land
at the same screen position each delta.

No public API change; pure layout fix. 209/209 tests GREEN; ruff clean.
v0.2.0 → v0.2.1 (patch).

Cannot directly verify in TTY from a non-interactive session; operator
verification needed in real terminal.
This commit is contained in:
2026-05-23 17:13:27 -07:00
parent 3b9c610587
commit c85f6bd701
3 changed files with 37 additions and 5 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "ratatoskr"
version = "0.2.0"
version = "0.2.1"
description = "Worldtree Conversation API debug TUI — multi-pane observability dashboard"
readme = "README.md"
requires-python = ">=3.12"
+35 -3
View File
@@ -214,6 +214,33 @@ class TuiPresenterState:
class RatatoskrApp(App[int]):
"""Textual TUI shell — single chat pane."""
# Issue #12 follow-up: anchor layout so Input never moves.
# Pre-fix: every widget was auto-stacked. RichLog grew with content,
# thinking-current toggled display 0↔N rows per thinking-run — both pushed
# Input around mid-turn. Fix: dock the chrome to the top/bottom edges and
# let RichLog (the only `1fr` widget) absorb all layout reflows internally
# via its scroll viewport, so screen-relative positions stay stable.
DEFAULT_CSS = """
#thinking-current {
dock: top;
height: auto;
}
#transcript {
height: 1fr;
}
#prompt {
dock: bottom;
}
#identity {
dock: bottom;
height: 1;
}
#hint {
dock: bottom;
height: 1;
}
"""
BINDINGS: ClassVar[list[Binding]] = [
Binding("ctrl+c", "interrupt", "Cancel / Exit", priority=True),
Binding("ctrl+d", "quit", "Exit immediately", priority=True),
@@ -243,6 +270,14 @@ class RatatoskrApp(App[int]):
def compose(self) -> ComposeResult:
yield Header()
# Issue #12 follow-up: thinking-current sits at the TOP under Header (via
# DEFAULT_CSS `dock: top`). Pre-fix it lived between hint and Footer in
# the auto-stacked flow, so its display=True/False toggle per
# thinking-run pushed Input + identity + hint up/down on every cycle.
# Docking top + RichLog filling middle stabilises Input's screen
# position; thinking-current grows/shrinks under Header where the
# reflow doesn't affect anything else.
yield Static("", id="thinking-current")
# markup=False so labeled lines like "[cancel_failed] ..." render verbatim
# (Rich would otherwise interpret square-bracket spans as style markup and
# strip them). The post-Done markdown render uses Markdown() directly which
@@ -255,9 +290,6 @@ class RatatoskrApp(App[int]):
# always-visible.
yield Static("", id="identity")
yield Static(self.HINT_IDLE, id="hint")
# Issue #12: live thinking widget — hidden by default, shown per-delta
# during a thinking run, cleared+hidden at turn terminal.
yield Static("", id="thinking-current")
yield Footer()
async def on_mount(self) -> None:
Generated
+1 -1
View File
@@ -968,7 +968,7 @@ wheels = [
[[package]]
name = "ratatoskr"
version = "0.2.0"
version = "0.2.1"
source = { editable = "." }
dependencies = [
{ name = "httpx" },