"""Status bar, spinner, turn-summary, pet pane, and prompt-stash rendering for the
interactive CLI.

Mixin bound onto ``HermesCLI`` via the MRO. cli.py-internal symbols are imported LAZILY
inside each method (``from cli import ...``) — never at module load time (import cycle).
"""

from __future__ import annotations

import errno
import shutil
import threading
import time

from agent.i18n import t
from agent.pet import render as pet_render
from hermes_cli.banner import _format_context_length
from typing import Any, Dict, Optional

_SB = "class:status-bar"
_DIM = "class:status-bar-dim"
_STRONG = "class:status-bar-strong"
_AGENT_COUNTERS = (
    "session_input_tokens", "session_output_tokens", "session_cache_read_tokens",
    "session_cache_write_tokens", "session_prompt_tokens", "session_completion_tokens",
    "session_total_tokens", "session_api_calls")


def _threshold_style(value, ladder, fallback: str) -> str:
    """First ``class:status-bar-<name>`` whose ``value >= bound`` in a descending ladder."""
    for bound, name in ladder:
        if value >= bound:
            return f"class:status-bar-{name}"
    return f"class:status-bar-{fallback}"


def _finite(v):
    """Drop NaN / negative / absurd provider timings (e.g. -0.8s seen in logs)."""
    return None if v is None or v != v or v < 0 or v > 1e6 else v


class CLIStatusBarMixin:
    """Status bar, spinner, turn-summary, pet pane, and prompt-stash rendering for the
    interactive CLI."""

    def _status_bar_context_style(self, percent_used: Optional[int]) -> str:
        if percent_used is None:
            return _DIM
        if percent_used >= 95:
            return "class:status-bar-critical"
        if percent_used > 80:
            return "class:status-bar-bad"
        return _threshold_style(percent_used, ((50, "warn"),), "good")

    def _cache_hit_rate(self, snapshot: dict, precision: int = 1) -> "tuple[float, str] | None":
        """Return (cache_pct, label) or None without cache data. Prefers the baseline-delta pct
        from ``_get_status_bar_snapshot`` (resets on model switch / compression, so it reflects
        the *current* cache regime); falls back to the session-lifetime ratio."""
        delta_pct = snapshot.get("cache_hit_pct")
        if delta_pct is not None:
            return float(delta_pct), f"◎ {float(delta_pct):.{precision}f}%"
        cache_read = snapshot.get("session_cache_read_tokens", 0)
        prompt_total = snapshot.get("session_prompt_tokens", 0)
        if cache_read > 0 and prompt_total > 0:
            cache_pct = cache_read / prompt_total * 100
            return cache_pct, f"◎ {cache_pct:.{precision}f}%"
        return None

    def _cache_hit_rate_style(self, cache_pct: float) -> str:
        """Higher is better (opposite of context %)."""
        return _threshold_style(cache_pct, ((70, "good"), (40, "warn")), "bad")

    @staticmethod
    def _battery_status_style(category: str) -> str:
        return {
            "good": "class:status-bar-good",
            "warn": "class:status-bar-warn",
            "bad": "class:status-bar-bad",
            "critical": "class:status-bar-critical",
        }.get(category, _DIM)

    def _vim_mode_label(self) -> str:
        """Current vi editing mode as a short status-bar label; empty when vim mode is off
        or the application is not running yet."""
        if not getattr(self, "_vim_mode", False):
            return ""
        try:
            from prompt_toolkit.key_binding.vi_state import InputMode
            app = getattr(self, "_app", None)
            if app is None:
                return ""
            mode = app.vi_state.input_mode
            if mode in (InputMode.INSERT, InputMode.INSERT_MULTIPLE):
                return t("cli.status_bar.vim_insert")
            if mode == InputMode.REPLACE:
                return t("cli.status_bar.vim_replace")
            return t("cli.status_bar.vim_normal")
        except Exception:
            return ""

    def _handle_battery_command(self, cmd_original: str) -> None:
        """``/battery`` toggles, ``/battery on|off`` sets, ``/battery status`` reports the
        setting plus a live reading. Persisted to ``display.battery``."""
        from cli import save_config_value
        parts = (cmd_original or "").split()
        arg = parts[1].strip().lower() if len(parts) > 1 else ""

        try:
            from agent.battery import format_battery, read_battery
            reading = read_battery(use_cache=False)
        except Exception:
            reading = None

        def _detail(no_battery: str) -> str:
            if reading is None:
                return ""
            return f" — {format_battery(reading)}" if reading.available else f" — {no_battery}"

        if arg in ("status", "show"):
            state = t("cli.shared.label_on") if self._battery_visible else t("cli.shared.label_off")
            detail = _detail(t("cli.status_bar.battery_none"))
            if reading is not None and reading.available:
                detail = t("cli.status_bar.battery_currently", reading=format_battery(reading))
            self._console_print(f"  {t('cli.status_bar.battery_state', state=state, detail=detail)}")
            return

        if arg in ("on", "true", "yes"):
            target = True
        elif arg in ("off", "false", "no"):
            target = False
        elif arg in ("", "toggle"):
            target = not self._battery_visible
        else:
            self._console_print(f"  {t('cli.status_bar.battery_usage')}")
            return

        self._battery_visible = target
        save_config_value("display.battery", target)
        if target:
            self._console_print(
                f"  {t('cli.status_bar.battery_on', detail=_detail(t('cli.status_bar.battery_none_nothing_shown')))}"
            )
        else:
            self._console_print(f"  {t('cli.status_bar.battery_off')}")

    @staticmethod
    def _compression_count_style(count: int) -> str:
        return _threshold_style(count, ((10, "bad"), (5, "warn")), "dim")

    def _build_context_bar(self, percent_used: Optional[int], width: int = 10) -> str:
        safe_percent = max(0, min(100, percent_used or 0))
        filled = round((safe_percent / 100) * width)
        return f"[{('█' * filled) + ('░' * max(0, width - filled))}]"

    @staticmethod
    def _format_prompt_elapsed(
        prompt_start_time: Optional[float], prompt_duration: float, live: bool = False) -> str:
        """Per-prompt elapsed time. Always a string (``⏲ 0s`` on fresh start); seconds stay
        visible at every scale so it increments smoothly (``1m 59s → 2m → 2m 1s``). ⏱ while
        live, ⏲ frozen — width-1 glyphs (no variation selector) keep the bar aligned."""
        if prompt_start_time is None and prompt_duration == 0.0:
            return "⏲ 0s"
        if prompt_start_time is not None:
            elapsed = max(0.0, time.time() - prompt_start_time)
        else:
            elapsed = max(0.0, prompt_duration)
        days, remaining = divmod(elapsed, 86400)
        hours, remaining = divmod(remaining, 3600)
        minutes, seconds = int(remaining // 60), int(remaining % 60)
        days, hours = int(days), int(hours)
        if days > 0:
            time_str = t("cli.shared.duration_dhm", days=days, hours=hours, minutes=minutes)
        elif hours > 0:
            time_str = (t("cli.shared.duration_hms", hours=hours, minutes=minutes, seconds=seconds) if seconds
                        else t("cli.shared.duration_hm", hours=hours, minutes=minutes))
        elif minutes > 0:
            time_str = (t("cli.shared.duration_ms", minutes=minutes, seconds=seconds) if seconds
                        else t("cli.shared.duration_m", minutes=minutes))
        else:
            time_str = t("cli.shared.duration_s", seconds=int(elapsed))
        return f"{'⏱' if live else '⏲'} {time_str}"

    @staticmethod
    def _format_idle_since(last_finished_at: Optional[float], turn_live: bool) -> str:
        """``✓ 42s`` since the last final response; empty while a turn is live or before the
        first turn completes."""
        from cli import format_duration_compact
        if turn_live or last_finished_at is None:
            return ""
        return f"✓ {format_duration_compact(max(0.0, time.time() - last_finished_at))}"

    def _get_status_bar_snapshot(self) -> Dict[str, Any]:
        from cli import _reverse_alias_for_display, datetime, format_duration_compact
        agent = getattr(self, "agent", None)
        # Prefer the agent's model name — it updates on fallback; self.model never changes.
        model_name = (getattr(agent, "model", None) or self.model or t("cli.shared.unknown"))
        # Friendly display: reverse-alias from config ``model_aliases:`` first (turns long
        # Palantir RIDs into the user's short name), else slash/length truncation.
        model_short = _reverse_alias_for_display(model_name)
        if model_short == model_name:
            model_short = model_name.split("/")[-1] if "/" in model_name else model_name
            # Shared RID-prefix stripper so this and ModelSwitchResult can't drift.
            from hermes_cli.model_switch import format_model_for_display
            model_short = format_model_for_display(model_short)
        if model_short.endswith(".gguf"):
            model_short = model_short[:-5]
        if len(model_short) > 26:
            model_short = f"{model_short[:23]}..."

        prompt_start = getattr(self, "_prompt_start_time", None)
        turn_live = prompt_start is not None
        elapsed_seconds = max(0.0, (datetime.now() - self.session_start).total_seconds())
        snapshot = {
            "model_name": model_name,
            "model_short": model_short,
            "duration": format_duration_compact(elapsed_seconds),
            "session_title": self._get_status_bar_session_title(),
            "prompt_elapsed": self._format_prompt_elapsed(
                prompt_start, getattr(self, "_prompt_duration", 0.0), live=turn_live),
            "idle_since": self._format_idle_since(
                getattr(self, "_last_turn_finished_at", None), turn_live=turn_live),
            "context_tokens": 0,
            "context_length": None,
            "context_percent": None,
            **dict.fromkeys(_AGENT_COUNTERS, 0),
            "compressions": 0,
            "active_background_tasks": 0,
            "active_background_processes": 0,
            "active_background_subagents": 0,
            "battery_label": "",
            "battery_category": "dim",
            "focus_label": "",  # /focus badge: the reduced-output mode is never invisible.
            "git_branch": "",
            "goal_active": False,
            "goal_turns_used": 0,
            "goal_max_turns": 0}

        try:
            from hermes_cli.focus_view import focus_statusbar_segment

            snapshot["focus_label"] = focus_statusbar_segment(
                bool(getattr(self, "_focus_view_enabled", False)))
        except Exception:
            pass

        # Git branch (⎇) — opt-in via display.status_bar.fields, so the filesystem probe
        # (TTL-cached in status_bar_git) only runs when the user asked for the segment.
        try:
            _fields = self._get_status_bar_field_set()
            if _fields is not None and "git_branch" in _fields:
                from hermes_cli.status_bar_git import current_git_branch

                snapshot["git_branch"] = current_git_branch()
        except Exception:
            pass

        # Battery reads are memoised inside agent.battery, so per-repaint polling is cheap.
        if getattr(self, "_battery_visible", False):
            try:
                from agent.battery import battery_category, format_battery, read_battery

                _batt = read_battery()
                snapshot["battery_label"] = format_battery(_batt)
                snapshot["battery_category"] = battery_category(_batt)
            except Exception:
                pass

        # Live /bg tasks: entries are removed in the task thread's finally block; dict len()
        # is atomic in CPython, no lock needed.
        try:
            bg_tasks = getattr(self, "_background_tasks", None)
            if bg_tasks:
                snapshot["active_background_tasks"] = len(bg_tasks)
        except Exception:
            pass
        try:
            from tools.process_registry import process_registry
            snapshot["active_background_processes"] = process_registry.count_running()
        except Exception:
            pass
        try:
            from tools.async_delegation import active_count as _async_active_count
            snapshot["active_background_subagents"] = _async_active_count()
        except Exception:
            pass

        # Standing /goal (Ralph loop): GoalManager is cached on self — no DB hit per repaint.
        # Only an *active* goal earns a segment (paused/done stay out, like the desktop).
        try:
            goal_mgr = self._get_goal_manager()
            if goal_mgr is not None and goal_mgr.is_active():
                goal_state = goal_mgr.state
                snapshot["goal_active"] = True
                snapshot["goal_turns_used"] = int(getattr(goal_state, "turns_used", 0) or 0)
                snapshot["goal_max_turns"] = int(getattr(goal_state, "max_turns", 0) or 0)
        except Exception:
            pass

        if not agent:
            return snapshot

        for key in _AGENT_COUNTERS:
            snapshot[key] = getattr(agent, key, 0) or 0

        compressor = getattr(agent, "context_compressor", None)
        if compressor:
            # last_prompt_tokens parks at the -1 sentinel right after a compression until the
            # next real API call; clamp so the bar never renders "-1/200K".
            context_tokens = max(0, getattr(compressor, "last_prompt_tokens", 0) or 0)
            from agent.context_breakdown import context_display_source
            snapshot["context_estimated"] = context_display_source(compressor) != "provider_usage"
            # Display-only anchoring: on reasoning models a long tool loop replays the turn's
            # thinking on every request, so the LAST request's prompt_tokens can exceed the
            # durable transcript by hundreds of K and the bar sawtooths at the turn boundary.
            # Anchor on the turn's FIRST response plus a delta estimate of appended messages.
            # The compression trigger keeps using real last-request usage.
            try:
                from agent.usage_anchor import anchored_context_tokens

                _msgs = getattr(agent, "_session_messages", None)
                _anchored = anchored_context_tokens(
                    _msgs if isinstance(_msgs, list) else [],
                    getattr(agent, "_turn_base_usage_anchor", None),
                    charge_stale_thinking=False)
                if _anchored is not None and _anchored > 0:
                    context_tokens = _anchored
                    anchor = agent._turn_base_usage_anchor
                    delta = _msgs[int(anchor["base_count"]):]
                    if delta and delta[0].get("role") == "assistant":
                        delta = delta[1:]
                    snapshot["context_estimated"] = bool(delta)
            except Exception:
                pass
            context_length = max(0, getattr(compressor, "context_length", 0) or 0)
            if context_length:
                context_tokens = min(context_tokens, context_length)
            snapshot["context_tokens"] = context_tokens
            snapshot["context_length"] = context_length or None
            from agent.context_pin import is_context_pinned
            snapshot["context_pinned"] = is_context_pinned(
                context_length, getattr(compressor, "_config_context_length", None))
            snapshot["compressions"] = getattr(compressor, "compression_count", 0) or 0
            if context_length:
                pct = round((context_tokens / context_length) * 100)
                snapshot["context_percent"] = max(0, min(100, pct))

        # Cache-hit ratio since the last baseline reset (model switch and compression both
        # invalidate the prompt cache). hit = cache_read / prompt_tokens, where
        # prompt = input + cache_read + cache_write (CanonicalUsage).
        pct = None
        try:
            base_model = getattr(self, "_cache_hit_baseline_model", None)
            base_prompt = int(getattr(self, "_cache_hit_baseline_prompt", 0) or 0)
            base_read = int(getattr(self, "_cache_hit_baseline_read", 0) or 0)
            base_comps = int(getattr(self, "_cache_hit_baseline_compressions", 0) or 0)
            cur_comps = int(snapshot["compressions"] or 0)
            cur_prompt = int(snapshot["session_prompt_tokens"] or 0)
            cur_read = int(snapshot["session_cache_read_tokens"] or 0)

            def _rebase(*, tokens: bool) -> None:
                nonlocal base_prompt, base_read, base_comps
                self._cache_hit_baseline_model = model_name
                self._cache_hit_baseline_compressions = base_comps = cur_comps
                if tokens:
                    self._cache_hit_baseline_prompt = base_prompt = cur_prompt
                    self._cache_hit_baseline_read = base_read = cur_read

            if base_model is None:
                _rebase(tokens=False)
            elif model_name != base_model:
                _rebase(tokens=True)
            if cur_comps != base_comps:
                _rebase(tokens=True)
            delta_prompt = cur_prompt - base_prompt
            delta_read = cur_read - base_read
            # A zero-read regime hides the segment (no data ≠ an alarming 0%); pct stays a
            # float so renderers choose their own precision.
            if delta_prompt > 0 and delta_read > 0:
                pct = max(0.0, min(100.0, (delta_read / delta_prompt) * 100))
        except Exception:
            pct = None
        snapshot["cache_hit_pct"] = pct
        snapshot["cache_hit_label"] = f"{pct:.0f}%" if pct is not None else ""

        # Rolling avg latency / velocity over the deques kept by agent/conversation_loop.py
        # (hidden on Codex app-server, which reports no latency).
        avg_lat = avg_vel = None
        try:
            lhist = list(getattr(agent, "_api_latency_history", []) or [])
            ohist = list(getattr(agent, "_api_output_history", []) or [])
            n = min(len(lhist), len(ohist))  # appended together; keep aligned
            if n:
                lhist, ohist = lhist[-n:], ohist[-n:]
                total_lat = sum(lhist)
                # Mean for latency; sum/sum for velocity (true throughput, not mean of ratios).
                avg_lat = _finite(total_lat / n)
                avg_vel = _finite(sum(ohist) / total_lat if total_lat > 0 else None)
        except Exception:
            avg_lat = avg_vel = None
        snapshot["avg_latency"] = float(avg_lat) if avg_lat is not None else None
        snapshot["avg_latency_label"] = t("cli.status_bar.latency_label", value=f"{avg_lat:.1f}") if avg_lat is not None else ""
        snapshot["avg_velocity"] = float(avg_vel) if avg_vel is not None else None
        snapshot["avg_velocity_label"] = t("cli.status_bar.velocity_label", value=f"{avg_vel:.0f}") if avg_vel is not None else ""
        return snapshot

    def _get_status_bar_session_title(self) -> str:
        """Current title, polling state.db at most every 1.5s (not on every repaint)."""
        pending = str(getattr(self, "_pending_title", None) or "").strip()
        session_id = str(getattr(self, "session_id", "") or "")
        now = time.monotonic()
        checked_at = float(getattr(self, "_status_bar_title_checked_at", 0.0) or 0.0)
        cache_fresh = (
            getattr(self, "_status_bar_title_session_id", None) == session_id and now - checked_at < 1.5
        )
        if not pending and cache_fresh:
            return str(getattr(self, "_status_bar_title_cache", "") or "")
        title = pending
        db = getattr(self, "_session_db", None)
        if not pending and db is not None and session_id:
            try:
                title = str(db.get_session_title(session_id) or "").strip()
            except Exception:
                title = ""
        self._status_bar_title_session_id = session_id
        self._status_bar_title_cache = title
        self._status_bar_title_checked_at = now
        return title

    @staticmethod
    def _status_bar_display_width(text: str) -> int:
        """Terminal cell width (some glyphs render wider than one codepoint); keeps the bar
        from wrapping onto a second line and leaving duplicate rows."""
        try:
            from prompt_toolkit.utils import get_cwidth
            return get_cwidth(text or "")
        except Exception:
            return len(text or "")

    @classmethod
    def _trim_status_bar_text(cls, text: str, max_width: int) -> str:
        """Trim status-bar text to a single terminal row."""
        if max_width <= 0:
            return ""
        cw = cls._status_bar_display_width
        if cw(text) <= max_width:
            return text
        ellipsis_width = cw("...")
        if max_width <= ellipsis_width:
            return "..."[:max_width]
        out, width = [], 0
        for ch in text:
            if width + cw(ch) + ellipsis_width > max_width:
                break
            out.append(ch)
            width += cw(ch)
        return "".join(out).rstrip() + "..."

    @classmethod
    def _status_title_badge(cls, title: str, width: int) -> "tuple[str, int] | None":
        """(badge, left_width) for the far-right session-title badge, or None when it
        doesn't fit (no title / bar narrower than 24 cells)."""
        title = str(title or "").strip()
        if not title or width < 24:
            return None
        title_width = max(6, min(30, width // 3))
        badge = f" {cls._trim_status_bar_text(title, title_width - 2)} "
        suffix_width = cls._status_bar_display_width(" ─") + cls._status_bar_display_width(badge)
        return badge, max(0, width - suffix_width)

    @classmethod
    def _right_align_status_title(cls, text: str, title: str, width: int) -> str:
        """Pin a bounded session-title badge to the far-right status-bar edge."""
        placed = cls._status_title_badge(title, width)
        if placed is None:
            return cls._trim_status_bar_text(text, width)
        badge, left_width = placed
        left = cls._trim_status_bar_text(text.rstrip(), left_width)
        padding = " " * max(0, left_width - cls._status_bar_display_width(left))
        return f"{left}{padding} ─{badge}"

    @classmethod
    def _right_align_status_title_fragments(cls, frags, title: str, width: int):
        """Styled counterpart to :meth:`_right_align_status_title`."""
        placed = cls._status_title_badge(title, width)
        if placed is None:
            return frags
        badge, left_width = placed
        trimmed = []
        used = 0
        for style, value in frags:
            remaining = left_width - used
            if remaining <= 0:
                break
            value_width = cls._status_bar_display_width(value)
            if value_width <= remaining:
                trimmed.append((style, value))
                used += value_width
                continue
            clipped = cls._trim_status_bar_text(value, remaining)
            if clipped:
                trimmed.append((style, clipped))
                used += cls._status_bar_display_width(clipped)
            break
        if used < left_width:
            trimmed.append((_DIM, " " * (left_width - used)))
        trimmed.extend([(_DIM, " ─"), ("class:status-bar-session-title", badge)])
        return trimmed

    @staticmethod
    def _get_tui_terminal_width(default: tuple[int, int] = (80, 24)) -> int:
        """Live prompt_toolkit width (can be narrower than shutil's, esp. Termux), falling
        back to ``shutil``."""
        try:
            from prompt_toolkit.application import get_app
            return get_app().output.get_size().columns
        except Exception:
            return shutil.get_terminal_size(default).columns

    def _use_minimal_tui_chrome(self, width: Optional[int] = None) -> bool:
        """Hide low-value chrome on narrow/mobile terminals to preserve rows."""
        if width is None:
            width = self._get_tui_terminal_width()
        return width < 64

    @staticmethod
    def _scrollback_box_width(width: Optional[int] = None) -> int:
        """Full viewport width for printed scrollback box rules, floored at 32 cols so tiny
        terminals never hit negative ``'─' * (w - 2)`` math. (The old 56-col clamp against
        reflow-on-shrink is gone: the ``_output_screen_diff`` patch keeps chrome out of
        scrollback, and reflow of already-printed borders is a cosmetic artifact.)

        Previously this clamped to ``max(32, min(width, 56))`` as a defense against terminal-emulator reflow
        on column-shrink (#25975, salvaging 24403). That clamp made response/reasoning borders look stubby
        on any modern wide terminal. We now trust the prompt_toolkit ``_output_screen_diff`` monkey-patch
        landed in #26137 (salvaging 25981) to keep chrome out of scrollback in the first place, and accept
        that an aggressive column-shrink may visually reflow already printed Panel borders — that's a
        cosmetic artifact of stamped scrollback history, not a live-render bug.
        """
        if width is None:
            try:
                width = shutil.get_terminal_size((80, 24)).columns
            except Exception:
                width = 80
        return max(32, int(width or 80))

    def _agent_spacer_height(self, width: Optional[int] = None) -> int:
        """Spacer height above the status bar while the agent runs."""
        if not getattr(self, "_agent_running", False):
            return 0
        return 0 if self._use_minimal_tui_chrome(width=width) else 1

    def _spinner_widget_height(self, width: Optional[int] = None) -> int:
        """Visible height of the spinner/status line above the status bar."""
        spinner_line = self._render_spinner_text()
        if not spinner_line or self._use_minimal_tui_chrome(width=width):
            return 0
        width = width or self._get_tui_terminal_width()
        if width and width > 10:
            return max(1, -(-self._status_bar_display_width(spinner_line) // width))
        return 1

    def _render_spinner_text(self) -> str:
        """The live spinner/status text exactly as rendered in the TUI."""
        txt = getattr(self, "_spinner_text", "")
        if not txt:
            return ""
        flow = self._spinner_token_flow()
        t0 = getattr(self, "_tool_start_time", 0) or 0
        if t0 > 0:
            elapsed = time.monotonic() - t0
            # Fixed-width timers (01m05s / " 5.2s") avoid status-line wrap jitter on repaint.
            if elapsed >= 60:
                elapsed_str = t("cli.shared.duration_ms_compact",
                                minutes=f"{int(elapsed // 60):02d}", seconds=f"{int(elapsed % 60):02d}")
            else:
                elapsed_str = t("cli.shared.duration_s", seconds=f"{elapsed:5.1f}")
            return f"  {txt}  ({elapsed_str} · {flow})" if flow else f"  {txt}  ({elapsed_str})"
        return f"  {txt}  ({flow})" if flow else f"  {txt}"

    def _spinner_token_flow(self) -> str:
        """Cumulative output tokens for the running turn, for the spinner."""
        if not getattr(self, "_spinner_token_flow_enabled", False):
            return ""
        if not getattr(self, "_agent_running", False):
            return ""
        agent = getattr(self, "agent", None)
        if agent is None:
            return ""
        try:
            from agent.turn_summary import format_token_flow

            produced = (getattr(agent, "session_output_tokens", 0) or 0) - (
                getattr(self, "_turn_token_baseline", 0) or 0)
            return format_token_flow(produced)
        except Exception:
            return ""

    def _turn_summary_is_active(self) -> bool:
        """Whether the per-turn summary line renders here: off for the config key, quiet /
        tool-progress-off mode, and every non-interactive path (-q, -Q, gateway)."""
        if not getattr(self, "_turn_summary_enabled", False):
            return False
        if getattr(self, "tool_progress_mode", "all") == "off":
            return False
        agent = getattr(self, "agent", None)
        if agent is not None and getattr(agent, "quiet_mode", False):
            return False
        return bool(getattr(self, "_interactive_turn", False))

    def _turn_summary_begin(self) -> None:
        """Start per-turn accounting for the turn that is about to run."""
        try:
            from agent.turn_summary import TurnSummaryCollector

            collector = getattr(self, "_turn_summary_collector", None)
            if collector is None:
                collector = TurnSummaryCollector()
                self._turn_summary_collector = collector
            collector.begin()
            self._turn_summary_start = time.monotonic()
            agent = getattr(self, "agent", None)
            self._turn_token_baseline = (
                getattr(agent, "session_output_tokens", 0) or 0
            ) if agent is not None else 0
        except Exception:
            self._turn_summary_collector = None

    def _turn_summary_record(self, function_name, result, is_error: bool) -> None:
        """Feed one completed tool call into the active tally."""
        collector = getattr(self, "_turn_summary_collector", None)
        if collector is None:
            return
        try:
            collector.record_tool(function_name, result=result, is_error=bool(is_error))
        except Exception:
            pass

    def _turn_summary_emit(self) -> None:
        """Print the post-turn accounting line, when enabled for this surface."""
        from cli import _DIM as _D, _RST, _cprint, logger
        collector = getattr(self, "_turn_summary_collector", None)
        if collector is None or not self._turn_summary_is_active():
            return
        try:
            started = getattr(self, "_turn_summary_start", 0.0) or 0.0
            line = collector.render(max(0.0, time.monotonic() - started) if started else 0.0)
            if line:
                _cprint(f"  {_D}{line}{_RST}")
        except Exception:
            logger.debug("Turn summary render failed", exc_info=True)

    # ── pet pane ──────────────────────────────────────────────────────────────

    def _pet_clear_runtime(self) -> None:
        """Drop renderer + queued Kitty state. Caller holds ``_pet_lock``."""
        self._pet_enabled = False
        self._pet_renderer = None
        self._pet_frames_cache.clear()
        self._pet_kitty_cache.clear()
        self._pet_kitty_pending = ""
        self._pet_kitty_image_id = 0

    def _pet_resolve_config(self) -> None:
        """(Re)resolve the active pet from config so ``/pet`` / ``hermes pets`` changes apply
        without a restart (mirrors the TUI's steady poll). Fail-open: any problem disables."""
        try:
            from agent.pet import constants, store
            from hermes_cli.config import load_config
            from utils import is_truthy_value

            cfg = load_config()
            display = cfg.get("display", {}) if isinstance(cfg.get("display"), dict) else {}
            pet_cfg = display.get("pet", {}) if isinstance(display.get("pet"), dict) else {}
            enabled = is_truthy_value(pet_cfg.get("enabled"), default=False)
            slug = str(pet_cfg.get("slug", "") or "")
            scale = float(pet_cfg.get("scale", constants.DEFAULT_SCALE) or constants.DEFAULT_SCALE)
            cols = constants.resolve_cols(scale, pet_cfg.get("unicode_cols", 0))
            configured_mode = str(pet_cfg.get("render_mode", "auto") or "auto").lower()
            # Placeholders only on kitty/Ghostty: WezTerm speaks kitty APC but not U+10EEEE
            # while detect_terminal_graphics() still says kitty, hence the narrower gate.
            use_kitty = (
                configured_mode in ("", "auto", "kitty") and pet_render.supports_kitty_placeholders()
            )
            renderer_mode = "kitty" if use_kitty else "unicode"

            pet = None
            if enabled and configured_mode != "off":
                pet = store.resolve_active_pet(slug)
            if pet is None or not pet.exists:
                with self._pet_lock:
                    self._pet_clear_runtime()
                return

            with self._pet_lock:
                # Rebuild only when the resolved pet, mode, or geometry changes.
                if (
                    self._pet_renderer is None
                    or self._pet_slug != pet.slug
                    or self._pet_cols != cols
                    or self._pet_scale != scale
                    or self._pet_renderer.mode != renderer_mode):
                    self._pet_renderer = pet_render.PetRenderer(
                        str(pet.spritesheet), mode=renderer_mode, scale=scale, unicode_cols=cols)
                    self._pet_slug = pet.slug
                    self._pet_cols = cols
                    self._pet_scale = scale
                    self._pet_frames_cache.clear()
                    self._pet_kitty_cache.clear()
                    self._pet_kitty_pending = ""
                    self._pet_kitty_image_id = pet_render.kitty_image_id(pet.slug)
                    self._pet_frame_idx = 0
                self._pet_enabled = True
        except Exception:
            with self._pet_lock:
                self._pet_clear_runtime()

    def _pet_flash(self, state: str, secs: float = 1.6) -> None:
        """Briefly force a transient reaction (wave/jump/failed) before resting."""
        self._pet_event = state
        self._pet_event_until = time.monotonic() + secs

    def _on_reaction(self, kind: str) -> None:
        """Core-detected user affection (ily / <3 / good bot): the pet's share of the vibe
        signal that plays hearts on the TUI/desktop."""
        if kind == "vibe":
            self._pet_flash("jump")

    def _pet_react_turn_end(self) -> None:
        """End-of-turn beat: failed on error, jump on a finished plan, else wave."""
        if not self._pet_enabled:
            return
        from agent.pet.state import todos_all_done

        if self._pet_turn_error:
            self._pet_flash("failed")
            return
        try:
            store = getattr(self.agent, "_todo_store", None)
            done = todos_all_done(store.read()) if store else False
        except Exception:
            done = False
        self._pet_flash("jump" if done else "wave")

    def _derive_pet_state(self) -> str:
        """A live transient beat wins; otherwise the shared ``agent.pet.state.derive_pet_state``
        priority order so the CLI can't drift from the TUI/desktop."""
        if self._pet_event and time.monotonic() < self._pet_event_until:
            return self._pet_event
        self._pet_event = ""
        from agent.pet.state import derive_pet_state

        # Any blocking modal (approval / clarify / sudo / secret / slash confirm) means the
        # agent is paused on the user → `waiting`, which outranks the in-flight signals.
        awaiting_input = bool(
            self._approval_state
            or self._clarify_state
            or self._sudo_state
            or self._secret_state
            or getattr(self, "_slash_confirm_state", None))
        return derive_pet_state(
            awaiting_input=awaiting_input,
            busy=getattr(self, "_agent_running", False),
            reasoning=self._pet_reasoning,
        ).value

    def _pet_frames_for(self, state: str) -> list:
        """Return (and cache) the half-block grids for one state."""
        cached = self._pet_frames_cache.get(state)
        if cached is not None:
            return cached
        renderer = self._pet_renderer
        if renderer is None:
            return []
        try:
            count = renderer.frame_count(state) or 1
            grids = [renderer.cells(state, i, cols=self._pet_cols) for i in range(count)]
        except Exception:
            grids = []
        self._pet_frames_cache[state] = grids
        return grids

    def _pet_kitty_payload_for(self, state: str) -> dict | None:
        """Return and cache a Kitty virtual-placeholder payload for *state*."""
        with self._pet_lock:
            cached = self._pet_kitty_cache.get(state)
            if cached is not None:
                return cached
            renderer = self._pet_renderer
            image_id = self._pet_kitty_image_id
            if renderer is None or renderer.mode != "kitty":
                return None
        try:
            # PNG encoding outside _pet_lock: first visit of a state must not stall the prompt.
            payload = renderer.kitty_payload(state, image_id=image_id)
        except Exception:
            payload = None
        if payload is not None:
            payload = {**payload, "image_id": image_id}
            with self._pet_lock:
                if self._pet_renderer is renderer and self._pet_kitty_image_id == image_id:
                    self._pet_kitty_cache[state] = payload
        return payload

    def _pet_queue_kitty_frame(self, state: str | None = None) -> None:
        """Queue one virtual Kitty frame for the next prompt_toolkit render. No-op when the
        pet pane was never initialized (``__new__`` fixtures, redraw on a pet-less CLI)."""
        if not getattr(self, "_pet_enabled", False):
            return
        if state is None:
            state = self._derive_pet_state()
        payload = self._pet_kitty_payload_for(state)
        if not payload or not payload.get("frames"):
            return
        with self._pet_lock:
            if self._pet_renderer is not None and self._pet_renderer.mode == "kitty":
                frames = payload["frames"]
                self._pet_kitty_pending = frames[self._pet_frame_idx % len(frames)]

    def _pet_flush_kitty_frame(self, app) -> None:
        """Write a queued APC after prompt_toolkit has finished its screen diff."""
        with self._pet_lock:
            frame = self._pet_kitty_pending
            self._pet_kitty_pending = ""
        if not frame:
            return
        try:
            # U=1/q=2 leaves the cursor and input stream untouched.
            app.output.write_raw(frame)
            app.output.flush()
        except (OSError, ValueError):
            pass

    def _pet_view(self) -> "tuple[str, bool] | None":
        """(state, is_kitty) for the current frame, or None when no pet shows."""
        with self._pet_lock:
            if not self._pet_enabled or self._pet_renderer is None:
                return None
            return self._derive_pet_state(), self._pet_renderer.mode == "kitty"

    def _pet_fragments(self):
        """prompt_toolkit FormattedText for the current pet frame, or []."""
        view = self._pet_view()
        if view is None:
            return []
        state, kitty = view
        frags = []
        if kitty:
            payload = self._pet_kitty_payload_for(state)
            if not payload:
                return []
            color = pet_render.kitty_color_hex(payload["image_id"])
            for y, row in enumerate(payload["placeholder"]):
                if y:
                    frags.append(("", "\n"))
                frags.append((f"fg:{color}", row))
            return frags
        with self._pet_lock:
            grids = self._pet_frames_for(state)
            if not grids:
                return []
            grid = grids[self._pet_frame_idx % len(grids)]

        def _hex(r, g, b):
            return f"#{r:02x}{g:02x}{b:02x}"

        for y, row in enumerate(grid):
            if y:
                frags.append(("", "\n"))
            for (tr, tg, tb, ta), (br, bg, bb, ba) in row:
                top_op, bot_op = ta >= 32, ba >= 32
                if not top_op and not bot_op:
                    frags.append(("", " "))
                elif top_op and bot_op:
                    frags.append((f"fg:{_hex(tr, tg, tb)} bg:{_hex(br, bg, bb)}", "▀"))
                elif top_op:
                    # Upper half only — leave the lower half the terminal's bg (cleaner on light
                    # themes).
                    frags.append((f"fg:{_hex(tr, tg, tb)}", "▀"))
                else:
                    frags.append((f"fg:{_hex(br, bg, bb)}", "▄"))
        return frags

    def _pet_widget_height(self) -> int:
        """Visible rows for the pet window — 0 collapses it when no pet shows."""
        view = self._pet_view()
        if view is None:
            return 0
        state, kitty = view
        if kitty:
            payload = self._pet_kitty_payload_for(state)
            return int(payload.get("rows", 0)) if payload else 0
        with self._pet_lock:
            grids = self._pet_frames_for(state)
            return len(grids[0]) if grids and grids[0] else 0

    def _pet_anim_loop(self) -> None:
        """Advance the frame + invalidate on a timer while a pet is enabled."""
        while self._pet_anim_running:
            time.sleep(self._PET_FRAME_INTERVAL)
            if getattr(self, "_terminal_io_broken", False):
                self._pet_anim_running = False
                break
            now = time.monotonic()
            if now - self._pet_cfg_checked >= self._PET_CFG_INTERVAL:
                self._pet_cfg_checked = now
                self._pet_resolve_config()
            if not self._pet_enabled:
                continue
            with self._pet_lock:
                self._pet_frame_idx += 1
                kitty = self._pet_renderer is not None and self._pet_renderer.mode == "kitty"
            if kitty:
                self._pet_queue_kitty_frame()
            app = getattr(self, "_app", None)
            if app is not None:
                try:
                    app.invalidate()
                except OSError as exc:
                    if getattr(exc, "errno", None) == errno.EIO:
                        self._mark_terminal_io_broken("pet_anim")
                        break
                except Exception:
                    pass

    def _pet_start_anim(self) -> None:
        if self._pet_anim_running:
            return
        self._pet_resolve_config()
        view = self._pet_view()
        if view is not None and view[1]:
            self._pet_queue_kitty_frame()
        self._pet_anim_running = True
        self._pet_anim_thread = threading.Thread(target=self._pet_anim_loop, daemon=True)
        self._pet_anim_thread.start()

    def _pet_stop_anim(self) -> None:
        self._pet_anim_running = False
        thread = self._pet_anim_thread
        if thread is not None:
            thread.join(timeout=0.3)
        self._pet_anim_thread = None

    # ── voice ─────────────────────────────────────────────────────────────────

    def _voice_record_key_label(self) -> str:
        """The push-to-talk key label every voice-facing hint advertises. Cached at startup
        (``set_voice_record_key_cache``) because the prompt_toolkit binding is registered once —
        re-reading config per render could advertise a chord that isn't bound — and this sits on
        the hot render path.

        Two reasons (Copilot round-13 on 19835): See #19835.
        """
        return getattr(self, "_voice_record_key_display_cache", None) or "Ctrl+B"

    def set_voice_record_key_cache(self, raw_key: object) -> None:
        """Populate the voice label cache from a raw ``voice.record_key``; called after the
        prompt_toolkit binding is registered so the label matches the live binding."""
        try:
            from hermes_cli.voice import format_voice_record_key_for_status
            self._voice_record_key_display_cache = format_voice_record_key_for_status(raw_key)
        except Exception:
            self._voice_record_key_display_cache = "Ctrl+B"

    def _get_voice_status_fragments(self, width: Optional[int] = None):
        """Voice status bar fragments for the interactive TUI."""
        width = width or self._get_tui_terminal_width()
        compact = self._use_minimal_tui_chrome(width=width)
        label = self._voice_record_key_label()
        if self._voice_recording:
            if compact:
                return [("class:voice-status-recording", f" {t('cli.status_bar.voice_rec')} ")]
            return [("class:voice-status-recording", f" {t('cli.status_bar.voice_rec_stop_hint', shortcut=label)} ")]
        if self._voice_processing:
            return [("class:voice-status",
                     f" {t('cli.status_bar.voice_stt')} " if compact else f" {t('cli.status_bar.voice_transcribing')} ")]
        if compact:
            return [("class:voice-status", f" 🎤 {label} ")]
        tts = t("cli.status_bar.voice_tts_on") if self._voice_tts else ""
        cont = t("cli.status_bar.voice_continuous") if self._voice_continuous else ""
        return [("class:voice-status", f" {t('cli.status_bar.voice_mode_hint', tts=tts, cont=cont, shortcut=label)} ")]

    # ── status bar rendering ──────────────────────────────────────────────────

    @staticmethod
    def _status_bar_goal_segment(snapshot: Dict[str, Any]) -> str:
        """``⊙ goal 3/20`` while a goal is active, else ``""`` (paused/done goals already
        print their own glyph lines in the thread)."""
        if not snapshot.get("goal_active"):
            return ""
        used = snapshot.get("goal_turns_used") or 0
        max_turns = snapshot.get("goal_max_turns") or 0
        return t("cli.status_bar.goal_turns", used=used, max=max_turns) if max_turns else t("cli.status_bar.goal")

    def _get_status_bar_field_set(self) -> Optional[frozenset]:
        """Visible status-bar fields from ``display.status_bar.fields`` (module-level
        ``CLI_CONFIG``; no per-render YAML parse). ``None`` = not customized, show everything.

        Fields: model, context_detail, context_pct, cache_hit, latency, tps, compressions,
        bg_tasks, bg_processes, bg_subagents, goal, git_branch (opt-in only), duration,
        prompt_elapsed, idle_since, focus, yolo, stash, battery, title, total_tokens
        (opt-in only). Order is fixed; the config controls visibility only.
        """
        from cli import CLI_CONFIG
        if hasattr(self, "_status_bar_field_set_cache"):
            return self._status_bar_field_set_cache
        result = None
        try:
            display = CLI_CONFIG.get("display") if isinstance(CLI_CONFIG, dict) else None
            status_bar = (display or {}).get("status_bar") if isinstance(display, dict) else None
            fields = status_bar.get("fields") if isinstance(status_bar, dict) else None
            if isinstance(fields, list) and fields:
                result = frozenset(str(f) for f in fields)
        except Exception:
            result = None
        self._status_bar_field_set_cache = result
        return result

    def _status_bar_segments(
        self, snapshot, width: int, field_set, yolo_active: bool, *, styled: bool) -> list:
        """Ordered status-bar segments for one width tier (<52 / <76 / wide), each a list of
        ``(style, text)`` fragments. Shared by the plain-text and prompt_toolkit renderers so
        the two can never drift; ``styled`` selects the graphical context bar."""
        from cli import format_token_count_compact
        model_short = snapshot["model_short"]
        duration_label = snapshot["duration"]
        goal_segment = self._status_bar_goal_segment(snapshot)
        focus_label = snapshot.get("focus_label") or ""

        def _ok(name: str) -> bool:
            return field_set is None or name in field_set

        segs: list = []

        def add(name, style, text):
            if _ok(name):
                segs.append([(style, text)])

        def add_count(name, key, glyph, style=_STRONG):
            count = snapshot.get(key, 0)
            if count:
                add(name, style(count) if callable(style) else style, f"{glyph} {count}")

        if _ok("model"):
            if styled:
                segs.append([(_SB, " ☤ "), (_STRONG, model_short)])
            else:
                segs.append([("", f"☤ {model_short}")])
        narrow, wide = width < 52, width >= 76
        if narrow:
            # Narrow bars put duration ahead of the goal segment; the other tiers reverse it.
            add("duration", _DIM, duration_label)
        else:
            percent = snapshot["context_percent"]
            mark = "~" if snapshot.get("context_estimated") else ""
            percent_label = f"{mark}{percent}%" if percent is not None else "--"
            if wide and _ok("context_detail"):
                if snapshot["context_length"]:
                    ctx_total = _format_context_length(snapshot["context_length"])
                    ctx_used = format_token_count_compact(snapshot["context_tokens"])
                    pin = t("cli.status_bar.ctx_pinned") if snapshot.get("context_pinned") else ""
                    context_label = f"{mark}{ctx_used}/{ctx_total}{pin}"
                else:
                    context_label = t("cli.status_bar.ctx_unknown")
                segs.append([(_DIM, context_label)])
            if _ok("context_pct"):
                bar_style = self._status_bar_context_style(percent)
                if wide and styled:
                    segs.append([
                        (bar_style, self._build_context_bar(percent)), (_DIM, " "), (bar_style, percent_label),
                    ])
                else:
                    segs.append([(bar_style, percent_label)])
            cache = self._cache_hit_rate(snapshot, precision=1 if wide else 0)
            if cache:
                add("cache_hit", self._cache_hit_rate_style(cache[0]), cache[1])
            if wide:
                for name, key, glyph in (
                    ("latency", "avg_latency_label", "◷"), ("tps", "avg_velocity_label", "↑")):
                    label = snapshot.get(key) or ""
                    if label:
                        add(name, _DIM, f"{glyph} {label}")
            add_count("compressions", "compressions", "🗜️", self._compression_count_style)
            add_count("bg_tasks", "active_background_tasks", "▶")
            add_count("bg_processes", "active_background_processes", "⚙")
            add_count("bg_subagents", "active_background_subagents", "⛓")
        if goal_segment:
            add("goal", _STRONG, goal_segment)
        git_branch = snapshot.get("git_branch") or ""
        if git_branch:
            add("git_branch", _DIM, f"⎇ {git_branch}")
        if not narrow:
            add("duration", _DIM, duration_label)
        if wide:
            for name in ("prompt_elapsed", "idle_since"):
                label = snapshot.get(name)
                if label:
                    add(name, _DIM, label)
        if focus_label:
            add("focus", _STRONG, focus_label)
        if yolo_active:
            add("yolo", "class:status-bar-yolo", t("cli.status_bar.yolo_badge"))
        if wide:
            # Session token total (Σ) — opt-in only via an explicit fields list.
            total_tokens = snapshot.get("session_total_tokens", 0)
            if total_tokens and field_set is not None and "total_tokens" in field_set:
                segs.append([(_DIM, f"Σ{format_token_count_compact(total_tokens)}")])
        return segs

    def _build_status_bar_text(self, width: Optional[int] = None) -> str:
        """Compact one-line session status string for the TUI footer."""
        try:
            snapshot = self._get_status_bar_snapshot()
            if width is None:
                width = self._get_tui_terminal_width()
            model_short = snapshot["model_short"]
            battery_label = snapshot.get("battery_label") or ""
            field_set = self._get_status_bar_field_set()
            show_title = field_set is None or "title" in field_set
            session_title = (snapshot.get("session_title") or "") if show_title else ""
            segs = self._status_bar_segments(
                snapshot, width, field_set, self._is_session_yolo_active(), styled=False)
            parts = ["".join(t for _, t in seg) for seg in segs] or [f"☤ {model_short}"]
            # Narrow bars always join the battery with │; wider tiers use the tier separator.
            if battery_label:
                parts.insert(0, battery_label)
            if width < 52:
                text = f"{parts[0]} │ " + " · ".join(parts[1:]) if battery_label else " · ".join(parts)
            else:
                text = (" · " if width < 76 else " │ ").join(parts)
            return self._right_align_status_title(text, session_title, width)
        except Exception:
            return f"☤ {self.model if getattr(self, 'model', None) else 'Hermes'}"

    def _get_status_bar_fragments(self):
        if (
            not self._status_bar_visible
            or getattr(self, "_model_picker_state", None)
            or getattr(self, "_command_palette_state", None)):
            return []
        try:
            snapshot = self._get_status_bar_snapshot()
            # prompt_toolkit's own width: shutil's can be stale (esp. over SSH) and an overflow
            # produces duplicated status-bar rows over long sessions.
            width = self._get_tui_terminal_width()
            field_set = self._get_status_bar_field_set()

            def _ok(name: str) -> bool:
                return field_set is None or name in field_set

            session_title = (snapshot.get("session_title") or "") if _ok("title") else ""
            segs = self._status_bar_segments(
                snapshot, width, field_set, self._is_session_yolo_active(), styled=True)
            sep = " · " if width < 76 else " │ "
            frags: list = []
            for seg in segs or [[(_SB, " ☤ "), (_STRONG, snapshot["model_short"])]]:
                if frags:
                    frags.append((_DIM, sep))
                frags.extend(seg)
            # Stash indicator (📌 N) after every width tier so a parked draft is never
            # invisible; before the battery prepend, and the first thing the trim drops.
            try:
                stash_indicator = self._prompt_stash.indicator()
            except Exception:
                stash_indicator = ""
            if stash_indicator and _ok("stash"):
                frags.extend([(_DIM, " · "), (_STRONG, stash_indicator)])
            frags.append((_SB, " "))  # one-cell right margin
            # Battery is the first element when enabled: prepend ahead of the ☤ marker.
            battery_label = snapshot.get("battery_label") or ""
            if battery_label and _ok("battery"):
                battery_style = self._battery_status_style(snapshot.get("battery_category", "dim"))
                frags[0:0] = [(_SB, " "), (battery_style, battery_label), (_DIM, " │")]

            frags = self._right_align_status_title_fragments(frags, session_title, width)
            vim_label = self._vim_mode_label()
            if vim_label:
                frags.extend([(_DIM, " │ "), (_STRONG, vim_label), (_SB, " ")])
            total_width = sum(self._status_bar_display_width(text) for _, text in frags)
            if total_width > width:
                plain_text = "".join(text for _, text in frags)
                return [(_SB, self._trim_status_bar_text(plain_text, width))]
            return frags
        except Exception:
            return [(_SB, f" {self._build_status_bar_text()} ")]

    # ── prompt stash panel ────────────────────────────────────────────────────

    @staticmethod
    def _fmt_stash_age(stashed_at: float) -> str:
        secs = int(time.monotonic() - stashed_at)
        if secs < 10:
            return t("cli.shared.just_now")
        if secs < 90:
            return t("cli.shared.ago_seconds", seconds=secs)
        mins = secs // 60
        return t("cli.shared.ago_minutes", minutes=mins) if mins < 60 else t("cli.shared.ago_hours", hours=mins // 60)

    def _render_stash_panel(self, stash_list: list, cursor: int, width: int) -> list:
        """prompt_toolkit fragments for the stash panel box. Every horizontal measurement uses
        ``_status_bar_display_width`` (display cells), not ``len()`` — 📌 is one codepoint but
        two cells, and CJK previews would otherwise bleed past the right border."""
        cw = self._status_bar_display_width
        W = max(12, min(width - 4, 80))

        n = len(stash_list)
        hdr_key = "cli.status_bar.stash_header_one" if n == 1 else "cli.status_bar.stash_header_other"
        hdr_prefix_str = f"╭─ {t(hdr_key, count=n)} "
        HDR_SUFFIX = " Ctrl+S ─╮"
        FTR_PREFIX = "╰"
        FTR_SUFFIX = f" {t('cli.status_bar.stash_footer')} ─╯"

        # On narrow terminals the full hint text is wider than the box: drop to compact
        # affordances rather than letting the frame bleed past the right edge.
        if cw(hdr_prefix_str) + cw(HDR_SUFFIX) > W:
            hdr_prefix_str = f"╭─ 📌 {n} "
            HDR_SUFFIX = "─╮"
        if cw(FTR_PREFIX) + cw(FTR_SUFFIX) > W:
            FTR_SUFFIX = f" {t('cli.status_bar.stash_footer_compact')} ─╯"
        if cw(FTR_PREFIX) + cw(FTR_SUFFIX) > W:
            FTR_SUFFIX = "─╯"

        hdr_dashes = max(0, W - cw(hdr_prefix_str) - cw(HDR_SUFFIX))
        ftr_dashes = max(0, W - cw(FTR_PREFIX) - cw(FTR_SUFFIX))
        INNER = W - 2  # minus the two '│' border cells
        frags: list = []

        def line(text: str, style: str = "") -> None:
            # Final guard: never emit a line wider than the box.
            frags.append((style, self._trim_status_bar_text(text, W) + "\n"))

        line(f"{hdr_prefix_str}{'─' * hdr_dashes}{HDR_SUFFIX}", "class:subagent-border")
        for i, item in enumerate(stash_list):
            age = self._fmt_stash_age(item["stashed_at"])
            marker = "►" if i == cursor else " "
            prefix = f" {marker} [{i + 1}] {age:<10} "
            if cw(prefix) > INNER - 2:
                prefix = f" {marker} [{i + 1}] "
            avail = max(0, INNER - cw(prefix) - 1)
            preview = self._trim_status_bar_text(item.get("preview") or "", avail)
            preview = preview + " " * max(0, avail - cw(preview))
            if i == cursor:
                row = self._trim_status_bar_text(f"│{prefix}{preview} │", W)
                frags.append(("class:subagent-selected", row + "\n"))
            else:
                frags.append(("class:subagent-border", "│"))
                frags.append(("class:subagent-sub", f"{prefix}{preview} "))
                frags.append(("class:subagent-border", "│\n"))
        line(f"{FTR_PREFIX}{'─' * ftr_dashes}{FTR_SUFFIX}", "class:subagent-border")
        return frags
