cli: pretty tree tool summary like read_file

Tool results for tree render as a single line: * Tree 'path' (N dirs,
M files, depth D) for markdown/decorated, dropping the depth for flat.
Counts and depth are parsed from the rendered output per format.
This commit is contained in:
2026-08-15 17:09:57 +08:00
parent 4fa05e2456
commit b5014cd3e7
2 changed files with 96 additions and 1 deletions
+58 -1
View File
@@ -31,7 +31,7 @@ _TOOL_ARGS_PREVIEW = 80
# Tool calls that render as a single pretty summary line instead of # Tool calls that render as a single pretty summary line instead of
# ``[tool]`` / ``[tool result]``. # ``[tool]`` / ``[tool result]``.
_PRETTY_TOOLS = frozenset({"read_file", "todo_push", "todo_update"}) _PRETTY_TOOLS = frozenset({"read_file", "todo_push", "todo_update", "tree"})
# Process/session display flags (set from ReplState / slash). # Process/session display flags (set from ReplState / slash).
_verbose_tool_results: ContextVar[bool] = ContextVar("verbose_tool_results", default=False) _verbose_tool_results: ContextVar[bool] = ContextVar("verbose_tool_results", default=False)
@@ -108,10 +108,67 @@ def _todo_pretty(name: str, result: str) -> str:
return f"* {label}:\n{result}" return f"* {label}:\n{result}"
def _tree_line_stats(fmt: str, line: str) -> tuple[str, int] | None:
"""Classify one ``tree`` result line as ``(kind, depth)`` or None (not an entry).
``kind`` is ``"dir"`` or ``"file"``; depth is 0 for ``flat`` (no hierarchy).
"""
stripped = line.strip()
if not stripped:
return None
kind: str | None = None
depth = 0
if fmt == "flat":
if not stripped.startswith("…"):
kind = "dir" if stripped.endswith("/") else "file"
elif fmt == "markdown":
if stripped.startswith("- ") and not stripped.startswith(("- …", "- error")):
kind = "dir" if stripped[2:].endswith("/") else "file"
depth = (len(line) - len(line.lstrip(" "))) // 2 + 1
elif "… (" not in stripped:
branch = "├── " if "├── " in line else ("└── " if "└── " in line else None)
if branch is not None:
name = line[line.index(branch) + len(branch) :].rstrip()
kind = "dir" if name.endswith("/") else "file"
depth = line.index(branch) // 4 + 1
if kind is None:
return None
return kind, depth
def _tree_pretty(args_json: str, result: str) -> str:
"""One-line summary for a ``tree`` call: ``* Tree 'src' (3 dirs, 24 files, depth 3)``.
Flat trees have no depth, so the depth part is omitted for ``format=flat``.
"""
path = _json_str_arg(args_json, "path") or "."
fmt = _json_str_arg(args_json, "format") or "markdown"
if result.startswith("error:"):
return f"* Tree '{path}' (error)"
dirs = 0
files = 0
depth = 0
for line in result.splitlines():
stats = _tree_line_stats(fmt, line)
if stats is None:
continue
kind, line_depth = stats
if kind == "dir":
dirs += 1
else:
files += 1
depth = max(depth, line_depth)
if depth:
return f"* Tree '{path}' ({dirs} dirs, {files} files, depth {depth})"
return f"* Tree '{path}' ({dirs} dirs, {files} files)"
def _pretty_tool_result(name: str, args_json: str, result: str) -> str | None: def _pretty_tool_result(name: str, args_json: str, result: str) -> str | None:
"""Pretty summary for a known tool call + result; None keeps the old style.""" """Pretty summary for a known tool call + result; None keeps the old style."""
if name == "read_file": if name == "read_file":
return _read_file_pretty(args_json, result) return _read_file_pretty(args_json, result)
if name == "tree":
return _tree_pretty(args_json, result)
if name in {"todo_push", "todo_update"}: if name in {"todo_push", "todo_update"}:
return _todo_pretty(name, result) return _todo_pretty(name, result)
return None return None
+38
View File
@@ -116,6 +116,44 @@ async def test_pretty_read_file_statuses(capsys: pytest.CaptureFixture[str]) ->
assert f"* Read 'a.txt' {status}" in out, f"status {status} not rendered for {content!r}" assert f"* Read 'a.txt' {status}" in out, f"status {status} not rendered for {content!r}"
def _tree_call(args_json: str) -> ToolCallEvent:
return ToolCallEvent(
tool_call=AssistantFunctionToolCall(
id="1",
function=AssistantFunctionTool(name="tree", arguments=args_json),
)
)
async def test_pretty_tree_markdown(capsys: pytest.CaptureFixture[str]) -> None:
result = "- src/\n - main.py\n - nested/\n - deep.txt\n- a.txt\n"
await render_events(_aiter([_tree_call('{"path": "."}'), _result(result)]))
out = capsys.readouterr().out
assert "* Tree '.' (2 dirs, 3 files, depth 3)" in out
assert "[tool]" not in out
async def test_pretty_tree_flat_omits_depth(capsys: pytest.CaptureFixture[str]) -> None:
result = "src/\nsrc/main.py\na.txt\n… (5 more paths not shown)"
await render_events(_aiter([_tree_call('{"path": ".", "format": "flat"}'), _result(result)]))
out = capsys.readouterr().out
assert "* Tree '.' (1 dirs, 2 files)" in out
assert "depth" not in out
async def test_pretty_tree_decorated(capsys: pytest.CaptureFixture[str]) -> None:
result = "src/\n├── main.py\n└── nested/\n └── deep.txt\n"
await render_events(_aiter([_tree_call('{"path": "src", "format": "decorated"}'), _result(result)]))
out = capsys.readouterr().out
assert "* Tree 'src' (1 dirs, 2 files, depth 2)" in out
async def test_pretty_tree_error(capsys: pytest.CaptureFixture[str]) -> None:
await render_events(_aiter([_tree_call('{"path": "nope"}'), _result("error: not a directory: nope")]))
out = capsys.readouterr().out
assert "* Tree 'nope' (error)" in out
async def test_pretty_todo_push(capsys: pytest.CaptureFixture[str]) -> None: async def test_pretty_todo_push(capsys: pytest.CaptureFixture[str]) -> None:
call = ToolCallEvent( call = ToolCallEvent(
tool_call=AssistantFunctionToolCall( tool_call=AssistantFunctionToolCall(