Skip to content
This repository was archived by the owner on Sep 23, 2026. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
fix(tools): count-dominant line endings in StrReplaceFile
Detect CRLF vs LF by occurrence count instead of "any CRLF wins", so a
mostly-LF file with a stray CRLF still matches model-supplied multi-line
edits. Addresses Copilot feedback on mixed-newline files.
  • Loading branch information
686f6c61 committed Aug 7, 2026
commit 169363af5653071e7b163b677268c8fc907a2b99
11 changes: 9 additions & 2 deletions src/kimi_cli/tools/file/replace.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,13 +87,20 @@ def _apply_edit(self, content: str, edit: Edit) -> str:

@staticmethod
def _detect_line_ending(content: bytes) -> bytes:
"""Return the file's dominant newline bytes (CRLF if any, else LF).
"""Return the file's dominant newline bytes by counting occurrences.

ReadFile exposes lines with universal newlines, so the model always
supplies ``\\n`` in multi-line ``old``/``new``. Byte matching must
re-apply the on-disk ending or CRLF files reject every multi-line edit.

Dominance is by count (not “any CRLF wins”): a mostly-LF file with a
stray ``\\r\\n`` keeps LF so multi-line ``old`` still matches.
Ties prefer LF.
"""
if b"\r\n" in content:
crlf = content.count(b"\r\n")
# Newlines that are not the second byte of a CRLF pair.
lf_only = content.count(b"\n") - crlf
if crlf > lf_only:
return b"\r\n"
return b"\n"

Expand Down
21 changes: 21 additions & 0 deletions tests/tools/test_str_replace_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,6 +304,27 @@ async def test_replace_multiline_crlf_file(
assert out.startswith(b"Line 1\r\n")


async def test_replace_multiline_mostly_lf_with_stray_crlf(
str_replace_file_tool: StrReplaceFile, temp_work_dir: KaosPath
):
"""A stray CRLF must not force whole-file CRLF rewriting of old/new (Copilot)."""
file_path = temp_work_dir / "mostly-lf.txt"
# Three LF newlines dominate a single CRLF elsewhere in the file.
original = b"Line 1\nLine 2\nLine 3\ntrailer\r\n"
await file_path.write_bytes(original)

result = await str_replace_file_tool(
Params(
path=str(file_path),
edit=Edit(old="Line 2\nLine 3", new="Modified 2\nModified 3"),
)
)

assert not result.is_error
out = await file_path.read_bytes()
assert out == b"Line 1\nModified 2\nModified 3\ntrailer\r\n"


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