From 77e0bbfb1dab5e92547244692dc8419cbe9a8d7a Mon Sep 17 00:00:00 2001 From: banteg <4562643+banteg@users.noreply.github.com> Date: Thu, 1 Jan 2026 02:20:17 +0400 Subject: [PATCH] fix(render): improve file-change paths --- src/takopi/render.py | 35 +++++++++++++++++++++++++++++++---- tests/test_exec_render.py | 25 ++++++++++++++++++++++++- 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/src/takopi/render.py b/src/takopi/render.py index b7bd6cb..51b61f7 100644 --- a/src/takopi/render.py +++ b/src/takopi/render.py @@ -4,6 +4,7 @@ from __future__ import annotations import textwrap from collections import deque +from pathlib import Path from typing import Callable from .model import Action, ActionEvent, ResumeToken, StartedEvent, TakopiEvent @@ -18,7 +19,33 @@ HARD_BREAK = " \n" MAX_PROGRESS_CMD_LEN = 300 MAX_FILE_CHANGES_INLINE = 3 -FILE_CHANGE_PREFIX = {"add": "+", "delete": "-", "update": "~"} +FILE_CHANGE_VERB = {"add": "added", "delete": "deleted", "update": "updated"} +_ABSOLUTE_PATH_PREFIXES = ("Users/", "home/", "private/", "var/", "etc/", "opt/", "usr/") + + +def format_changed_file_path(path: str, *, base_dir: Path | None = None) -> str: + raw = path.strip() + if raw.startswith("./"): + raw = raw[2:] + + # Some clients may accidentally prefix "~" onto an absolute path (e.g. "~" + "/Users/…"). + if raw.startswith("~/") and raw[2:].startswith(_ABSOLUTE_PATH_PREFIXES): + raw = "/" + raw[2:] + + base = Path.cwd() if base_dir is None else base_dir + try: + raw_path = Path(raw) + except Exception: + return f"`{raw}`" + + if raw_path.is_absolute(): + try: + raw_path = raw_path.relative_to(base) + raw = raw_path.as_posix() + except Exception: + pass + + return f"`{raw}`" def format_elapsed(elapsed_s: float) -> str: @@ -82,13 +109,13 @@ def format_file_change_title(action: Action, *, command_width: int | None) -> st if not isinstance(path, str) or not path: continue kind = raw.get("kind") - prefix = FILE_CHANGE_PREFIX.get(kind, "~") if isinstance(kind, str) else "~" - rendered.append(f"{prefix}{path}") + verb = FILE_CHANGE_VERB.get(kind, "updated") if isinstance(kind, str) else "updated" + rendered.append(f"{verb} {format_changed_file_path(path)}") if rendered: if len(rendered) > MAX_FILE_CHANGES_INLINE: remaining = len(rendered) - MAX_FILE_CHANGES_INLINE - rendered = rendered[:MAX_FILE_CHANGES_INLINE] + [f"…(+{remaining})"] + rendered = rendered[:MAX_FILE_CHANGES_INLINE] + [f"…({remaining} more)"] inline = shorten(", ".join(rendered), command_width) return f"files: {inline}" diff --git a/tests/test_exec_render.py b/tests/test_exec_render.py index c3a6ec2..30161d4 100644 --- a/tests/test_exec_render.py +++ b/tests/test_exec_render.py @@ -1,5 +1,6 @@ from typing import cast from types import SimpleNamespace +from pathlib import Path from takopi.markdown import render_markdown from takopi.model import TakopiEvent @@ -79,10 +80,32 @@ def test_render_event_cli_handles_action_kinds() -> None: for line in out ) assert any("tool: github.search_issues" in line for line in out) - assert any("files: +README.md, ~src/compute_answer.py" in line for line in out) + assert any( + "files: added `README.md`, updated `src/compute_answer.py`" in line for line in out + ) assert any(line.startswith("✗ stream error") for line in out) +def test_file_change_renders_relative_paths_inside_cwd() -> None: + readme_abs = str(Path.cwd() / "README.md") + weird_abs = "~" + readme_abs + out = render_event_cli( + action_completed( + "f-abs", + "file_change", + "README.md", + ok=True, + detail={ + "changes": [ + {"path": readme_abs, "kind": "update"}, + {"path": weird_abs, "kind": "update"}, + ] + }, + ) + ) + assert any("files: updated `README.md`, updated `README.md`" in line for line in out) + + def test_progress_renderer_renders_progress_and_final() -> None: r = ExecProgressRenderer(max_actions=5, resume_formatter=_format_resume) for evt in SAMPLE_EVENTS: