@@ -3,18 +3,46 @@ package tool
33import (
44 "context"
55 "fmt"
6+ "maps"
67 "strings"
78)
89
910const fileReadMaxLines = 500
1011
12+ // DiffPaths records what this review's diff did to paths that no longer exist
13+ // at the review ref (renames and deletions). A file_read miss on such a path
14+ // is the model following a stale reference from the diff itself (rename
15+ // headers, leftover imports), so it gets redirected or explained instead of
16+ // surfacing a raw git error the model can't act on. Frozen after
17+ // construction; safe for concurrent reads.
18+ type DiffPaths struct {
19+ renamedTo map [string ]string // old path -> new path
20+ deleted map [string ]bool // old path -> removed in this diff
21+ }
22+
23+ // NewDiffPaths creates a frozen DiffPaths from plain maps.
24+ func NewDiffPaths (renamedTo map [string ]string , deleted map [string ]bool ) DiffPaths {
25+ r := make (map [string ]string , len (renamedTo ))
26+ maps .Copy (r , renamedTo )
27+ d := make (map [string ]bool , len (deleted ))
28+ maps .Copy (d , deleted )
29+ return DiffPaths {renamedTo : r , deleted : d }
30+ }
31+
1132// FileReadProvider reads file content at a given path and optional line range.
1233type FileReadProvider struct {
1334 FileReader * FileReader
35+ diffPaths DiffPaths
1436}
1537
1638func NewFileRead (fr * FileReader ) * FileReadProvider { return & FileReadProvider {FileReader : fr } }
1739
40+ // SetDiffPaths installs the rename/delete map for this run. Must be called
41+ // before concurrent access begins (same contract as FileReadDiffProvider.SetDiffMap).
42+ func (p * FileReadProvider ) SetDiffPaths (dp DiffPaths ) {
43+ p .diffPaths = dp
44+ }
45+
1846func (p * FileReadProvider ) Tool () Tool { return FileRead }
1947
2048func (p * FileReadProvider ) Execute (ctx context.Context , args map [string ]any ) (string , error ) {
@@ -44,6 +72,18 @@ func (p *FileReadProvider) Execute(ctx context.Context, args map[string]any) (st
4472 }
4573
4674 lines , totalLines , err := p .FileReader .ReadLines (ctx , filePath , int (startLine ), maxLines )
75+ var renameNote string
76+ if err != nil {
77+ // The miss may be the model chasing a path this very diff moved or
78+ // removed (rename headers and stale imports keep the old path visible).
79+ if to , ok := p .diffPaths .renamedTo [filePath ]; ok {
80+ renameNote = fmt .Sprintf ("NOTE: %q was renamed to %q in this diff; showing the renamed file.\n " , filePath , to )
81+ filePath = to
82+ lines , totalLines , err = p .FileReader .ReadLines (ctx , filePath , int (startLine ), maxLines )
83+ } else if p .diffPaths .deleted [filePath ] {
84+ return fmt .Sprintf ("File %q was deleted in this diff; it no longer exists at the review ref. Use file_read_diff to see the removed content." , filePath ), nil
85+ }
86+ }
4787 if err != nil {
4888 return "" , fmt .Errorf ("file %q not found: %w" , filePath , err )
4989 }
@@ -62,6 +102,7 @@ func (p *FileReadProvider) Execute(ctx context.Context, args map[string]any) (st
62102 displayEnd := int (startLine ) - 1 + len (lines )
63103
64104 var sb strings.Builder
105+ sb .WriteString (renameNote )
65106 sb .WriteString (fmt .Sprintf ("File: %s (Total lines: %d)\n " , filePath , totalLines ))
66107 sb .WriteString (fmt .Sprintf ("IS_TRUNCATED: %t\n " , truncated ))
67108 sb .WriteString (fmt .Sprintf ("LINE_RANGE: %d-%d\n " , int (startLine ), displayEnd ))
0 commit comments