Compare commits

...

2 Commits

Author SHA1 Message Date
vh 85143b866c fix(tui): disable RichLog min_width floor so wrap actually applies (v0.14.2)
The four right-column panes (tools/debug/thinking/persona) have all
carried `wrap=True` since their introduction, but long lines were
still horizontally scrolling instead of wrapping. Root cause: Textual's
RichLog defaults `min_width=78`, and the App's render path takes
`max(renderable_width, min_width)` after the shrink step. The right
column is 1fr against the left column's 2fr, so at common terminal
widths (≤120 cols) the panes are narrower than 78 cells — the 78-cell
floor was forcing content to render at 78 wide and horizontally scroll
instead of wrapping at the actual pane width.

Set `min_width=0` on all four right-column RichLog instances so
shrink-to-widget-width can actually shrink. `wrap=True` now takes
effect on long lines as expected.

Patch per SemVer discipline: bug fix to a long-standing visible-UX
defect; no public API change, no behavior change for callers, every
existing caller continues to work — the substrate is more correct.
2026-05-27 12:25:16 -07:00
vh 00854ce618 fix(cli): wire AffectUpdate + AwaitingLlmFirstToken into --send presenter (v0.14.1)
The CLI presenter at cli.py:201 carries its own isinstance check on
the Event union (mirroring the TUI presenter's same pattern). v0.11.0
+ v0.14.0 added AffectUpdate + AwaitingLlmFirstToken to the wire layer
but only updated the TUI presenter, leaving the CLI presenter stuck
on the pre-v0.11.0 event vocabulary.

Effect: `ratatoskr --send` crashes with AssertionError on any v0.28.0+
server emitting either of those events. Persona-enabled agents
(affect_update fires on every qualifying turn) and slow-first-token
turns (awaiting_llm_first_token heartbeats fire at 5s intervals) are
both reliably broken. Surfaced while running a wire-trace smoke test
against a Gemma4-based Tier 3 agent.

Patch-bump per SemVer discipline: corrects drift on the just-shipped
surface (v0.11.0 / v0.14.0 wire layer); no public signature change,
no new behavior, existing callers don't care — the bug fix lets them
keep working against current servers.

Routing additions in cli.py:
- AffectUpdate: stderr line with status + (for current) dominant_emotion
- AwaitingLlmFirstToken: stderr line with turn_id + elapsed (seconds)
2026-05-27 00:25:47 -07:00
4 changed files with 43 additions and 6 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "ratatoskr"
version = "0.14.0"
version = "0.14.2"
description = "Worldtree Conversation API debug TUI — multi-pane observability dashboard"
readme = "README.md"
requires-python = ">=3.12"
+25
View File
@@ -18,6 +18,8 @@ import httpx
from ratatoskr.sessions import AgentNotFound, SessionApiFailed, create_session
from ratatoskr.sse_client import (
AffectUpdate,
AwaitingLlmFirstToken,
CancelAlreadyCompleted,
CancelFailed,
Cancelled,
@@ -205,6 +207,7 @@ class CliPresenterState:
(
WorkerPhase, Thinking, Text, TextBoundary,
ToolStart, ToolResult, Done, Error, Cancelled,
AffectUpdate, AwaitingLlmFirstToken,
),
)
# Thinking events accumulate into the open run.
@@ -275,6 +278,28 @@ class CliPresenterState:
f". text_boundary: kind={event.kind} char_offset={event.char_offset}\n"
)
return
if isinstance(event, AffectUpdate):
# Worldtree #204 / v0.28.0. CLI surface is debug telemetry —
# one line to stderr with status + (for current) dominant_emotion.
if event.snapshot is not None:
dom = event.snapshot.get("dominant_emotion")
stderr.write(
f". affect_update: status={event.status} turn_id={event.turn_id} "
f"dominant_emotion={dom!r}\n"
)
else:
stderr.write(
f". affect_update: status={event.status} turn_id={event.turn_id}\n"
)
return
if isinstance(event, AwaitingLlmFirstToken):
# Worldtree #201 / v0.29.0. Heartbeat during BuildingPrompt →
# CallingLLM gap. Stderr surface, one line per heartbeat.
secs = event.elapsed_ms_since_building_prompt / 1000.0
stderr.write(
f". awaiting_llm_first_token: turn_id={event.turn_id} elapsed={secs:.1f}s\n"
)
return
async def _cancel_and_log(
+16 -4
View File
@@ -1023,13 +1023,23 @@ class RatatoskrApp(App[int]):
yield Input(id="prompt", placeholder="Type a message and press Enter")
with Vertical(id="right-column"):
with TabbedContent(id="side-panes"):
# v0.14.2: min_width=0 disables Textual's 78-cell floor
# on RichLog. The right column is 1fr against the left
# column's 2fr, so at typical terminal widths the right-
# column panes are narrower than 78 cells — and the
# default min_width=78 was forcing content to render at
# 78 wide and horizontally scroll instead of wrapping at
# the actual widget width. With min_width=0, wrap=True
# finally takes effect on long lines.
with TabPane("Tools", id="tools-tab"):
yield RichLog(
id="tools-log", wrap=True, markup=False, highlight=False
id="tools-log", wrap=True, markup=False,
highlight=False, min_width=0,
)
with TabPane("Debug", id="debug-tab"):
yield RichLog(
id="debug-log", wrap=True, markup=False, highlight=False
id="debug-log", wrap=True, markup=False,
highlight=False, min_width=0,
)
with TabPane("Thinking", id="thinking-tab"):
# v0.6.5: thinking streams directly into this
@@ -1039,7 +1049,8 @@ class RatatoskrApp(App[int]):
# as content arrives — no more "200-char tail
# window scrolling at the bottom".
yield RichLog(
id="thinking-log", wrap=True, markup=False, highlight=False
id="thinking-log", wrap=True, markup=False,
highlight=False, min_width=0,
)
with TabPane("Persona", id="persona-tab"):
# v0.13.0: full persona-snapshot detail (PAD,
@@ -1047,7 +1058,8 @@ class RatatoskrApp(App[int]):
# appended) on each AffectUpdate(current) — the
# snapshot is absolute state, not incremental.
yield RichLog(
id="persona-log", wrap=True, markup=False, highlight=False
id="persona-log", wrap=True, markup=False,
highlight=False, min_width=0,
)
# INV-002 + INV-003: visible identity + hint widgets (Footer-area).
# pane-name widget displays current side-pane name.
Generated
+1 -1
View File
@@ -968,7 +968,7 @@ wheels = [
[[package]]
name = "ratatoskr"
version = "0.14.0"
version = "0.14.2"
source = { editable = "." }
dependencies = [
{ name = "httpx" },