Track and render hierarchical steps without emojis, similar to Claude Code tree output. Supports live auto-refresh via an attached refresh callback.
| 39 | err_console = Console(stderr=True, highlight=False) |
| 40 | |
| 41 | class StepTracker: |
| 42 | """Track and render hierarchical steps without emojis, similar to Claude Code tree output. |
| 43 | Supports live auto-refresh via an attached refresh callback. |
| 44 | """ |
| 45 | def __init__(self, title: str): |
| 46 | self.title = title |
| 47 | self.steps = [] # list of dicts: {key, label, status, detail} |
| 48 | self.status_order = {"pending": 0, "running": 1, "done": 2, "error": 3, "skipped": 4} |
| 49 | self._refresh_cb: Callable[[], None] | None = None |
| 50 | |
| 51 | def attach_refresh(self, cb: Callable[[], None]) -> None: |
| 52 | self._refresh_cb = cb |
| 53 | |
| 54 | def add(self, key: str, label: str): |
| 55 | if key not in [s["key"] for s in self.steps]: |
| 56 | self.steps.append({"key": key, "label": label, "status": "pending", "detail": ""}) |
| 57 | self._maybe_refresh() |
| 58 | |
| 59 | def start(self, key: str, detail: str = ""): |
| 60 | self._update(key, status="running", detail=detail) |
| 61 | |
| 62 | def complete(self, key: str, detail: str = ""): |
| 63 | self._update(key, status="done", detail=detail) |
| 64 | |
| 65 | def error(self, key: str, detail: str = ""): |
| 66 | self._update(key, status="error", detail=detail) |
| 67 | |
| 68 | def skip(self, key: str, detail: str = ""): |
| 69 | self._update(key, status="skipped", detail=detail) |
| 70 | |
| 71 | def _update(self, key: str, status: str, detail: str): |
| 72 | for s in self.steps: |
| 73 | if s["key"] == key: |
| 74 | s["status"] = status |
| 75 | if detail: |
| 76 | s["detail"] = detail |
| 77 | self._maybe_refresh() |
| 78 | return |
| 79 | |
| 80 | self.steps.append({"key": key, "label": key, "status": status, "detail": detail}) |
| 81 | self._maybe_refresh() |
| 82 | |
| 83 | def _maybe_refresh(self): |
| 84 | if self._refresh_cb: |
| 85 | try: |
| 86 | self._refresh_cb() |
| 87 | except Exception: |
| 88 | pass |
| 89 | |
| 90 | def render(self): |
| 91 | tree = Tree(f"[cyan]{self.title}[/cyan]", guide_style="grey50") |
| 92 | for step in self.steps: |
| 93 | label = step["label"] |
| 94 | detail_text = step["detail"].strip() if step["detail"] else "" |
| 95 | |
| 96 | status = step["status"] |
| 97 | if status == "done": |
| 98 | symbol = "[green]●[/green]" |
no outgoing calls