@@ -2,8 +2,11 @@ package chattool
22
33import (
44 "context"
5+ "fmt"
6+ "strings"
57
68 "charm.land/fantasy"
9+ "golang.org/x/xerrors"
710
811 "github.com/coder/coder/v2/codersdk/workspacesdk"
912)
@@ -22,9 +25,10 @@ func ReadFile(options ReadFileOptions) fantasy.AgentTool {
2225 return fantasy .NewAgentTool (
2326 "read_file" ,
2427 "Read a file from the workspace. Returns line-numbered content. " +
25- "The offset parameter is a 1-based line number (default: 1). " +
26- "The limit parameter is the number of lines to return (default: 2000). " +
27- "For large files, use offset and limit to paginate." ,
28+ "When reading a directory, returns a non-recursive directory listing. " +
29+ "The offset parameter is a 1-based line number or directory entry (default: 1). " +
30+ "The limit parameter is the number of lines or directory entries to return (default: 2000). " +
31+ "For large files and directories, use offset and limit to paginate." ,
2832 func (ctx context.Context , args ReadFileArgs , _ fantasy.ToolCall ) (fantasy.ToolResponse , error ) {
2933 if options .GetWorkspaceConn == nil {
3034 return fantasy .NewTextErrorResponse ("workspace connection resolver is not configured" ), nil
@@ -62,6 +66,9 @@ func executeReadFileTool(
6266 }
6367
6468 if ! resp .Success {
69+ if readFileLinesErrorIsDirectory (resp .Error ) {
70+ return executeReadFileDirectoryListing (ctx , conn , args , offset , limit , resp .Error )
71+ }
6572 return fantasy .NewTextErrorResponse (resp .Error ), nil
6673 }
6774
@@ -72,3 +79,101 @@ func executeReadFileTool(
7279 "lines_read" : resp .LinesRead ,
7380 }), nil
7481}
82+
83+ func readFileLinesErrorIsDirectory (err string ) bool {
84+ return strings .HasPrefix (strings .TrimSpace (err ), "not a file:" )
85+ }
86+
87+ func executeReadFileDirectoryListing (
88+ ctx context.Context ,
89+ conn workspacesdk.AgentConn ,
90+ args ReadFileArgs ,
91+ offset int64 ,
92+ limit int64 ,
93+ readErr string ,
94+ ) (fantasy.ToolResponse , error ) {
95+ resp , err := conn .LS (ctx , args .Path , workspacesdk.LSRequest {
96+ Relativity : workspacesdk .LSRelativityRoot ,
97+ })
98+ if err != nil {
99+ return fantasy .NewTextErrorResponse (
100+ fmt .Sprintf ("%s; failed to list directory: %s" , readErr , err ),
101+ ), nil
102+ }
103+
104+ listing , err := directoryListingResult (resp , offset , limit )
105+ if err != nil {
106+ return fantasy .NewTextErrorResponse (err .Error ()), nil
107+ }
108+
109+ return toolResponse (map [string ]any {
110+ "content" : listing .content ,
111+ "is_directory" : true ,
112+ "absolute_path" : resp .AbsolutePath ,
113+ "absolute_path_string" : resp .AbsolutePathString ,
114+ "entries" : listing .entries ,
115+ "entries_read" : listing .entriesRead ,
116+ "total_entries" : len (resp .Contents ),
117+ "truncated" : listing .truncated ,
118+ }), nil
119+ }
120+
121+ type directoryListing struct {
122+ content string
123+ entries []workspacesdk.LSFile
124+ entriesRead int
125+ truncated bool
126+ }
127+
128+ func directoryListingResult (resp workspacesdk.LSResponse , offset , limit int64 ) (directoryListing , error ) {
129+ if offset < 1 {
130+ offset = 1
131+ }
132+ if limit <= 0 {
133+ limit = workspacesdk .DefaultMaxResponseLines
134+ }
135+
136+ totalEntries := len (resp .Contents )
137+ if totalEntries == 0 {
138+ return directoryListing {}, nil
139+ }
140+ if offset > int64 (totalEntries ) {
141+ return directoryListing {}, xerrors .Errorf ("offset %d is beyond the directory length of %d entries" , offset , totalEntries )
142+ }
143+
144+ start := int (offset - 1 )
145+ remaining := totalEntries - start
146+ entriesToRead := remaining
147+ if limit < int64 (remaining ) {
148+ entriesToRead = int (limit )
149+ }
150+ end := start + entriesToRead
151+
152+ content , entriesRead , byteTruncated := formatDirectoryListing (
153+ resp .Contents [start :end ],
154+ offset ,
155+ int (workspacesdk .DefaultMaxResponseBytes ),
156+ )
157+ return directoryListing {
158+ content : content ,
159+ entries : resp .Contents [start : start + entriesRead ],
160+ entriesRead : entriesRead ,
161+ truncated : byteTruncated || start + entriesRead < totalEntries ,
162+ }, nil
163+ }
164+
165+ func formatDirectoryListing (entries []workspacesdk.LSFile , offset int64 , maxBytes int ) (string , int , bool ) {
166+ var b strings.Builder
167+ for i , entry := range entries {
168+ name := entry .Name
169+ if entry .IsDir {
170+ name += "/"
171+ }
172+ line := fmt .Sprintf ("%d\t %s\n " , offset + int64 (i ), name )
173+ if b .Len ()+ len (line ) > maxBytes {
174+ return b .String (), i , true
175+ }
176+ _ , _ = b .WriteString (line )
177+ }
178+ return b .String (), len (entries ), false
179+ }
0 commit comments