From e1394aee4c7f02e6b606f1efa1c75d47a1230bf2 Mon Sep 17 00:00:00 2001 From: banteg <4562643+banteg@users.noreply.github.com> Date: Mon, 29 Dec 2025 15:33:51 +0400 Subject: [PATCH] fix(exec_render): rename header item to step --- src/takopi/exec_render.py | 15 ++++++++++++--- tests/test_exec_render.py | 38 +++++++++++++++++++++++++++++++++----- 2 files changed, 45 insertions(+), 8 deletions(-) diff --git a/src/takopi/exec_render.py b/src/takopi/exec_render.py index 7eb6e62..dac2a14 100644 --- a/src/takopi/exec_render.py +++ b/src/takopi/exec_render.py @@ -32,7 +32,7 @@ def format_header(elapsed_s: float, item: int | None, label: str) -> str: elapsed = format_elapsed(elapsed_s) parts = [label, elapsed] if item is not None: - parts.append(f"item {item}") + parts.append(f"step {item}") return HEADER_SEP.join(parts) @@ -68,6 +68,7 @@ def format_event( last_item: int | None, *, command_width: int | None = None, + escape_markdown: bool = False, ) -> tuple[int | None, list[str], str | None, str | None]: """ Returns (new_last_item, cli_lines, progress_line, progress_prefix). @@ -92,6 +93,9 @@ def format_event( item_num = extract_numeric_id(item["id"], last_item) last_item = item_num if item_num is not None else last_item prefix = f"{item_num}. " + if escape_markdown and item_num is not None: + # Avoid ordered-list parsing which renumbers items in MarkdownIt/CommonMark. + prefix = f"{item_num}\\." + " " match (item["type"], etype): case ("agent_message", "item.completed"): @@ -160,7 +164,9 @@ def format_event( def render_event_cli( event: dict[str, Any], last_item: int | None = None ) -> tuple[int | None, list[str]]: - last_item, cli_lines, _, _ = format_event(event, last_item, command_width=None) + last_item, cli_lines, _, _ = format_event( + event, last_item, command_width=None, escape_markdown=False + ) return last_item, cli_lines @@ -180,7 +186,10 @@ class ExecProgressRenderer: return True self.last_item, _, progress_line, progress_prefix = format_event( - event, self.last_item, command_width=self.command_width + event, + self.last_item, + command_width=self.command_width, + escape_markdown=True, ) if progress_line is None: return False diff --git a/tests/test_exec_render.py b/tests/test_exec_render.py index d2f5fca..561dbdd 100644 --- a/tests/test_exec_render.py +++ b/tests/test_exec_render.py @@ -2,6 +2,7 @@ import json from pathlib import Path from takopi.exec_render import ExecProgressRenderer, render_event_cli +from takopi.rendering import render_markdown def _loads(lines: str) -> list[dict]: @@ -67,11 +68,11 @@ def test_progress_renderer_renders_progress_and_final() -> None: r.note_event(evt) progress = r.render_progress(3.0) - assert progress.startswith("working · 3s · item 3") - assert "1. ✓ `bash -lc ls`" in progress + assert progress.startswith("working · 3s · step 3") + assert "1\\. ✓ `bash -lc ls`" in progress final = r.render_final(3.0, "answer", status="done") - assert final.startswith("done · 3s · item 3") + assert final.startswith("done · 3s · step 3") assert "running:" not in final assert "ran:" not in final assert final.endswith("answer") @@ -97,6 +98,33 @@ def test_progress_renderer_clamps_actions_and_ignores_unknown() -> None: assert r.note_event(evt) is True assert len(r.recent_actions) == 3 - assert r.recent_actions[0].startswith("3. ") - assert r.recent_actions[-1].startswith("5. ") + assert r.recent_actions[0].startswith("3\\. ") + assert r.recent_actions[-1].startswith("5\\. ") assert r.note_event({"type": "mystery"}) is False + + +def test_progress_renderer_preserves_item_ids_in_telegram_text() -> None: + r = ExecProgressRenderer(max_actions=5, command_width=None) + for i in (30, 31, 32): + r.note_event( + { + "type": "item.completed", + "item": { + "id": f"item_{i}", + "type": "command_execution", + "command": f"echo {i}", + "exit_code": 0, + "status": "completed", + }, + } + ) + + md = r.render_progress(0.0) + assert "30\\." in md + assert "31\\." in md + assert "32\\." in md + + text, _ = render_markdown(md) + assert "30. ✓ echo 30" in text + assert "31. ✓ echo 31" in text + assert "32. ✓ echo 32" in text