@@ -64,34 +64,218 @@ def run_command(command: str) -> str:
6464 return f"Error executing command: { str (e )} "
6565
6666
67- def get_workspace_summary () -> str :
68- """Gathers git context, branch, status, recent commits, and project documentation (like README.md or rule files) to help the agent understand the whole project."""
69- summary_parts = []
67+ # ---------------------------------------------------------------------------
68+ # Dedicated directory listing tool
69+ # ---------------------------------------------------------------------------
70+
71+ # Directories to always skip when walking the tree
72+ _IGNORE_DIRS = {
73+ ".git" ,
74+ "__pycache__" ,
75+ "node_modules" ,
76+ ".venv" ,
77+ "venv" ,
78+ "env" ,
79+ ".ruff_cache" ,
80+ "dist" ,
81+ "build" ,
82+ ".next" ,
83+ ".cache" ,
84+ ".tox" ,
85+ ".mypy_cache" ,
86+ ".pytest_cache" ,
87+ "*.egg-info" ,
88+ }
89+
90+
91+ def _should_skip_dir (name : str ) -> bool :
92+ """Return True if a directory name matches any ignore pattern."""
93+ if name in _IGNORE_DIRS :
94+ return True
95+ # Handle glob-style suffix patterns like *.egg-info
96+ for pat in _IGNORE_DIRS :
97+ if pat .startswith ("*" ) and name .endswith (pat [1 :]):
98+ return True
99+ return False
100+
101+
102+ def list_directory (path : str = "." , max_depth : int = 3 , git_aware : bool = True ) -> str :
103+ """Lists directory contents as a tree structure with smart filtering.
104+
105+ Returns a compact, indented tree of files and directories. By default it
106+ respects .gitignore (via `git ls-files`) so the model never wastes tokens
107+ on build artefacts or vendored deps.
108+
109+ Args:
110+ path: Root directory to list (defaults to current directory '.').
111+ max_depth: How many levels deep to recurse (defaults to 3).
112+ git_aware: If True, use git ls-files to respect .gitignore (defaults to True).
113+ """
114+ abs_path = os .path .abspath (path )
115+ if not os .path .isdir (abs_path ):
116+ return f"Error: '{ path } ' is not a directory."
117+
118+ # ---- Fast path: use git ls-files when inside a repo ----
119+ if git_aware :
120+ try :
121+ tracked = subprocess .check_output (
122+ ["git" , "ls-files" , "--cached" , "--others" , "--exclude-standard" , path ],
123+ text = True ,
124+ stderr = subprocess .DEVNULL ,
125+ ).strip ()
126+ if tracked :
127+ lines = tracked .splitlines ()
128+ # Enforce max_depth for git-aware output as well.
129+ normalized_root = os .path .normpath (path )
130+ filtered : list [str ] = []
131+ for p in lines :
132+ rel = (
133+ os .path .relpath (p , normalized_root )
134+ if normalized_root not in ("." , "" )
135+ else p
136+ )
137+ depth = rel .count (os .sep ) + 1
138+ if depth <= max_depth :
139+ filtered .append (p )
140+ lines = filtered
141+ if len (lines ) > 300 :
142+ return (
143+ "\n " .join (lines [:300 ])
144+ f"\n \n ... and { len (lines ) - 300 } more files."
145+ )
146+ return "\n " .join (lines )
147+ except Exception :
148+ pass # Not a git repo or git not available — fall through to manual walk
149+
150+ # ---- Fallback: manual os.scandir walk ----
151+ output_lines : list [str ] = []
152+
153+ def _walk (dir_path : str , prefix : str , depth : int ):
154+ if depth > max_depth :
155+ return
156+ try :
157+ entries = sorted (
158+ os .scandir (dir_path ), key = lambda e : (not e .is_dir (), e .name .lower ())
159+ )
160+ except PermissionError :
161+ return
162+
163+ visible_dirs = [
164+ e
165+ for e in entries
166+ if e .is_dir ()
167+ and not _should_skip_dir (e .name )
168+ and not e .name .startswith ("." )
169+ ]
170+ files = [e for e in entries if e .is_file ()]
171+ combined = visible_dirs + files
172+
173+ for i , entry in enumerate (combined ):
174+ is_last = i == len (combined ) - 1
175+ connector = "└── " if is_last else "├── "
176+ suffix = "/" if entry .is_dir () else ""
177+ output_lines .append (f"{ prefix } { connector } { entry .name } { suffix } " )
178+ if entry .is_dir ():
179+ extension = " " if is_last else "│ "
180+ _walk (entry .path , prefix + extension , depth + 1 )
181+
182+ _walk (abs_path , "" , 1 )
183+ result = "\n " .join (output_lines )
184+ if len (result ) > 4000 :
185+ result = result [:4000 ] + "\n ...[TRUNCATED]"
186+ return result or "Empty directory."
187+
188+
189+ # ---------------------------------------------------------------------------
190+ # Dedicated git status tool
191+ # ---------------------------------------------------------------------------
192+
193+
194+ def get_git_status (include_diff : bool = False ) -> str :
195+ """Returns a comprehensive git status summary in a single call.
196+
197+ Bundles branch name, porcelain status, and recent commits so the agent
198+ does not need multiple run_command calls.
199+
200+ Args:
201+ include_diff: If True, also include a condensed diff stat of staged and unstaged changes (defaults to False).
202+ """
203+ parts : list [str ] = []
70204
71- # 1. Gather Git Context
205+ # Branch
72206 try :
73207 branch = subprocess .check_output (
74- ["git" , "branch" , "--show-current" ], text = True , stderr = subprocess .STDOUT
208+ ["git" , "branch" , "--show-current" ], text = True , stderr = subprocess .DEVNULL
75209 ).strip ()
76- status = subprocess .check_output (
77- ["git" , "status" , "-s" ], text = True , stderr = subprocess .STDOUT
78- )
79- log = subprocess .check_output (
80- ["git" , "log" , "-n" , "5" , "--oneline" ], text = True , stderr = subprocess .STDOUT
81- )
82-
83- summary_parts .append (
84- f"### Git Context\n **Branch**: { branch } \n **Status**:\n { status if status else 'Clean' } \n **Recent Commits**:\n { log } "
85- )
210+ parts .append (f"Branch: { branch } " )
86211 except Exception :
87- summary_parts . append ( "### Git Context \n Not a git repository or git error." )
212+ return "Not a git repository."
88213
89- # 2. Gather Directory Structure (limited to root )
214+ # Status (porcelain for compact, stable output )
90215 try :
91- files = os .listdir ("." )
92- summary_parts .append (f"### Root Directory Files\n { ', ' .join (files )} " )
216+ status = subprocess .check_output (
217+ ["git" , "status" , "--porcelain=v2" , "--branch" ],
218+ text = True ,
219+ stderr = subprocess .DEVNULL ,
220+ ).strip ()
221+ parts .append (f"Status:\n { status } " if status else "Status: Clean working tree" )
93222 except Exception as e :
94- summary_parts .append (f"### Directory Listing Error\n { e } " )
223+ parts .append (f"Status error: { e } " )
224+
225+ # Recent commits
226+ try :
227+ log = subprocess .check_output (
228+ ["git" , "log" , "-n" , "5" , "--oneline" , "--no-decorate" ],
229+ text = True ,
230+ stderr = subprocess .DEVNULL ,
231+ ).strip ()
232+ if log :
233+ parts .append (f"Recent commits:\n { log } " )
234+ except Exception :
235+ pass
236+
237+ # Optional diff stat
238+ if include_diff :
239+ try :
240+ diff = subprocess .check_output (
241+ ["git" , "diff" , "--stat" , "--no-color" ],
242+ text = True ,
243+ stderr = subprocess .DEVNULL ,
244+ ).strip ()
245+ if diff :
246+ parts .append (f"Unstaged changes:\n { diff } " )
247+ except Exception :
248+ pass
249+ try :
250+ staged = subprocess .check_output (
251+ ["git" , "diff" , "--cached" , "--stat" , "--no-color" ],
252+ text = True ,
253+ stderr = subprocess .DEVNULL ,
254+ ).strip ()
255+ if staged :
256+ parts .append (f"Staged changes:\n { staged } " )
257+ except Exception :
258+ pass
259+
260+ return "\n \n " .join (parts )
261+
262+
263+ # ---------------------------------------------------------------------------
264+ # Workspace summary (used at session start)
265+ # ---------------------------------------------------------------------------
266+
267+
268+ def get_workspace_summary () -> str :
269+ """Gathers git context, project structure, and documentation to help the agent understand the whole project."""
270+ summary_parts = []
271+
272+ # 1. Git context — reuse the dedicated tool
273+ git_info = get_git_status ()
274+ summary_parts .append (f"### Git Context\n { git_info } " )
275+
276+ # 2. Project structure — reuse the dedicated tool (depth 2 to keep it compact)
277+ tree = list_directory ("." , max_depth = 2 )
278+ summary_parts .append (f"### Project Structure\n { tree } " )
95279
96280 # 3. Read important docs
97281 docs_to_check = [
@@ -214,6 +398,8 @@ def finish_task(message: str) -> str:
214398 "read_file" : read_file ,
215399 "write_file" : write_file ,
216400 "run_command" : run_command ,
401+ "list_directory" : list_directory ,
402+ "get_git_status" : get_git_status ,
217403 "search_repo" : search_repo ,
218404 "ask_user" : ask_user ,
219405 "finish_task" : finish_task ,
@@ -227,6 +413,8 @@ def finish_task(message: str) -> str:
227413 read_file ,
228414 write_file ,
229415 run_command ,
416+ list_directory ,
417+ get_git_status ,
230418 search_repo ,
231419 ask_user ,
232420 finish_task ,
0 commit comments