The uniqueness guard counted occurrences via normalize_for_fuzzy_match,
while fuzzy_find_text located matches with a whitespace-flexible regex,
so the two could disagree. Extract the pattern builder as a single
source of truth (_build_fuzzy_pattern) and add count_matches, which
counts with the same exact-then-fuzzy strategy used to locate and
replace. This is the optional follow-up suggested in the review of #2942.
Adds regression tests for exact and fuzzy multi-match rejection.
The fuzzy fallback anchored every line with a leading [ \t]* that
greedily consumed the file's indentation into the matched region, so a
no-indent oldText dropped the edited line's indentation. Fold leading
whitespace into the match only when oldText was itself indented on the
first line, mirroring exact-substring semantics. Add a regression test.
The Read tool documents that a negative offset reads from the end (-N = last N lines). Content is split on newline, so a file ending in a newline produces a trailing empty element and total_file_lines is one too high. Every negative offset was therefore off by one: offset=-1 returned the empty string after the final newline instead of the last line, and -N returned N-1 real lines.
Exclude the trailing empty element when computing the start line for negative offsets. Adds regression tests.
Co-authored-by: Cursor <cursoragent@cursor.com>
The Edit tool falls back to a whitespace-tolerant fuzzy match when oldText does not match byte-for-byte. On a fuzzy hit it replaced text inside a whitespace-normalized copy of the whole file and wrote that copy back, so every untouched line lost its original indentation (runs of spaces/tabs collapsed to a single space). For indentation-sensitive files such as Python this silently corrupts the file.
Locate the fuzzy match in the original content with a whitespace-flexible regex and return offsets into that original content, so only the matched region is replaced. Adds regression tests.
Co-authored-by: Cursor <cursoragent@cursor.com>
When a command's output exceeds DEFAULT_MAX_BYTES the Bash tool spills the full output to a temp file. The file was opened in text mode without an explicit encoding, so it used the platform locale encoding (cp936/GBK on Chinese Windows). Output containing emoji or other characters not representable in that codepage raised UnicodeEncodeError, which propagated out and turned a successful command (exit code 0) into a tool error, discarding all output.
Open the temp file with encoding='utf-8', matching the sibling temp file written in _rewrite_long_python_c. Adds a regression test.
Co-authored-by: Cursor <cursoragent@cursor.com>