mirror of
https://github.com/NCBM/plyngent.git
synced 2026-11-07 15:37:55 +08:00
cli/display: keep the line above an erased tool prefix
Erasing the in-flight prefix reused a helper that cleared one row more than requested, so a parallel tool batch also erased the line above the prefix - the streamed reasoning text that sits there when a model reasons and then calls tools. The helper now clears exactly the requested rows and takes explicit control of where the cursor lands; the parallel fallback erases its own line and lets the leading newline of the whole-line write reuse it. Row counts are computed from physical terminal rows instead of logical newlines, so an assistant segment wider than the terminal is fully erased before the markdown re-render rather than leaving a stray copy behind. Tests: a byte-level regression for reasoning before a parallel batch, unit coverage for the row-counting and erase primitives, and a wrapped-line flush.
This commit is contained in:
@@ -4,6 +4,8 @@ from typing import TYPE_CHECKING
|
||||
|
||||
from plyngent.agent import ErrorEvent, ReasoningDeltaEvent, TextDeltaEvent, ToolCallEvent, ToolResultEvent
|
||||
from plyngent.cli.display import (
|
||||
_clear_streamed_lines,
|
||||
_line_count_for_clear,
|
||||
get_markdown_enabled,
|
||||
markdown_render_available,
|
||||
print_markdown,
|
||||
@@ -637,6 +639,93 @@ async def test_pretty_parallel_batch_erases_open_prefix(
|
||||
assert "* Read 'b.txt' L1-9 (done)\n" in out
|
||||
|
||||
|
||||
async def test_pretty_parallel_batch_keeps_the_line_above_the_prefix(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
) -> None:
|
||||
"""Regression: erasing the open prefix must not eat the line above it.
|
||||
|
||||
Reasoning streams without a trailing newline, so the prefix line sits right
|
||||
below the reasoning text; the erase used to clear one row too many and wiped
|
||||
that reasoning line out of the screen.
|
||||
"""
|
||||
monkeypatch.setattr("plyngent.cli.display.interactive_terminal", lambda: True)
|
||||
await render_events(
|
||||
_aiter(
|
||||
[
|
||||
ReasoningDeltaEvent(content="think"),
|
||||
_pretty_call("read_file", '{"path": "a.txt"}'),
|
||||
_pretty_call("read_file", '{"path": "b.txt"}'),
|
||||
_result("L1-4\none\n"),
|
||||
_result("L1-9\ntwo\n"),
|
||||
]
|
||||
)
|
||||
)
|
||||
out = capsys.readouterr().out
|
||||
assert out == (
|
||||
"\nreasoning:\nthink"
|
||||
"\n* Read 'a.txt' "
|
||||
"\r\x1b[2K\x1b[1A"
|
||||
"\n* Read 'a.txt' L1-4 (done)\n"
|
||||
"\n* Read 'b.txt' L1-9 (done)\n"
|
||||
"\n\n"
|
||||
)
|
||||
|
||||
|
||||
def test_clear_streamed_lines_clears_exactly_the_given_rows(capsys: pytest.CaptureFixture[str]) -> None:
|
||||
"""Two rows means two clears; the cursor lands on the topmost cleared row."""
|
||||
_clear_streamed_lines(2)
|
||||
assert capsys.readouterr().out == "\r\x1b[2K\x1b[1A\r\x1b[2K"
|
||||
|
||||
|
||||
def test_clear_streamed_lines_resume_above_steps_back_one_row(capsys: pytest.CaptureFixture[str]) -> None:
|
||||
"""``resume_above`` clears the cursor row only, then steps above it."""
|
||||
_clear_streamed_lines(1, resume_above=True)
|
||||
assert capsys.readouterr().out == "\r\x1b[2K\x1b[1A"
|
||||
|
||||
|
||||
def test_clear_streamed_lines_ignores_non_positive_counts(capsys: pytest.CaptureFixture[str]) -> None:
|
||||
_clear_streamed_lines(0)
|
||||
_clear_streamed_lines(-1)
|
||||
assert capsys.readouterr().out == ""
|
||||
|
||||
|
||||
def test_line_count_for_clear_counts_wrapped_rows(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
"""A streamed line wider than the terminal owns more than one row."""
|
||||
monkeypatch.setenv("COLUMNS", "20")
|
||||
assert _line_count_for_clear("assistant:", "x" * 20) == 2 # label + full row
|
||||
assert _line_count_for_clear("assistant:", "x" * 21) == 3 # label + two rows
|
||||
assert _line_count_for_clear("assistant:", "a\nb") == 3
|
||||
assert _line_count_for_clear("", "") == 0
|
||||
|
||||
|
||||
async def test_flush_markdown_erases_wrapped_assistant_rows(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
) -> None:
|
||||
"""A wrapped assistant segment is fully erased before the markdown re-render."""
|
||||
monkeypatch.setattr("plyngent.cli.display.interactive_terminal", lambda: True)
|
||||
monkeypatch.setattr("plyngent.cli.display.markdown_render_available", lambda: True)
|
||||
monkeypatch.setenv("COLUMNS", "20")
|
||||
set_markdown_enabled(True)
|
||||
try:
|
||||
await render_events(
|
||||
_aiter(
|
||||
[
|
||||
TextDeltaEvent(content="x" * 21),
|
||||
_pretty_call("read_file", '{"path": "a.txt"}'),
|
||||
_result("L1-4\none\n"),
|
||||
]
|
||||
)
|
||||
)
|
||||
finally:
|
||||
set_markdown_enabled(True)
|
||||
out = capsys.readouterr().out
|
||||
# label + two wrapped body rows + the blank separator above the label.
|
||||
assert out.count("\x1b[2K") == 4
|
||||
assert out.count("\x1b[1A") == 3
|
||||
|
||||
|
||||
async def test_pretty_prefix_closed_before_error_line(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
capsys: pytest.CaptureFixture[str],
|
||||
|
||||
Reference in New Issue
Block a user