11import { parseJSON } from "partial-json"
22
3- import { type ToolName , toolNames } from "@roo-code/types"
3+ import { type ToolName , toolNames , type FileEntry } from "@roo-code/types"
44import { customToolRegistry } from "@roo-code/core"
55
66import {
@@ -326,6 +326,47 @@ export class NativeToolCallParser {
326326 return undefined
327327 }
328328
329+ /**
330+ * Convert raw file entries from API (with line_ranges) to FileEntry objects
331+ * (with lineRanges). Handles multiple formats for backward compatibility:
332+ *
333+ * New tuple format: { path: string, line_ranges: [[1, 50], [100, 150]] }
334+ * Object format: { path: string, line_ranges: [{ start: 1, end: 50 }] }
335+ * Legacy string format: { path: string, line_ranges: ["1-50"] }
336+ *
337+ * Returns: { path: string, lineRanges: [{ start: 1, end: 50 }] }
338+ */
339+ private static convertFileEntries ( files : unknown [ ] ) : FileEntry [ ] {
340+ return files . map ( ( file : unknown ) => {
341+ const f = file as Record < string , unknown >
342+ const entry : FileEntry = { path : f . path as string }
343+ if ( f . line_ranges && Array . isArray ( f . line_ranges ) ) {
344+ entry . lineRanges = ( f . line_ranges as unknown [ ] )
345+ . map ( ( range : unknown ) => {
346+ // Handle tuple format: [start, end]
347+ if ( Array . isArray ( range ) && range . length >= 2 ) {
348+ return { start : Number ( range [ 0 ] ) , end : Number ( range [ 1 ] ) }
349+ }
350+ // Handle object format: { start: number, end: number }
351+ if ( typeof range === "object" && range !== null && "start" in range && "end" in range ) {
352+ const r = range as { start : unknown ; end : unknown }
353+ return { start : Number ( r . start ) , end : Number ( r . end ) }
354+ }
355+ // Handle legacy string format: "1-50"
356+ if ( typeof range === "string" ) {
357+ const match = range . match ( / ^ ( \d + ) - ( \d + ) $ / )
358+ if ( match ) {
359+ return { start : parseInt ( match [ 1 ] , 10 ) , end : parseInt ( match [ 2 ] , 10 ) }
360+ }
361+ }
362+ return null
363+ } )
364+ . filter ( ( r ) : r is { start : number ; end : number } => r !== null )
365+ }
366+ return entry
367+ } )
368+ }
369+
329370 /**
330371 * Create a partial ToolUse from currently parsed arguments.
331372 * Used during streaming to show progress.
@@ -352,9 +393,40 @@ export class NativeToolCallParser {
352393 // Build partial nativeArgs based on what we have so far
353394 let nativeArgs : any = undefined
354395
396+ // Track if legacy format was used (for telemetry)
397+ let usedLegacyFormat = false
398+
355399 switch ( name ) {
356400 case "read_file" :
357- if ( partialArgs . path !== undefined ) {
401+ // Check for legacy format first: { files: [...] }
402+ // Handle both array and stringified array (some models double-stringify)
403+ if ( partialArgs . files !== undefined ) {
404+ let filesArray : unknown [ ] | null = null
405+
406+ if ( Array . isArray ( partialArgs . files ) ) {
407+ filesArray = partialArgs . files
408+ } else if ( typeof partialArgs . files === "string" ) {
409+ // Handle double-stringified case: files is a string containing JSON array
410+ try {
411+ const parsed = JSON . parse ( partialArgs . files )
412+ if ( Array . isArray ( parsed ) ) {
413+ filesArray = parsed
414+ }
415+ } catch {
416+ // Not valid JSON, ignore
417+ }
418+ }
419+
420+ if ( filesArray && filesArray . length > 0 ) {
421+ usedLegacyFormat = true
422+ nativeArgs = {
423+ files : this . convertFileEntries ( filesArray ) ,
424+ _legacyFormat : true as const ,
425+ }
426+ }
427+ }
428+ // New format: { path: "...", mode: "..." }
429+ if ( ! nativeArgs && partialArgs . path !== undefined ) {
358430 nativeArgs = {
359431 path : partialArgs . path ,
360432 mode : partialArgs . mode ,
@@ -589,6 +661,11 @@ export class NativeToolCallParser {
589661 result . originalName = originalName
590662 }
591663
664+ // Track legacy format usage for telemetry
665+ if ( usedLegacyFormat ) {
666+ result . usedLegacyFormat = true
667+ }
668+
592669 return result
593670 }
594671
@@ -652,9 +729,40 @@ export class NativeToolCallParser {
652729 // nativeArgs object. If validation fails, we treat the tool call as invalid and fail fast.
653730 let nativeArgs : NativeArgsFor < TName > | undefined = undefined
654731
732+ // Track if legacy format was used (for telemetry)
733+ let usedLegacyFormat = false
734+
655735 switch ( resolvedName ) {
656736 case "read_file" :
657- if ( args . path !== undefined ) {
737+ // Check for legacy format first: { files: [...] }
738+ // Handle both array and stringified array (some models double-stringify)
739+ if ( args . files !== undefined ) {
740+ let filesArray : unknown [ ] | null = null
741+
742+ if ( Array . isArray ( args . files ) ) {
743+ filesArray = args . files
744+ } else if ( typeof args . files === "string" ) {
745+ // Handle double-stringified case: files is a string containing JSON array
746+ try {
747+ const parsed = JSON . parse ( args . files )
748+ if ( Array . isArray ( parsed ) ) {
749+ filesArray = parsed
750+ }
751+ } catch {
752+ // Not valid JSON, ignore
753+ }
754+ }
755+
756+ if ( filesArray && filesArray . length > 0 ) {
757+ usedLegacyFormat = true
758+ nativeArgs = {
759+ files : this . convertFileEntries ( filesArray ) ,
760+ _legacyFormat : true as const ,
761+ } as NativeArgsFor < TName >
762+ }
763+ }
764+ // New format: { path: "...", mode: "..." }
765+ if ( ! nativeArgs && args . path !== undefined ) {
658766 nativeArgs = {
659767 path : args . path ,
660768 mode : args . mode ,
@@ -921,6 +1029,11 @@ export class NativeToolCallParser {
9211029 result . originalName = toolCall . name
9221030 }
9231031
1032+ // Track legacy format usage for telemetry
1033+ if ( usedLegacyFormat ) {
1034+ result . usedLegacyFormat = true
1035+ }
1036+
9241037 return result
9251038 } catch ( error ) {
9261039 console . error (
0 commit comments