@@ -27,17 +27,42 @@ function optionalInt(value: unknown): number | undefined {
2727 : undefined ;
2828}
2929
30+ // Models pad the unused mode's fields with fillers (old_string: "", start_line: 0).
31+ // For mode selection a filler counts as absent: "" is never a valid old_string and
32+ // 0/null/non-integers are never valid 1-based lines. Treating them as present made
33+ // filler-padded calls look like "both edit modes" and rejected them (CL-6900).
3034function hasOldStringArg ( args : Record < string , unknown > ) : boolean {
31- return typeof args . old_string === "string" ;
35+ return typeof args . old_string === "string" && args . old_string . length > 0 ;
36+ }
37+
38+ function validLineNumber ( value : unknown ) : number | undefined {
39+ const n = optionalInt ( value ) ;
40+ return n !== undefined && n >= 1 ? n : undefined ;
3241}
3342
3443function hasLineRangeArgs ( args : Record < string , unknown > ) : boolean {
35- return optionalInt ( args . start_line ) !== undefined || optionalInt ( args . end_line ) !== undefined ;
44+ return (
45+ validLineNumber ( args . start_line ) !== undefined || validLineNumber ( args . end_line ) !== undefined
46+ ) ;
47+ }
48+
49+ function describeReceived ( args : Record < string , unknown > ) : string {
50+ const old = args . old_string ;
51+ return [
52+ typeof old === "string" ? `old_string (len ${ old . length } )` : "no old_string" ,
53+ args . start_line === undefined
54+ ? "no start_line"
55+ : `start_line=${ JSON . stringify ( args . start_line ) } ` ,
56+ args . end_line === undefined ? "no end_line" : `end_line=${ JSON . stringify ( args . end_line ) } ` ,
57+ ] . join ( ", " ) ;
3658}
3759
38- const MIXED_MODE_MESSAGE =
39- "edit_file: received both old_string and start_line/end_line; only one edit mode is allowed. " +
40- "Omit old_string to use line-range mode, or omit start_line/end_line to use substring mode." ;
60+ function mixedModeMessage ( args : Record < string , unknown > ) : string {
61+ return (
62+ `edit_file: received ${ describeReceived ( args ) } ; only one edit mode is allowed. ` +
63+ "Omit old_string to use line-range mode, or omit start_line/end_line to use substring mode."
64+ ) ;
65+ }
4166
4267export function parseLineRangeFields (
4368 path : string ,
@@ -88,30 +113,30 @@ export function parseEditFileMode(args: Record<string, unknown>): EditFileModePa
88113 // slice, producing a confusing "old_string does not match" error even when the caller
89114 // meant plain substring mode (CL-4399). One explicit error beats a wrong guess.
90115 if ( substring && lineRange ) {
91- return { kind : "invalid" , message : MIXED_MODE_MESSAGE } ;
116+ return { kind : "invalid" , message : mixedModeMessage ( args ) } ;
92117 }
93118
94119 if ( lineRange ) {
95120 return parseLineRangeFields ( path , new_string , args ) ;
96121 }
97122
98123 if ( ! substring ) {
124+ const emptyOldString = typeof args . old_string === "string" ;
99125 return {
100126 kind : "invalid" ,
101- message :
102- "edit_file requires old_string (substring mode) or start_line and end_line (line-range mode)" ,
127+ message : emptyOldString
128+ ? "edit_file: old_string is empty; provide the exact text to replace (substring mode), " +
129+ "or omit it and send start_line/end_line >= 1 (line-range mode). " +
130+ `Received ${ describeReceived ( args ) } .`
131+ : "edit_file requires old_string (substring mode) or start_line and end_line (line-range mode); " +
132+ `received ${ describeReceived ( args ) } ` ,
103133 } ;
104134 }
105135
106- const old_string = String ( args . old_string ) ;
107- if ( old_string . length === 0 ) {
108- return { kind : "invalid" , message : "old_string must not be empty" } ;
109- }
110-
111136 return {
112137 kind : "substring" ,
113138 path,
114- old_string,
139+ old_string : String ( args . old_string ) ,
115140 new_string,
116141 replace_all : Boolean ( args . replace_all ) ,
117142 } ;
0 commit comments