From c63c4304eea8aa9a5d957bc335eb6058c3d8f937 Mon Sep 17 00:00:00 2001 From: Oscar-Williams Date: Sun, 27 Sep 2026 22:19:21 +0800 Subject: [PATCH] fix(list_files): report truncated directory listings --- mini_coding_agent.py | 19 ++++++++++++++++--- tests/test_mini_coding_agent.py | 17 +++++++++++++++++ 2 files changed, 33 insertions(+), 3 deletions(-) diff --git a/mini_coding_agent.py b/mini_coding_agent.py index 1bf2403..c32bbf3 100644 --- a/mini_coding_agent.py +++ b/mini_coding_agent.py @@ -282,9 +282,9 @@ def remember(bucket, item, limit): def build_tools(self): tools = { "list_files": { - "schema": {"path": "str='.'"}, + "schema": {"path": "str='.'", "start": "int=1", "end": "int=200"}, "risky": False, - "description": "List files in the workspace.", + "description": "List files in the workspace, up to 200 entries per page.", "run": self.tool_list_files, }, "read_file": { @@ -540,6 +540,10 @@ def validate_tool(self, name, args): path = self.path(args.get("path", ".")) if not path.is_dir(): raise ValueError("path is not a directory") + start = int(args.get("start", 1)) + end = int(args.get("end", 200)) + if start < 1 or end < start: + raise ValueError("invalid entry range") return if name == "read_file": @@ -743,14 +747,23 @@ def tool_list_files(self, args): path = self.path(args.get("path", ".")) if not path.is_dir(): raise ValueError("path is not a directory") + start = int(args.get("start", 1)) + end = int(args.get("end", 200)) entries = [ item for item in sorted(path.iterdir(), key=lambda item: (item.is_file(), item.name.lower())) if item.name not in IGNORED_PATH_NAMES ] + page_end = min(end, start + 199) lines = [] - for entry in entries[:200]: + for entry in entries[start - 1 : page_end]: kind = "[D]" if entry.is_dir() else "[F]" lines.append(f"{kind} {entry.relative_to(self.root)}") + if page_end < len(entries): + display_path = str(path.relative_to(self.root)) + lines.append( + f"... {page_end - start + 1} entries shown; {len(entries) - page_end} additional entries omitted. " + f"Call list_files with path='{display_path}', start={page_end + 1}, end={page_end + 200} to continue." + ) return "\n".join(lines) or "(empty)" def tool_read_file(self, args): diff --git a/tests/test_mini_coding_agent.py b/tests/test_mini_coding_agent.py index 5c62693..8a41aa1 100644 --- a/tests/test_mini_coding_agent.py +++ b/tests/test_mini_coding_agent.py @@ -189,6 +189,23 @@ def test_list_files_hides_internal_agent_state(tmp_path): assert "[F] hello.txt" in result +def test_list_files_reports_truncation_and_supports_pagination(tmp_path): + agent = build_agent(tmp_path, []) + for index in range(203): + (tmp_path / f"item-{index:03}.txt").write_text("item\n", encoding="utf-8") + + first_page = agent.run_tool("list_files", {}) + assert "200 entries shown; 4 additional entries omitted" in first_page + + wide_page = agent.run_tool("list_files", {"end": 400}) + assert "[F] item-198.txt" in wide_page + assert "[F] item-200.txt" not in wide_page + + next_page = agent.run_tool("list_files", {"start": 201, "end": 400}) + assert "[F] item-202.txt" in next_page + assert "[F] item-000.txt" not in next_page + + def test_path_rejects_parent_escape(tmp_path): agent = build_agent(tmp_path, [])