Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9918c10acf |
@@ -161,7 +161,7 @@ New `Static(id="pane-name")` widget alongside the existing `identity` + `hint` w
|
|||||||
- **INV-019** *(amended v0.6.0)*: Three TabPanes in the right column: `Tools` (id `tools-tab`, contains `#tools-log`) + `Debug` (id `debug-tab`, contains `#debug-log`) + `Thinking` (id `thinking-tab`, contains `#thinking-log`). Ctrl+1/Ctrl+2/Ctrl+3 activate respective tabs. `pane-name` Static reflects active tab name dynamically.
|
- **INV-019** *(amended v0.6.0)*: Three TabPanes in the right column: `Tools` (id `tools-tab`, contains `#tools-log`) + `Debug` (id `debug-tab`, contains `#debug-log`) + `Thinking` (id `thinking-tab`, contains `#thinking-log`). Ctrl+1/Ctrl+2/Ctrl+3 activate respective tabs. `pane-name` Static reflects active tab name dynamically.
|
||||||
- **INV-020** *(amended v0.6.0)*: Render-exception fallback (INV-009) preserves routing per event class: `ToolStart` / `ToolResult` → `tools_log`; `Thinking` → `thinking_log`; `WorkerPhase` / `TextBoundary` → `debug_log`; everything else → `log`.
|
- **INV-020** *(amended v0.6.0)*: Render-exception fallback (INV-009) preserves routing per event class: `ToolStart` / `ToolResult` → `tools_log`; `Thinking` → `thinking_log`; `WorkerPhase` / `TextBoundary` → `debug_log`; everything else → `log`.
|
||||||
- **INV-021** *(new v0.6.0)*: `Text` events do NOT route to `log` per-delta. They accumulate into `TuiPresenterState.text_buffer` and update a single `current_text` Static (docked above the prompt). On terminal event (`Done`/`Error`/`Cancelled`), `current_text` is cleared and (raw mode) accumulated text or (non-raw) post-Done `Markdown(response)` is written to `log`. The pre-v0.6.0 per-token RichLog spam is retired.
|
- **INV-021** *(new v0.6.0)*: `Text` events do NOT route to `log` per-delta. They accumulate into `TuiPresenterState.text_buffer` and update a single `current_text` Static (docked above the prompt). On terminal event (`Done`/`Error`/`Cancelled`), `current_text` is cleared and (raw mode) accumulated text or (non-raw) post-Done `Markdown(response)` is written to `log`. The pre-v0.6.0 per-token RichLog spam is retired.
|
||||||
- **INV-022** *(amended v0.6.5)*: Thinking deltas stream DIRECTLY into `thinking_log` (one delta = one RichLog line). The first delta of a run writes `Rule(title=f"turn N · thinking #K start")`; subsequent deltas write their raw content as lines; the run closes on the next non-thinking event with `Rule(title=f"turn N · thinking #K end")`. Pre-v0.6.5 markdown re-render dropped — the streamed deltas ARE the content; the whole pane scrolls naturally as content arrives.
|
- **INV-022** *(amended v0.7.1)*: Thinking deltas COALESCE on `\n` boundaries before writing to `thinking_log`. The first delta of a run writes `Rule(title=f"turn N · thinking #K start")`; subsequent deltas accumulate in `TuiPresenterState.thinking_chunk_buffer`; whenever the buffer contains `\n`, the leading line(s) flush as RichLog entries (one entry per natural paragraph). The run closes on the next non-thinking event: any tail in the buffer flushes as a final line, then `Rule(title=f"turn N · thinking #K end")`. Pre-v0.7.1 per-delta-per-line caused token-spam (Worldtree emits thinking at token granularity); coalescing produces one log line per natural paragraph, not per token.
|
||||||
- **INV-023** *(new v0.6.0)*: Turn-ID header `Rule(title=f"turn N")` is written to all four log panes (`log`, `tools_log`, `debug_log`, `thinking_log`) by `_stream_turn_worker` on the first event of each turn — enables cross-pane visual correlation during multi-turn debugging.
|
- **INV-023** *(new v0.6.0)*: Turn-ID header `Rule(title=f"turn N")` is written to all four log panes (`log`, `tools_log`, `debug_log`, `thinking_log`) by `_stream_turn_worker` on the first event of each turn — enables cross-pane visual correlation during multi-turn debugging.
|
||||||
- **INV-024** *(amended v0.6.5)*: `thinking-current` Static REMOVED. v0.6.1 placed it inside the Thinking pane (docked bottom); operators reported the bottom-docked Static "scrolling a little section at the bottom" (its 200-char tail acting as a scroll-window) instead of letting the whole pane scroll. v0.6.5 deletes the Static entirely and streams Thinking deltas directly into `thinking_log` (the scrollable RichLog) — the whole pane scrolls naturally as content arrives. The Rule(start) at the first delta of a run is now the live "thinking is happening" indicator.
|
- **INV-024** *(amended v0.6.5)*: `thinking-current` Static REMOVED. v0.6.1 placed it inside the Thinking pane (docked bottom); operators reported the bottom-docked Static "scrolling a little section at the bottom" (its 200-char tail acting as a scroll-window) instead of letting the whole pane scroll. v0.6.5 deletes the Static entirely and streams Thinking deltas directly into `thinking_log` (the scrollable RichLog) — the whole pane scrolls naturally as content arrives. The Rule(start) at the first delta of a run is now the live "thinking is happening" indicator.
|
||||||
|
|
||||||
|
|||||||
@@ -32,9 +32,9 @@ separate dev team rather than an in-tree Worldtree tool.
|
|||||||
|
|
||||||
## Current state / in-flight
|
## Current state / in-flight
|
||||||
|
|
||||||
_As of 2026-05-25 (post-v0.7.0 Tier 3 agent lifecycle):_
|
_As of 2026-05-25 (post-v0.7.1 thinking coalesce-by-newline):_
|
||||||
|
|
||||||
**Status: v0.7.0 shipped.** Ten core features complete (`sse_client`
|
**Status: v0.7.1 shipped.** Ten core features complete (`sse_client`
|
||||||
#1, `sessions` #2, `cli` #3, `tui` #4, `--end-user-id` #5, TUI
|
#1, `sessions` #2, `cli` #3, `tui` #4, `--end-user-id` #5, TUI
|
||||||
startup error visibility #6, presenter contract semantics amendment
|
startup error visibility #6, presenter contract semantics amendment
|
||||||
#12, startup agent picker #8, §5 layout reshape + Tools pane #13)
|
#12, startup agent picker #8, §5 layout reshape + Tools pane #13)
|
||||||
@@ -51,7 +51,8 @@ Static in the footer (static "Tools" v1; dynamic when more tabs
|
|||||||
land). CLI mode (--send) unaffected by design — INV-018.
|
land). CLI mode (--send) unaffected by design — INV-018.
|
||||||
|
|
||||||
Last commits on `main`:
|
Last commits on `main`:
|
||||||
- v0.7.0 feat(tier3): ratatoskr.tier3 module + CLI — Worldtree Tier 3 lifecycle
|
- v0.7.1 fix(tui): coalesce thinking deltas on `\n` — no more per-token newlines
|
||||||
|
- `c086ae2` feat(tier3): ratatoskr.tier3 module + CLI (v0.7.0)
|
||||||
- `d356990` refactor(tui): thinking streams into thinking-log (v0.6.5)
|
- `d356990` refactor(tui): thinking streams into thinking-log (v0.6.5)
|
||||||
- `82437bd` style(tui): picker highlighted item → Aurora blue (v0.6.4)
|
- `82437bd` style(tui): picker highlighted item → Aurora blue (v0.6.4)
|
||||||
- `ac690c1` style(tui): restore Australis palette, only $background → pure black (v0.6.3)
|
- `ac690c1` style(tui): restore Australis palette, only $background → pure black (v0.6.3)
|
||||||
|
|||||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "ratatoskr"
|
name = "ratatoskr"
|
||||||
version = "0.7.0"
|
version = "0.7.1"
|
||||||
description = "Worldtree Conversation API debug TUI — multi-pane observability dashboard"
|
description = "Worldtree Conversation API debug TUI — multi-pane observability dashboard"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.12"
|
requires-python = ">=3.12"
|
||||||
|
|||||||
+24
-6
@@ -191,6 +191,12 @@ class TuiPresenterState:
|
|||||||
text_buffer: list[str] = field(default_factory=list)
|
text_buffer: list[str] = field(default_factory=list)
|
||||||
# Thinking-run counter for turn-scoped start/end markers.
|
# Thinking-run counter for turn-scoped start/end markers.
|
||||||
thinking_run_index: int = 0
|
thinking_run_index: int = 0
|
||||||
|
# v0.7.1: thinking-content accumulator. Worldtree emits Thinking deltas
|
||||||
|
# at token granularity; flushing each delta as its own RichLog line
|
||||||
|
# produces per-token-per-newline visual spam. Buffer here and flush
|
||||||
|
# only on `\n` boundaries (one written line per natural paragraph) or
|
||||||
|
# when the run closes (any leftover tail).
|
||||||
|
thinking_chunk_buffer: str = ""
|
||||||
|
|
||||||
def render(
|
def render(
|
||||||
self,
|
self,
|
||||||
@@ -234,10 +240,11 @@ class TuiPresenterState:
|
|||||||
return RichText(s, style=_AU_DEMOTED)
|
return RichText(s, style=_AU_DEMOTED)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# v0.6.5: Thinking deltas stream directly into thinking_log.
|
# v0.7.1: Thinking deltas coalesce by newline before flushing.
|
||||||
# First delta of a run writes the Rule(start) header; each
|
# Worldtree emits Thinking events at token granularity; per-delta
|
||||||
# subsequent delta writes its content as a line; the run closes
|
# RichLog writes produce one visual line per token (per-token-per-
|
||||||
# on the next non-thinking event with a Rule(end).
|
# newline spam). Buffer the deltas and flush only on `\n` (one
|
||||||
|
# written line per natural paragraph) or run close.
|
||||||
if isinstance(event, Thinking):
|
if isinstance(event, Thinking):
|
||||||
from rich.rule import Rule
|
from rich.rule import Rule
|
||||||
|
|
||||||
@@ -249,13 +256,24 @@ class TuiPresenterState:
|
|||||||
style=_AU_DEMOTED,
|
style=_AU_DEMOTED,
|
||||||
))
|
))
|
||||||
self.thinking_open = True
|
self.thinking_open = True
|
||||||
# Stream the delta content (chunk-of-tokens) as one line.
|
self.thinking_chunk_buffer += event.content
|
||||||
thinking_log.write(event.content)
|
# Flush every complete line in the buffer. Whatever's after
|
||||||
|
# the final `\n` stays buffered for the next delta or close.
|
||||||
|
while "\n" in self.thinking_chunk_buffer:
|
||||||
|
line, _, rest = self.thinking_chunk_buffer.partition("\n")
|
||||||
|
if line: # skip empty lines (blank paragraph separators)
|
||||||
|
thinking_log.write(line)
|
||||||
|
self.thinking_chunk_buffer = rest
|
||||||
return
|
return
|
||||||
# Non-thinking event: close any open thinking run with Rule(end).
|
# Non-thinking event: close any open thinking run with Rule(end).
|
||||||
if self.thinking_open:
|
if self.thinking_open:
|
||||||
from rich.rule import Rule
|
from rich.rule import Rule
|
||||||
|
|
||||||
|
# Flush the tail (content with no trailing `\n`) before the
|
||||||
|
# end rule so nothing gets lost on close.
|
||||||
|
if self.thinking_chunk_buffer:
|
||||||
|
thinking_log.write(self.thinking_chunk_buffer)
|
||||||
|
self.thinking_chunk_buffer = ""
|
||||||
turn_id = event.sse_id.turn_id if hasattr(event, "sse_id") else (
|
turn_id = event.sse_id.turn_id if hasattr(event, "sse_id") else (
|
||||||
event.turn_id if hasattr(event, "turn_id") else "?"
|
event.turn_id if hasattr(event, "turn_id") else "?"
|
||||||
)
|
)
|
||||||
|
|||||||
+41
-19
@@ -115,10 +115,11 @@ SID = SseId(42, 5)
|
|||||||
class TestTuiPresenterState:
|
class TestTuiPresenterState:
|
||||||
"""Tests for the new TuiPresenterState — per issue #12 contract."""
|
"""Tests for the new TuiPresenterState — per issue #12 contract."""
|
||||||
|
|
||||||
def test_thinking_streams_into_thinking_log(self) -> None:
|
def test_thinking_coalesces_until_newline(self) -> None:
|
||||||
"""thinking_streams_into_thinking_log [happy,tracer, v0.6.5]:
|
"""thinking_coalesces_until_newline [happy,tracer, v0.7.1]:
|
||||||
3 Thinking deltas → thinking_log gets Rule(start) + 3 delta lines.
|
Per-token deltas accumulate in the buffer; flush only on `\\n`.
|
||||||
Transcript untouched; no thinking-current Static involved.
|
Three short token-shaped deltas without `\\n` → thinking_log gets
|
||||||
|
ONLY Rule(start); content stays buffered.
|
||||||
"""
|
"""
|
||||||
from rich.rule import Rule
|
from rich.rule import Rule
|
||||||
|
|
||||||
@@ -127,7 +128,7 @@ class TestTuiPresenterState:
|
|||||||
log = MagicMock()
|
log = MagicMock()
|
||||||
thinking_log = MagicMock()
|
thinking_log = MagicMock()
|
||||||
state = TuiPresenterState()
|
state = TuiPresenterState()
|
||||||
for chunk in ("a", "b", "c"):
|
for chunk in ("Let", " me", " think"):
|
||||||
state.render(
|
state.render(
|
||||||
Thinking(sse_id=SID, content=chunk),
|
Thinking(sse_id=SID, content=chunk),
|
||||||
log=log,
|
log=log,
|
||||||
@@ -138,19 +139,41 @@ class TestTuiPresenterState:
|
|||||||
raw=False,
|
raw=False,
|
||||||
)
|
)
|
||||||
writes = [c[0][0] for c in thinking_log.write.call_args_list]
|
writes = [c[0][0] for c in thinking_log.write.call_args_list]
|
||||||
# 1 Rule(start) + 3 content lines = 4 writes
|
# Only Rule(start) — content stays buffered (no `\n` seen).
|
||||||
assert len(writes) == 4
|
assert len(writes) == 1
|
||||||
assert isinstance(writes[0], Rule)
|
assert isinstance(writes[0], Rule)
|
||||||
assert writes[1] == "a"
|
assert state.thinking_chunk_buffer == "Let me think"
|
||||||
assert writes[2] == "b"
|
|
||||||
assert writes[3] == "c"
|
|
||||||
# Transcript untouched during thinking streaming.
|
|
||||||
assert log.write.call_count == 0
|
assert log.write.call_count == 0
|
||||||
|
|
||||||
|
def test_thinking_flushes_on_newline(self) -> None:
|
||||||
|
"""thinking_flushes_on_newline [happy, v0.7.1]:
|
||||||
|
Delta carrying `\\n` flushes the accumulated buffer as ONE line.
|
||||||
|
"""
|
||||||
|
from ratatoskr.tui import TuiPresenterState
|
||||||
|
|
||||||
|
thinking_log = MagicMock()
|
||||||
|
state = TuiPresenterState()
|
||||||
|
for chunk in ("Hello", " world", "\n"):
|
||||||
|
state.render(
|
||||||
|
Thinking(sse_id=SID, content=chunk),
|
||||||
|
log=MagicMock(),
|
||||||
|
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]
|
||||||
|
# Rule(start) + "Hello world" (one coalesced line) = 2 writes
|
||||||
|
assert len(writes) == 2
|
||||||
|
assert writes[1] == "Hello world"
|
||||||
|
assert state.thinking_chunk_buffer == ""
|
||||||
|
|
||||||
def test_thinking_closes_to_thinking_log(self) -> None:
|
def test_thinking_closes_to_thinking_log(self) -> None:
|
||||||
"""thinking_closes_to_thinking_log [happy, v0.6.5]: 2x Thinking + WorkerPhase →
|
"""thinking_closes_to_thinking_log [happy, v0.7.1]: 2x Thinking + WorkerPhase →
|
||||||
thinking_log gets Rule(start) + 2 delta lines + Rule(end); debug_log gets
|
v0.7.1 coalesces "a"+"b" into one buffered string; the close flushes
|
||||||
the worker_phase line; transcript untouched.
|
"ab" as a single line before Rule(end). Result: Rule(start) + "ab" +
|
||||||
|
Rule(end) = 3 writes. debug_log gets worker_phase; transcript untouched.
|
||||||
"""
|
"""
|
||||||
from rich.rule import Rule
|
from rich.rule import Rule
|
||||||
|
|
||||||
@@ -180,12 +203,11 @@ class TestTuiPresenterState:
|
|||||||
raw=False,
|
raw=False,
|
||||||
)
|
)
|
||||||
thinking_writes = [c[0][0] for c in thinking_log.write.call_args_list]
|
thinking_writes = [c[0][0] for c in thinking_log.write.call_args_list]
|
||||||
# 1 Rule(start) + 2 delta lines + 1 Rule(end) = 4 writes
|
# v0.7.1: 1 Rule(start) + 1 coalesced "ab" tail-flush + 1 Rule(end) = 3 writes
|
||||||
assert len(thinking_writes) == 4
|
assert len(thinking_writes) == 3
|
||||||
assert isinstance(thinking_writes[0], Rule)
|
assert isinstance(thinking_writes[0], Rule)
|
||||||
assert thinking_writes[1] == "a"
|
assert thinking_writes[1] == "ab"
|
||||||
assert thinking_writes[2] == "b"
|
assert isinstance(thinking_writes[2], Rule)
|
||||||
assert isinstance(thinking_writes[3], Rule)
|
|
||||||
# worker_phase still goes to debug_log; transcript untouched.
|
# 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 "· worker_phase:" in _text_of(debug_log.write.call_args_list[-1][0][0])
|
||||||
assert not log.write.called
|
assert not log.write.called
|
||||||
|
|||||||
Reference in New Issue
Block a user