mirror of
https://github.com/NCBM/plyngent.git
synced 2026-11-07 07:17:54 +08:00
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:
@@ -31,7 +31,7 @@ _TOOL_ARGS_PREVIEW = 80
|
||||
|
||||
# Tool calls that render as a single pretty summary line instead of
|
||||
# ``[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).
|
||||
_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}"
|
||||
|
||||
|
||||
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:
|
||||
"""Pretty summary for a known tool call + result; None keeps the old style."""
|
||||
if name == "read_file":
|
||||
return _read_file_pretty(args_json, result)
|
||||
if name == "tree":
|
||||
return _tree_pretty(args_json, result)
|
||||
if name in {"todo_push", "todo_update"}:
|
||||
return _todo_pretty(name, result)
|
||||
return None
|
||||
|
||||
@@ -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}"
|
||||
|
||||
|
||||
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:
|
||||
call = ToolCallEvent(
|
||||
tool_call=AssistantFunctionToolCall(
|
||||
|
||||
Reference in New Issue
Block a user