diff --git a/src/plyngent/tools/file/edit_lineno.py b/src/plyngent/tools/file/edit_lineno.py index d8f25a7..f366316 100644 --- a/src/plyngent/tools/file/edit_lineno.py +++ b/src/plyngent/tools/file/edit_lineno.py @@ -77,6 +77,14 @@ def _append_after(target: Path, path: str, text: str, lines: list[str], new_cont return f"appended content after line {n} in {path}" +def _excerpt(text: str, limit: int = 40) -> str: + """Collapse newlines and truncate for a one-line message excerpt.""" + one = text.replace("\r\n", " ").replace("\n", " ") + if len(one) > limit: + return one[:limit] + "…" + return one + + def _replace_range( target: Path, path: str, @@ -92,7 +100,11 @@ def _replace_range( new_lines = lines[: start_line - 1] + replacement + lines[end:] _ = target.write_text("".join(new_lines), encoding="utf-8") removed = end - start_line + 1 - return f"replaced lines {start_line}-{end} ({removed} lines) with {len(replacement)} lines in {path}" + old_first = _excerpt(lines[start_line - 1].rstrip("\r\n")) + return ( + f"replaced lines {start_line}-{end} ({removed} lines; first: {old_first!r}) " + f"with {len(replacement)} lines in {path}" + ) @tool(tags=ToolTag.LOCAL | ToolTag.INSTANCE_STATE | ToolTag.YOLO) diff --git a/src/plyngent/tools/file/edit_replace.py b/src/plyngent/tools/file/edit_replace.py index c90e2c0..a791571 100644 --- a/src/plyngent/tools/file/edit_replace.py +++ b/src/plyngent/tools/file/edit_replace.py @@ -18,15 +18,31 @@ def _count_non_overlapping(text: str, needle: str) -> int: start = index + len(needle) -def _success_message(path: str, *, replaced: int, found: int) -> str: +def _excerpt(text: str, limit: int = 40) -> str: + """Collapse newlines and truncate for a one-line message excerpt.""" + one = text.replace("\r\n", " ").replace("\n", " ") + if len(one) > limit: + return one[:limit] + "…" + return one + + +def _success_message( + path: str, + *, + replaced: int, + found: int, + old_string: str, + new_string: str, +) -> str: remaining = found - replaced unit = "occurrence" if replaced == 1 else "occurrences" + change = f" ('{_excerpt(old_string)}' → '{_excerpt(new_string)}')" if remaining == 0: if found == 1: - return f"replaced 1 occurrence in {path}" - return f"replaced {replaced} {unit} in {path} (all {found} matches)" + return f"replaced 1 occurrence in {path}{change}" + return f"replaced {replaced} {unit} in {path}{change} (all {found} matches)" return ( - f"replaced {replaced} {unit} in {path} " + f"replaced {replaced} {unit} in {path}{change} " f"({replaced} of {found} matches; {remaining} remain — " f"raise max_replaces or use a more specific old_string)" ) @@ -56,4 +72,10 @@ async def edit_replace(path: str, old_string: str, new_string: str, max_replaces _ = target.write_text(updated, encoding="utf-8") # The write changes the file's mtime, so edit_lineno's freshness check # rejects stale line numbers until the model re-reads with_lineno. - return _success_message(path, replaced=n, found=found) + return _success_message( + path, + replaced=n, + found=found, + old_string=old_string, + new_string=new_string, + ) diff --git a/tests/test_tools/test_edit_lineno.py b/tests/test_tools/test_edit_lineno.py index e83f2b0..0c55c22 100644 --- a/tests/test_tools/test_edit_lineno.py +++ b/tests/test_tools/test_edit_lineno.py @@ -19,6 +19,7 @@ def test_edit_lineno_replace_middle(workspace: object) -> None: _ = _read_lineno("a.txt") out = call_sync(edit_lineno, "a.txt", 2, 3, "TWO\nTHREE\n") assert "replaced lines 2-3" in out + assert "first: 'two'" in out # old-content excerpt for self-correction assert call_sync(read_file, "a.txt") == "L1-4\none\nTWO\nTHREE\nfour\n" diff --git a/tests/test_tools/test_file.py b/tests/test_tools/test_file.py index 8258eae..17726d2 100644 --- a/tests/test_tools/test_file.py +++ b/tests/test_tools/test_file.py @@ -74,6 +74,7 @@ def test_edit_replace_first_only(workspace: object) -> None: assert call_sync(read_file, "t.txt") == "L1-1\nbb aa" assert "1 of 2" in result or "1 of 2 matches" in result assert "remain" in result + assert "'aa' → 'bb'" in result # old→new excerpt for self-correction def test_edit_replace_max_replaces(workspace: object) -> None: