|
2 | 2 |
|
3 | 3 | from pythinker_core.message import ToolCall |
4 | 4 | from pythinker_core.tooling import ToolResult, ToolReturnValue |
5 | | -from rich.console import Console |
| 5 | +from rich.console import Console, Group |
6 | 6 |
|
7 | 7 | from pythinker_code.tools.display import TodoDisplayBlock, TodoDisplayItem |
8 | 8 | from pythinker_code.ui.shell.console import console as shell_console |
9 | 9 | from pythinker_code.ui.shell.keyboard import KeyEvent |
10 | 10 | from pythinker_code.ui.shell.visualize import _live_view as live_view_module |
11 | 11 | from pythinker_code.ui.shell.visualize import _LiveView, _PromptLiveView |
12 | 12 | from pythinker_code.wire.types import ( |
| 13 | + CompactionBegin, |
13 | 14 | HookOutput, |
14 | 15 | HookResolved, |
15 | 16 | HookTriggered, |
@@ -318,20 +319,32 @@ def test_prompt_live_view_keeps_non_background_task_notifications(monkeypatch): |
318 | 319 |
|
319 | 320 | def test_live_view_prints_turn_recap_when_enabled(monkeypatch): |
320 | 321 | printed = [] |
321 | | - monkeypatch.setattr( |
322 | | - live_view_module.console, "print", lambda *args, **_kwargs: printed.extend(args) |
323 | | - ) |
| 322 | + |
| 323 | + def fake_print(*args, **_kwargs): |
| 324 | + printed.append(args[0] if args else None) |
| 325 | + |
| 326 | + monkeypatch.setattr(live_view_module.console, "print", fake_print) |
324 | 327 |
|
325 | 328 | view = _LiveView(StatusUpdate(), show_turn_recaps=True) |
326 | 329 | view.dispatch_wire_message(TurnBegin(user_input="implement recaps")) |
327 | | - view.dispatch_wire_message(TextPart(text="Implemented a /recap command.")) |
| 330 | + view.dispatch_wire_message(TextPart(text="Implemented a ")) |
| 331 | + view.dispatch_wire_message(TextPart(text="/recap command.")) |
328 | 332 | view.dispatch_wire_message(TurnEnd()) |
329 | 333 | view.cleanup(is_interrupt=False) |
330 | 334 |
|
331 | | - plain = "\n".join(getattr(item, "plain", str(item)) for item in printed) |
| 335 | + plain = "\n".join(getattr(item, "plain", str(item)) for item in printed if item is not None) |
332 | 336 | assert "※ recap: Implemented a /recap command." in plain |
| 337 | + assert "Implemented a /recap" not in plain |
333 | 338 | assert "disable recaps in /settings" in plain |
334 | 339 |
|
| 340 | + recap_index = next( |
| 341 | + index |
| 342 | + for index, item in enumerate(printed) |
| 343 | + if item is not None and "※ recap:" in getattr(item, "plain", str(item)) |
| 344 | + ) |
| 345 | + assert printed[recap_index - 1] is None |
| 346 | + assert printed[recap_index + 1] is None |
| 347 | + |
335 | 348 |
|
336 | 349 | def test_cleanup_flushes_notifications_to_terminal_history(monkeypatch): |
337 | 350 | view = _LiveView(StatusUpdate()) |
@@ -371,6 +384,15 @@ def test_cleanup_flushes_all_notifications_even_when_live_view_shows_only_latest |
371 | 384 | assert f"Background task completed: build project {index}" in rendered |
372 | 385 |
|
373 | 386 |
|
| 387 | +def test_compaction_status_keeps_one_blank_row_above_live_block(): |
| 388 | + view = _LiveView(StatusUpdate(context_tokens=221_300)) |
| 389 | + |
| 390 | + view.dispatch_wire_message(CompactionBegin()) |
| 391 | + |
| 392 | + rendered = _render(Group(*view.compose_agent_output(include_working_indicator=False))) |
| 393 | + assert rendered.startswith("\n· Compacting conversation…") |
| 394 | + |
| 395 | + |
374 | 396 | def test_compose_inserts_gap_under_agent_output_before_nonempty_status(): |
375 | 397 | """Non-interactive compose() puts one blank row under the spinner verb |
376 | 398 | before a non-empty status line (the under-gap), and the status line stays last.""" |
|
0 commit comments