11"""Tool for listing directory contents."""
22
3- from typing import ClassVar , Union
3+ from typing import ClassVar
44
5- from pydantic import BaseModel , Field
5+ from pydantic import Field
66
77from codegen .sdk .core .codebase import Codebase
88from codegen .sdk .core .directory import Directory
99
1010from .observation import Observation
1111
1212
13- class DirectoryInfo (BaseModel ):
13+ class DirectoryInfo (Observation ):
1414 """Information about a directory."""
1515
16- name : str = Field (description = "Name of the directory" )
17- path : str = Field (description = "Full path to the directory" )
18- files : list [str ] = Field (description = "List of files in this directory" )
19- subdirectories : list [Union [str , "DirectoryInfo" ]] = Field (
20- description = "List of subdirectories (either names or full DirectoryInfo objects depending on depth)" ,
16+ name : str = Field (
17+ description = "Name of the directory" ,
18+ )
19+ path : str = Field (
20+ description = "Full path to the directory" ,
21+ )
22+ files : list [str ] | None = Field (
23+ default = None ,
24+ description = "List of files in this directory (None if at max depth)" ,
25+ )
26+ subdirectories : list ["DirectoryInfo" ] = Field (
27+ default_factory = list ,
28+ description = "List of subdirectories" ,
29+ )
30+ is_leaf : bool = Field (
31+ default = False ,
32+ description = "Whether this is a leaf node (at max depth)" ,
2133 )
2234
35+ str_template : ClassVar [str ] = "Directory {path} ({file_count} files, {dir_count} subdirs)"
36+
37+ def _get_details (self ) -> dict [str , int ]:
38+ """Get details for string representation."""
39+ return {
40+ "file_count" : len (self .files or []),
41+ "dir_count" : len (self .subdirectories ),
42+ }
43+
44+ def render (self ) -> str :
45+ """Render directory listing as a file tree."""
46+ lines = [
47+ f"[LIST DIRECTORY]: { self .path } " ,
48+ "" ,
49+ ]
50+
51+ def add_tree_item (name : str , prefix : str = "" , is_last : bool = False ) -> tuple [str , str ]:
52+ """Helper to format a tree item with proper prefix."""
53+ marker = "└── " if is_last else "├── "
54+ indent = " " if is_last else "│ "
55+ return prefix + marker + name , prefix + indent
56+
57+ def build_tree (items : list [tuple [str , bool , "DirectoryInfo | None" ]], prefix : str = "" ) -> list [str ]:
58+ """Recursively build tree with proper indentation."""
59+ if not items :
60+ return []
61+
62+ result = []
63+ for i , (name , is_dir , dir_info ) in enumerate (items ):
64+ is_last = i == len (items ) - 1
65+ line , new_prefix = add_tree_item (name , prefix , is_last )
66+ result .append (line )
67+
68+ # If this is a directory and not a leaf node, show its contents
69+ if dir_info and not dir_info .is_leaf :
70+ subitems = []
71+ # Add files first
72+ if dir_info .files :
73+ for f in sorted (dir_info .files ):
74+ subitems .append ((f , False , None ))
75+ # Then add subdirectories
76+ for d in dir_info .subdirectories :
77+ subitems .append ((d .name + "/" , True , d ))
78+
79+ result .extend (build_tree (subitems , new_prefix ))
80+
81+ return result
82+
83+ # Sort files and directories
84+ items = []
85+ if self .files :
86+ for f in sorted (self .files ):
87+ items .append ((f , False , None ))
88+ for d in self .subdirectories :
89+ items .append ((d .name + "/" , True , d ))
90+
91+ if not items :
92+ lines .append ("(empty directory)" )
93+ return "\n " .join (lines )
94+
95+ # Generate tree
96+ lines .extend (build_tree (items ))
97+
98+ return "\n " .join (lines )
99+
23100
24101class ListDirectoryObservation (Observation ):
25102 """Response from listing directory contents."""
26103
27- path : str = Field (description = "Path to the listed directory" )
28- directory_info : DirectoryInfo = Field (description = "Information about the directory and its contents" )
29- depth : int = Field (description = "How deep the directory traversal went" )
104+ directory_info : DirectoryInfo = Field (
105+ description = "Information about the directory" ,
106+ )
107+
108+ str_template : ClassVar [str ] = "{directory_info}"
30109
31- str_template : ClassVar [str ] = "Listed contents of {path} (depth={depth})"
110+ def render (self ) -> str :
111+ """Render directory listing."""
112+ return self .directory_info .render ()
32113
33114
34- def list_directory (codebase : Codebase , dirpath : str = "./" , depth : int = 1 ) -> ListDirectoryObservation :
115+ def list_directory (codebase : Codebase , path : str = "./" , depth : int = 2 ) -> ListDirectoryObservation :
35116 """List contents of a directory.
36117
37118 Args:
38119 codebase: The codebase to operate on
39- dirpath : Path to directory relative to workspace root
120+ path : Path to directory relative to workspace root
40121 depth: How deep to traverse the directory tree. Default is 1 (immediate children only).
41122 Use -1 for unlimited depth.
42-
43- Returns:
44- ListDirectoryObservation containing directory contents and metadata
45123 """
46124 try :
47- directory = codebase .get_directory (dirpath )
125+ directory = codebase .get_directory (path )
48126 except ValueError :
49127 return ListDirectoryObservation (
50128 status = "error" ,
51- error = f"Directory not found: { dirpath } " ,
52- path = dirpath ,
129+ error = f"Directory not found: { path } " ,
53130 directory_info = DirectoryInfo (
54- name = "" ,
55- path = dirpath ,
131+ status = "error" ,
132+ name = path .split ("/" )[- 1 ],
133+ path = path ,
56134 files = [],
57135 subdirectories = [],
58136 ),
59- depth = depth ,
60- )
61-
62- if not directory :
63- return ListDirectoryObservation (
64- status = "error" ,
65- error = f"Directory not found: { dirpath } " ,
66- path = dirpath ,
67- directory_info = DirectoryInfo (
68- name = "" ,
69- path = dirpath ,
70- files = [],
71- subdirectories = [],
72- ),
73- depth = depth ,
74137 )
75138
76139 def get_directory_info (dir_obj : Directory , current_depth : int ) -> DirectoryInfo :
77140 """Helper function to get directory info recursively."""
78- # Get direct files
141+ # Get direct files (always include files unless at max depth)
79142 all_files = []
80143 for file in dir_obj .files :
81144 if file .directory == dir_obj :
@@ -86,38 +149,32 @@ def get_directory_info(dir_obj: Directory, current_depth: int) -> DirectoryInfo:
86149 for subdir in dir_obj .subdirectories :
87150 # Only include direct descendants
88151 if subdir .parent == dir_obj :
89- if current_depth != 1 :
152+ if current_depth > 1 or current_depth == - 1 :
153+ # For deeper traversal, get full directory info
90154 new_depth = current_depth - 1 if current_depth > 1 else - 1
91155 subdirs .append (get_directory_info (subdir , new_depth ))
92156 else :
93- # At max depth, just include name
94- subdirs .append (subdir .name )
157+ # At max depth, return a leaf node
158+ subdirs .append (
159+ DirectoryInfo (
160+ status = "success" ,
161+ name = subdir .name ,
162+ path = subdir .dirpath ,
163+ files = None , # Don't include files at max depth
164+ is_leaf = True ,
165+ )
166+ )
95167
96168 return DirectoryInfo (
169+ status = "success" ,
97170 name = dir_obj .name ,
98171 path = dir_obj .dirpath ,
99- files = all_files ,
172+ files = sorted ( all_files ) ,
100173 subdirectories = subdirs ,
101174 )
102175
103- try :
104- directory_info = get_directory_info (directory , depth )
105- return ListDirectoryObservation (
106- status = "success" ,
107- path = dirpath ,
108- directory_info = directory_info ,
109- depth = depth ,
110- )
111- except Exception as e :
112- return ListDirectoryObservation (
113- status = "error" ,
114- error = f"Failed to list directory: { e !s} " ,
115- path = dirpath ,
116- directory_info = DirectoryInfo (
117- name = "" ,
118- path = dirpath ,
119- files = [],
120- subdirectories = [],
121- ),
122- depth = depth ,
123- )
176+ dir_info = get_directory_info (directory , depth )
177+ return ListDirectoryObservation (
178+ status = "success" ,
179+ directory_info = dir_info ,
180+ )
0 commit comments