@@ -64,34 +64,205 @@ 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+ if len (lines ) > 300 :
129+ return (
130+ "\n " .join (lines [:300 ])
131+ + f"\n \n ... and { len (lines ) - 300 } more files."
132+ )
133+ return "\n " .join (lines )
134+ except Exception :
135+ pass # Not a git repo or git not available — fall through to manual walk
136+
137+ # ---- Fallback: manual os.scandir walk ----
138+ output_lines : list [str ] = []
139+
140+ def _walk (dir_path : str , prefix : str , depth : int ):
141+ if depth > max_depth :
142+ return
143+ try :
144+ entries = sorted (
145+ os .scandir (dir_path ), key = lambda e : (not e .is_dir (), e .name .lower ())
146+ )
147+ except PermissionError :
148+ return
149+
150+ visible_dirs = [
151+ e
152+ for e in entries
153+ if e .is_dir ()
154+ and not _should_skip_dir (e .name )
155+ and not e .name .startswith ("." )
156+ ]
157+ files = [e for e in entries if e .is_file ()]
158+ combined = visible_dirs + files
159+
160+ for i , entry in enumerate (combined ):
161+ is_last = i == len (combined ) - 1
162+ connector = "└── " if is_last else "├── "
163+ suffix = "/" if entry .is_dir () else ""
164+ output_lines .append (f"{ prefix } { connector } { entry .name } { suffix } " )
165+ if entry .is_dir ():
166+ extension = " " if is_last else "│ "
167+ _walk (entry .path , prefix + extension , depth + 1 )
168+
169+ _walk (abs_path , "" , 1 )
170+ result = "\n " .join (output_lines )
171+ if len (result ) > 4000 :
172+ result = result [:4000 ] + "\n ...[TRUNCATED]"
173+ return result or "Empty directory."
174+
175+
176+ # ---------------------------------------------------------------------------
177+ # Dedicated git status tool
178+ # ---------------------------------------------------------------------------
179+
180+
181+ def get_git_status (include_diff : bool = False ) -> str :
182+ """Returns a comprehensive git status summary in a single call.
183+
184+ Bundles branch name, porcelain status, and recent commits so the agent
185+ does not need multiple run_command calls.
186+
187+ Args:
188+ include_diff: If True, also include a condensed diff stat of staged and unstaged changes (defaults to False).
189+ """
190+ parts : list [str ] = []
70191
71- # 1. Gather Git Context
192+ # Branch
72193 try :
73194 branch = subprocess .check_output (
74- ["git" , "branch" , "--show-current" ], text = True , stderr = subprocess .STDOUT
195+ ["git" , "branch" , "--show-current" ], text = True , stderr = subprocess .DEVNULL
75196 ).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- )
197+ parts .append (f"Branch: { branch } " )
86198 except Exception :
87- summary_parts . append ( "### Git Context \n Not a git repository or git error." )
199+ return "Not a git repository."
88200
89- # 2. Gather Directory Structure (limited to root )
201+ # Status (porcelain for compact, stable output )
90202 try :
91- files = os .listdir ("." )
92- summary_parts .append (f"### Root Directory Files\n { ', ' .join (files )} " )
203+ status = subprocess .check_output (
204+ ["git" , "status" , "--porcelain=v2" , "--branch" ],
205+ text = True ,
206+ stderr = subprocess .DEVNULL ,
207+ ).strip ()
208+ parts .append (f"Status:\n { status } " if status else "Status: Clean working tree" )
93209 except Exception as e :
94- summary_parts .append (f"### Directory Listing Error\n { e } " )
210+ parts .append (f"Status error: { e } " )
211+
212+ # Recent commits
213+ try :
214+ log = subprocess .check_output (
215+ ["git" , "log" , "-n" , "5" , "--oneline" , "--no-decorate" ],
216+ text = True ,
217+ stderr = subprocess .DEVNULL ,
218+ ).strip ()
219+ if log :
220+ parts .append (f"Recent commits:\n { log } " )
221+ except Exception :
222+ pass
223+
224+ # Optional diff stat
225+ if include_diff :
226+ try :
227+ diff = subprocess .check_output (
228+ ["git" , "diff" , "--stat" , "--no-color" ],
229+ text = True ,
230+ stderr = subprocess .DEVNULL ,
231+ ).strip ()
232+ if diff :
233+ parts .append (f"Unstaged changes:\n { diff } " )
234+ except Exception :
235+ pass
236+ try :
237+ staged = subprocess .check_output (
238+ ["git" , "diff" , "--cached" , "--stat" , "--no-color" ],
239+ text = True ,
240+ stderr = subprocess .DEVNULL ,
241+ ).strip ()
242+ if staged :
243+ parts .append (f"Staged changes:\n { staged } " )
244+ except Exception :
245+ pass
246+
247+ return "\n \n " .join (parts )
248+
249+
250+ # ---------------------------------------------------------------------------
251+ # Workspace summary (used at session start)
252+ # ---------------------------------------------------------------------------
253+
254+
255+ def get_workspace_summary () -> str :
256+ """Gathers git context, project structure, and documentation to help the agent understand the whole project."""
257+ summary_parts = []
258+
259+ # 1. Git context — reuse the dedicated tool
260+ git_info = get_git_status ()
261+ summary_parts .append (f"### Git Context\n { git_info } " )
262+
263+ # 2. Project structure — reuse the dedicated tool (depth 2 to keep it compact)
264+ tree = list_directory ("." , max_depth = 2 )
265+ summary_parts .append (f"### Project Structure\n { tree } " )
95266
96267 # 3. Read important docs
97268 docs_to_check = [
@@ -214,6 +385,8 @@ def finish_task(message: str) -> str:
214385 "read_file" : read_file ,
215386 "write_file" : write_file ,
216387 "run_command" : run_command ,
388+ "list_directory" : list_directory ,
389+ "get_git_status" : get_git_status ,
217390 "search_repo" : search_repo ,
218391 "ask_user" : ask_user ,
219392 "finish_task" : finish_task ,
@@ -227,6 +400,8 @@ def finish_task(message: str) -> str:
227400 read_file ,
228401 write_file ,
229402 run_command ,
403+ list_directory ,
404+ get_git_status ,
230405 search_repo ,
231406 ask_user ,
232407 finish_task ,
0 commit comments