From 103e11926f2211f5662dac3cba0e458a00db3310 Mon Sep 17 00:00:00 2001 From: Sofia Yang Date: Fri, 1 May 2026 18:24:06 -0500 Subject: [PATCH] feat(cli): show context compression count in status bar Display the number of context compressions in the CLI status bar when compressions > 0, helping users understand conversation compression pressure during long sessions. - Wide layout (>=76 cols): shows 'cmp N' between context percent and duration - Medium layout (52-75 cols): shows 'cmp N' between percent and duration - Narrow layout (<52 cols): omitted to save space - Color-coded: dim for 1-4, warn for 5-9, bad for 10+ - Hidden when zero to keep the bar clean for new sessions Closes #18564 --- cli.py | 31 ++++++++- tests/cli/test_cli_status_bar.py | 112 +++++++++++++++++++++++++++++++ 2 files changed, 141 insertions(+), 2 deletions(-) diff --git a/cli.py b/cli.py index c93a5dd073..059e59a628 100644 --- a/cli.py +++ b/cli.py @@ -2571,6 +2571,15 @@ class HermesCLI: return "class:status-bar-warn" return "class:status-bar-good" + @staticmethod + def _compression_count_style(count: int) -> str: + """Return a style class reflecting context compression pressure.""" + if count >= 10: + return "class:status-bar-bad" + if count >= 5: + return "class:status-bar-warn" + return "class:status-bar-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) @@ -2854,6 +2863,9 @@ class HermesCLI: return self._trim_status_bar_text(text, width) if width < 76: parts = [f"⚕ {snapshot['model_short']}", percent_label] + compressions = snapshot.get("compressions", 0) + if compressions: + parts.append(f"cmp {compressions}") parts.append(duration_label) return self._trim_status_bar_text(" · ".join(parts), width) @@ -2864,7 +2876,10 @@ class HermesCLI: else: context_label = "ctx --" + compressions = snapshot.get("compressions", 0) parts = [f"⚕ {snapshot['model_short']}", context_label, percent_label] + if compressions: + parts.append(f"cmp {compressions}") parts.append(duration_label) prompt_elapsed = snapshot.get("prompt_elapsed") if prompt_elapsed: @@ -2898,15 +2913,21 @@ class HermesCLI: percent = snapshot["context_percent"] percent_label = f"{percent}%" if percent is not None else "--" if width < 76: + compressions = snapshot.get("compressions", 0) frags = [ ("class:status-bar", " ⚕ "), ("class:status-bar-strong", snapshot["model_short"]), ("class:status-bar-dim", " · "), (self._status_bar_context_style(percent), percent_label), + ] + if compressions: + frags.append(("class:status-bar-dim", " · ")) + frags.append((self._compression_count_style(compressions), f"cmp {compressions}")) + frags.extend([ ("class:status-bar-dim", " · "), ("class:status-bar-dim", duration_label), ("class:status-bar", " "), - ] + ]) else: if snapshot["context_length"]: ctx_total = _format_context_length(snapshot["context_length"]) @@ -2916,6 +2937,7 @@ class HermesCLI: context_label = "ctx --" bar_style = self._status_bar_context_style(percent) + compressions = snapshot.get("compressions", 0) frags = [ ("class:status-bar", " ⚕ "), ("class:status-bar-strong", snapshot["model_short"]), @@ -2925,9 +2947,14 @@ class HermesCLI: (bar_style, self._build_context_bar(percent)), ("class:status-bar-dim", " "), (bar_style, percent_label), + ] + if compressions: + frags.append(("class:status-bar-dim", " │ ")) + frags.append((self._compression_count_style(compressions), f"cmp {compressions}")) + frags.extend([ ("class:status-bar-dim", " │ "), ("class:status-bar-dim", duration_label), - ] + ]) # Position 7: per-prompt elapsed timer (live or frozen) prompt_elapsed = snapshot.get("prompt_elapsed") if prompt_elapsed: diff --git a/tests/cli/test_cli_status_bar.py b/tests/cli/test_cli_status_bar.py index ff99856a89..d3b4fb193e 100644 --- a/tests/cli/test_cli_status_bar.py +++ b/tests/cli/test_cli_status_bar.py @@ -207,6 +207,118 @@ class TestCLIStatusBar: assert "⚕" in text assert "claude-sonnet-4-20250514" in text + def test_compression_count_shown_in_wide_status_bar(self): + cli_obj = _attach_agent( + _make_cli(), + prompt_tokens=10_230, + completion_tokens=2_220, + total_tokens=12_450, + api_calls=7, + context_tokens=12_450, + context_length=200_000, + compressions=3, + ) + + text = cli_obj._build_status_bar_text(width=120) + + assert "cmp 3" in text + + def test_compression_count_hidden_when_zero(self): + cli_obj = _attach_agent( + _make_cli(), + prompt_tokens=10_230, + completion_tokens=2_220, + total_tokens=12_450, + api_calls=7, + context_tokens=12_450, + context_length=200_000, + compressions=0, + ) + + text = cli_obj._build_status_bar_text(width=120) + + assert "cmp" not in text + + def test_compression_count_shown_in_medium_status_bar(self): + cli_obj = _attach_agent( + _make_cli(), + prompt_tokens=10_000, + completion_tokens=2_400, + total_tokens=12_400, + api_calls=7, + context_tokens=12_400, + context_length=200_000, + compressions=2, + ) + + text = cli_obj._build_status_bar_text(width=60) + + assert "cmp 2" in text + + def test_compression_count_hidden_in_narrow_status_bar(self): + cli_obj = _attach_agent( + _make_cli(), + prompt_tokens=10_000, + completion_tokens=2_400, + total_tokens=12_400, + api_calls=7, + context_tokens=12_400, + context_length=200_000, + compressions=5, + ) + + text = cli_obj._build_status_bar_text(width=50) + + assert "cmp" not in text + + def test_compression_count_style_thresholds(self): + cli_obj = _make_cli() + + assert cli_obj._compression_count_style(1) == "class:status-bar-dim" + assert cli_obj._compression_count_style(4) == "class:status-bar-dim" + assert cli_obj._compression_count_style(5) == "class:status-bar-warn" + assert cli_obj._compression_count_style(9) == "class:status-bar-warn" + assert cli_obj._compression_count_style(10) == "class:status-bar-bad" + assert cli_obj._compression_count_style(25) == "class:status-bar-bad" + + def test_compression_count_in_wide_fragments(self): + cli_obj = _attach_agent( + _make_cli(), + prompt_tokens=10_230, + completion_tokens=2_220, + total_tokens=12_450, + api_calls=7, + context_tokens=12_450, + context_length=200_000, + compressions=7, + ) + cli_obj._status_bar_visible = True + + frags = cli_obj._get_status_bar_fragments() + frag_texts = [text for _, text in frags] + + assert "cmp 7" in frag_texts + frag_styles = {text: style for style, text in frags} + assert frag_styles["cmp 7"] == "class:status-bar-warn" + + def test_compression_count_absent_from_fragments_when_zero(self): + cli_obj = _attach_agent( + _make_cli(), + prompt_tokens=10_230, + completion_tokens=2_220, + total_tokens=12_450, + api_calls=7, + context_tokens=12_450, + context_length=200_000, + compressions=0, + ) + cli_obj._status_bar_visible = True + + frags = cli_obj._get_status_bar_fragments() + frag_texts = [text for _, text in frags] + + assert not any("cmp" in t for t in frag_texts) + def test_minimal_tui_chrome_threshold(self): cli_obj = _make_cli()