Skip to content

Commit 45cefbd

Browse files
committed
fix(tools): count StrReplaceFile replacements against the running content
StrReplaceFile applies its edits sequentially but computed the reported replacement count against the original file content. A chained edit whose `old` string is produced by an earlier edit is not present in the original, so it was counted as zero: editing "hello" -> "goodbye" then "goodbye" -> "farewell" reported "1 total replacement(s)" instead of 2. Count each edit against the content it is actually applied to, and add a regression test.
1 parent cbc15c0 commit 45cefbd

3 files changed

Lines changed: 36 additions & 9 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ Only write entries that are worth mentioning to users.
1212
## Unreleased
1313

1414
- 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
15+
- 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
1516

1617
## 1.49.0 (2026-07-16)
1718

src/kimi_cli/tools/file/replace.py

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -134,9 +134,18 @@ async def __call__(self, params: Params) -> ToolReturnValue:
134134
original_content = content
135135
edits = [params.edit] if isinstance(params.edit, Edit) else params.edit
136136

137-
# Apply all edits
137+
# Apply all edits, counting replacements against the running content.
138+
# Edits apply sequentially, so counting against the original content
139+
# miscounts chained edits (e.g. one edit whose `old` is produced by an
140+
# earlier edit is not found in the original and would be counted 0).
141+
total_replacements = 0
138142
for edit in edits:
143+
before = content
139144
content = self._apply_edit(content, edit)
145+
if edit.replace_all:
146+
total_replacements += before.count(edit.old)
147+
elif edit.old in before:
148+
total_replacements += 1
140149

141150
# Check if any changes were made
142151
if content == original_content:
@@ -169,14 +178,6 @@ async def __call__(self, params: Params) -> ToolReturnValue:
169178
# Write the modified content back to the file
170179
await p.write_text(content, errors="replace")
171180

172-
# Count changes for success message
173-
total_replacements = 0
174-
for edit in edits:
175-
if edit.replace_all:
176-
total_replacements += original_content.count(edit.old)
177-
else:
178-
total_replacements += 1 if edit.old in original_content else 0
179-
180181
return ToolReturnValue(
181182
is_error=False,
182183
output="",

tests/tools/test_str_replace_file.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,31 @@ async def test_replace_multiple_edits(
7575
assert await file_path.read_text() == "Hi world! See you world!"
7676

7777

78+
async def test_replace_chained_edits_report_correct_count(
79+
str_replace_file_tool: StrReplaceFile, temp_work_dir: KaosPath
80+
):
81+
"""Chained edits (a later edit's `old` is produced by an earlier one) must
82+
report the true number of replacements, not the count against the original."""
83+
file_path = temp_work_dir / "test.txt"
84+
await file_path.write_text("hello world")
85+
86+
result = await str_replace_file_tool(
87+
Params(
88+
path=str(file_path),
89+
edit=[
90+
Edit(old="hello", new="goodbye"),
91+
Edit(old="goodbye", new="farewell"),
92+
],
93+
)
94+
)
95+
96+
assert not result.is_error
97+
assert await file_path.read_text() == "farewell world"
98+
# Both edits replaced text, so the message must say 2 replacements (the old
99+
# code counted "goodbye" against the original and reported 1).
100+
assert "2 total replacement(s)" in result.message
101+
102+
78103
async def test_replace_multiline_content(
79104
str_replace_file_tool: StrReplaceFile, temp_work_dir: KaosPath
80105
):

0 commit comments

Comments
 (0)