diff --git a/src/kimi_cli/tools/file/replace.py b/src/kimi_cli/tools/file/replace.py index 4f551de4f4..00a17f202e 100644 --- a/src/kimi_cli/tools/file/replace.py +++ b/src/kimi_cli/tools/file/replace.py @@ -134,8 +134,14 @@ async def __call__(self, params: Params) -> ToolReturnValue: original_content = content edits = [params.edit] if isinstance(params.edit, Edit) else params.edit - # Apply all edits + # Apply all edits and count replacements against the content as it + # exists before each edit (fixes under-counting for chained edits). + total_replacements = 0 for edit in edits: + if edit.replace_all: + total_replacements += content.count(edit.old) + else: + total_replacements += 1 if edit.old in content else 0 content = self._apply_edit(content, edit) # Check if any changes were made @@ -169,14 +175,6 @@ async def __call__(self, params: Params) -> ToolReturnValue: # Write the modified content back to the file await p.write_text(content, errors="replace") - # Count changes for success message - total_replacements = 0 - for edit in edits: - if edit.replace_all: - total_replacements += original_content.count(edit.old) - else: - total_replacements += 1 if edit.old in original_content else 0 - return ToolReturnValue( is_error=False, output="", diff --git a/tests/tools/test_str_replace_file.py b/tests/tools/test_str_replace_file.py index a16dad303b..fb16347985 100644 --- a/tests/tools/test_str_replace_file.py +++ b/tests/tools/test_str_replace_file.py @@ -75,6 +75,29 @@ async def test_replace_multiple_edits( assert await file_path.read_text() == "Hi world! See you world!" +async def test_replace_chained_edits_count( + str_replace_file_tool: StrReplaceFile, temp_work_dir: KaosPath +): + """Regression test for #2526: chained edits must count against intermediate content.""" + file_path = temp_work_dir / "test.txt" + await file_path.write_text("hello world") + + result = await str_replace_file_tool( + Params( + path=str(file_path), + edit=[ + Edit(old="hello", new="goodbye"), + Edit(old="goodbye", new="farewell"), + ], + ) + ) + + assert not result.is_error + assert await file_path.read_text() == "farewell world" + # The bug reported 1 replacement; the fix should report 2. + assert "2 total replacement" in result.message + + async def test_replace_multiline_content( str_replace_file_tool: StrReplaceFile, temp_work_dir: KaosPath ):