Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Only write entries that are worth mentioning to users.
## Unreleased

- Kosong: Stop sending an empty `anthropic-beta` header when no beta features are declared — adaptive thinking removes the interleaved-thinking beta, which previously left an empty header value that some backends reject
- Tool: Fix StrReplaceFile reporting the wrong replacement count when edits are chained. The total was counted against the original file content, so a later edit whose target text was produced by an earlier edit was counted as zero; the count now tracks the running content

## 1.49.0 (2026-07-16)

Expand Down
19 changes: 10 additions & 9 deletions src/kimi_cli/tools/file/replace.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,18 @@ 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, counting replacements against the running content.
# Edits apply sequentially, so counting against the original content
# miscounts chained edits (e.g. one edit whose `old` is produced by an
# earlier edit is not found in the original and would be counted 0).
total_replacements = 0
for edit in edits:
before = content
content = self._apply_edit(content, edit)
if edit.replace_all:
total_replacements += before.count(edit.old)
elif edit.old in before:
total_replacements += 1

# Check if any changes were made
if content == original_content:
Expand Down Expand Up @@ -169,14 +178,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="",
Expand Down
25 changes: 25 additions & 0 deletions tests/tools/test_str_replace_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,31 @@ async def test_replace_multiple_edits(
assert await file_path.read_text() == "Hi world! See you world!"


async def test_replace_chained_edits_report_correct_count(
str_replace_file_tool: StrReplaceFile, temp_work_dir: KaosPath
):
"""Chained edits (a later edit's `old` is produced by an earlier one) must
report the true number of replacements, not the count against the original."""
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"
# Both edits replaced text, so the message must say 2 replacements (the old
# code counted "goodbye" against the original and reported 1).
assert "2 total replacement(s)" in result.message


async def test_replace_multiline_content(
str_replace_file_tool: StrReplaceFile, temp_work_dir: KaosPath
):
Expand Down