"""Compact, readable progress lines for Zed agent chat.""" from __future__ import annotations import re from typing import Any _MODEL_LABEL = { "novita-planner": "Max", "novita-verifier": "verify", "a-medium-code": "Coder", "a-simple": "Llama", "b-complex": "DeepSeek", "c-complex": "Max", "approved-plan": "план", } _NOISE = ( "ждём LiteLLM", "payload:", "ctx ", "ctx-", "worker #", "волна ", "параллельно", "bytes≈", "upstream_stream", "non-stream", ) def model_label(model: str | None) -> str: if not model: return "модель" m = str(model) return _MODEL_LABEL.get(m, m.split("/")[-1][:18]) def short_path(path: str | None, *, max_parts: int = 3) -> str: if not path: return "" p = str(path).replace("/", "\\").rstrip("\\") parts = [x for x in p.split("\\") if x] if len(parts) <= max_parts: return "\\".join(parts) return "\\".join(parts[-max_parts:]) def plan_paths(plan: dict[str, Any] | None) -> list[str]: out: list[str] = [] for item in (plan or {}).get("subtasks") or []: if not isinstance(item, dict): continue for p in item.get("paths") or []: if isinstance(p, str) and p.strip() and p not in out: out.append(p.strip()) return out def is_noise(line: str) -> bool: s = (line or "").strip() if not s: return True low = s.lower() return any(n.lower() in low for n in _NOISE) def humanize_line(line: str) -> str | None: """Map internal progress → short Russian status. None = drop.""" s = (line or "").strip() if not s or is_noise(s): return None low = s.lower() if s in ("план…", "план...", "план"): return "Планирую задачу" if "план через" in low or low.startswith("hierarchical:"): return "Планирую задачу" if "утверждён" in low or "утвержден" in low: m = re.search(r"(\d+)\s*подзадач", s) n = m.group(1) if m else "" return f"План утверждён{f' · {n} шаг(а)' if n else ''}" if "отменён" in low or "отменен" in low: return "План отменён" if "правки от пользователя" in low or "пересборк" in low: return "Пересобираю план по правкам" if "ожидает утверждения" in low or "ждём утверждения" in low or "awaiting" in low: return "План готов — подтверди («ok»)" if "agent: план готов" in low or "executor с tools" in low: return "Перехожу к выполнению" if "готово за" in low: return "Готово" if "json битый" in low or "repair" in low: return "Чиню формат плана" if low.startswith("path_index") or "path_index hit" in low: return None # shown via dedicated path line if "path_resolve" in low or "поиск файл" in low: return "Ищу файлы в проекте" if "runtime_probe" in low or "runtime:" in low: return "Проверяю runtime" if "force edit" in low: return "Правлю файл" if "kickstart" in low: return "Читаю файл" if "escalate" in low or "mid-loop" in low: return None if "408" in s or "timeout" in low: return "Таймаут модели — продолжаю иначе" if "400" in s and "bad" in low: return None # strip technical prefixes s = re.sub(r"^план:\s*", "", s, flags=re.I) s = re.sub(r"^agent:\s*", "", s, flags=re.I) if len(s) > 90: s = s[:87] + "…" return s def format_progress_block(lines: list[str]) -> str: """Final/static progress block — numbered, no junk.""" steps: list[str] = [] seen: set[str] = set() for raw in lines or []: h = humanize_line(str(raw)) if not h or h in seen: continue seen.add(h) steps.append(h) if not steps: return "" body = "\n".join(f"{i}. {t}" for i, t in enumerate(steps, 1)) return f"**Ход**\n{body}\n\n---\n\n" def stream_header() -> str: return "**Agent**\n" def stream_step(text: str) -> str: return f"· {text}\n" def execution_banner( *, path_mode: bool, plan: dict[str, Any] | None, model: str | None, ) -> str: """Shown once after plan approve — what happens next.""" paths = plan_paths(plan) lines = ["", "---", "", "**Выполнение**"] facts = (plan or {}).get("runtime_facts") if isinstance(plan, dict) else None step = 1 if isinstance(facts, dict) and (facts.get("host") or facts.get("hint")): host = facts.get("host") or "?" status = facts.get("http_status") suggest = facts.get("suggested_backend_url") bit = f"{host}" if status is not None: bit += f" → HTTP {status}" if suggest: bit += f" · fix `{suggest}`" lines.append(f"{step}. Runtime: {bit}") step += 1 if paths: shown = ", ".join(f"`{short_path(p)}`" for p in paths[:2]) src = "" tasks = (plan or {}).get("subtasks") or [] if tasks and isinstance(tasks[0], dict): ps = str(tasks[0].get("path_source") or "") if ps == "index": src = " · индекс" elif ps == "find": src = " · поиск" lines.append(f"{step}. Файл{src}: {shown}") step += 1 if path_mode: lines.append(f"{step}. Уточняю путь в репозитории…") else: lines.append(f"{step}. Читаю → правлю") elif path_mode: lines.append(f"{step}. Ищу нужные файлы…") else: lines.append(f"{step}. Запуск ({model_label(model)})") lines.append("") return "\n".join(lines) + "\n" def tool_status(tool_names: list[str] | None, *, path: str | None = None) -> str: names = [str(n).lower() for n in (tool_names or []) if n] if any("edit" in n or "write" in n for n in names): base = "Правлю" elif any("read" in n for n in names): base = "Читаю" elif any("find" in n or "list" in n or "grep" in n for n in names): base = "Ищу" else: base = "Инструмент" if path: return stream_step(f"{base} `{short_path(path)}`") return stream_step(base)