Skip to content

Commit 9df22fd

Browse files
committed
feat(hud): complete Wave 3 integration for 1-D/2-A/2-D/2-E (Wave 3b)
Commit bd78195 ("integrate Wave 2-B/2-C in format_status_line") wired velocity and cache-savings into the cost segment but left four sibling Wave modules as dead code: hud_buddy, hud_rainbow, hud_context_bar, and hud_layout. Their unit tests passed, but format_status_line never called them — the statusLine rendered with a static buddy face, no adaptive layout, text-only context percentage, and no mode coloring. This commit closes that gap by hoisting all four modules as top-level imports (matching the velocity/cache_savings pattern) and refactoring format_status_line to build (name, priority, text) segments consumed by fit_segments. Wave 2-A — breathing buddy face BUDDY_FACE constant → select_face_from_state(hud_state). The face now reflects phase/blockerCount: ready/None → ◕‿◕ (idle) planning/evaluating → ◔‿◔ (thinking) executing/cycling → ◕◡◕ (active) blockerCount > 0 → ◕︵◕ (error, wins over phase) lastEvent=victory → ◕ᴗ◕ (victory, wins over phase) Wave 2-D — mode rainbow ANSI coloring (opt-in) Gated on CODINGBUDDY_HUD_RAINBOW=1 because Claude Code statusLine's ANSI support is environment-dependent; default OFF keeps existing terminals clean. Transitively honours NO_COLOR (https://no-color.org) via hud_rainbow.is_color_enabled. Only real modes (PLAN/ACT/EVAL/AUTO) are colored — the "Ready" fallback label stays plain so tests and logs don't see it uppercased. Coloring is applied POST-fit_segments (string replace on the already-assembled line1) so the ANSI escapes don't break hud_layout.visible_len width accounting during layout. Wave 2-E — smart context bar "Ctx:{pct}%" segment → format_context_bar_segment(stdin_data), rendering e.g. "[████░░░░░░] 42%" or "[████████▓░] 92%⚠" above the warning threshold. Legacy "Ctx:" prefix is gone; the three existing tests asserting "Ctx:45%"/"Ctx:10%" have been updated to check for the new bar + percentage combination. Wave 1-D — adaptive layout Final " | ".join(segments) → fit_segments(layout_segments, terminal_width()). Low-priority segments (worktree, rate_limits, model, cache, ctx) are dropped first on narrow terminals; face_version and mode_health are sacred and always render. shorten_model_label trims the "(1M context)" suffix so Opus display names fit mid-width terminals without being dropped. test_full_telemetry gained monkeypatch.setenv("COLUMNS", "300") because its "all segments render" assertion would otherwise flake against the pytest 80-col default now that adaptive layout actually runs. Test coverage: - 14 new tests split across four classes: TestWave2ABreathingFaceIntegration (4) TestWave2EContextBarIntegration (3) TestWave2DRainbowIntegration (4) TestWave1DAdaptiveLayoutIntegration (3) - 3 existing tests updated to match the Wave 2-E "[bar] 45%" format - 1 existing test (test_full_telemetry) hardened with COLUMNS=300 Local runs: 1077 passed (full plugin test suite). Manual CLI reproductions confirmed: - Default: ◕‿◕ CB v5.6.0 | Ready 🟢 | ... | [████░░░░░░] 42% | ... - 92% context: ... | [████████▓░] 92%⚠ | ... - RAINBOW=1 PLAN: ... | \x1b[38;2;64;128;255m◇ PLAN\x1b[0m 🟢 | ... - COLUMNS=60: ◕‿◕ CB v5.6.0 | Ready 🔴 | $1.50... | 12m (sacred + cost + duration only)
1 parent 3877014 commit 9df22fd

2 files changed

Lines changed: 352 additions & 20 deletions

File tree

packages/claude-code-plugin/hooks/codingbuddy-hud.py

Lines changed: 119 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,59 @@ def _format_velocity_segment(stdin_data, hud_state=None): # type: ignore[misc]
5959
def _format_cache_savings(stdin_data): # type: ignore[misc]
6060
return ""
6161

62+
# Wave 3b integration: hoist Wave 1-D/2-A/2-D/2-E entry points so
63+
# format_status_line does not pay a sys.modules lookup on every render.
64+
# Each import is guarded separately with a defensive fallback so a
65+
# single broken lib module can never take down the whole status line —
66+
# the outer main() catch-all still emits the minimal safe output.
67+
68+
# Wave 2-A — breathing buddy face
69+
try:
70+
from hud_buddy import select_face_from_state as _select_face_from_state
71+
except ImportError: # pragma: no cover - defensive
72+
def _select_face_from_state(hud_state): # type: ignore[misc]
73+
return BUDDY_FACE
74+
75+
# Wave 2-D — mode rainbow ANSI coloring (opt-in via CODINGBUDDY_HUD_RAINBOW)
76+
try:
77+
from hud_rainbow import (
78+
is_color_enabled as _rainbow_color_enabled,
79+
render_mode_rainbow as _render_mode_rainbow,
80+
)
81+
except ImportError: # pragma: no cover - defensive
82+
def _rainbow_color_enabled(env=None): # type: ignore[misc]
83+
return False
84+
85+
def _render_mode_rainbow(mode, *, enabled=None, env=None): # type: ignore[misc]
86+
return mode
87+
88+
# Wave 2-E — smart context bar visualization
89+
try:
90+
from hud_context_bar import format_context_bar_segment as _format_context_bar
91+
except ImportError: # pragma: no cover - defensive
92+
def _format_context_bar(stdin_data): # type: ignore[misc]
93+
return ""
94+
95+
# Wave 1-D — adaptive layout engine
96+
try:
97+
from hud_layout import (
98+
fit_segments as _fit_segments,
99+
terminal_width as _terminal_width,
100+
shorten_model_label as _shorten_model_label,
101+
DEFAULT_SEPARATOR as _LAYOUT_SEP,
102+
)
103+
except ImportError: # pragma: no cover - defensive
104+
_LAYOUT_SEP = " | "
105+
106+
def _terminal_width(*, fallback=120): # type: ignore[misc]
107+
return fallback
108+
109+
def _shorten_model_label(name, *, compact=False): # type: ignore[misc]
110+
return name
111+
112+
def _fit_segments(segments, width, *, separator=_LAYOUT_SEP): # type: ignore[misc]
113+
return separator.join(t for _, _, t in segments if t)
114+
62115
# Agent eye glyphs from .ai-rules agent definitions.
63116
AGENT_GLYPHS = {
64117
"act-mode": "\u25c6", # ◆
@@ -474,28 +527,78 @@ def format_status_line(
474527

475528
ver_str = f" v{version}" if version else ""
476529

477-
segments = [
478-
f"{BUDDY_FACE} CB{ver_str}",
479-
f"{mode_label} {health}",
480-
duration,
481-
f"{cost_prefix}{cost:.2f}{velocity_suffix}{savings_suffix}",
530+
# Wave 2-A: dynamic buddy face from hud_state phase/blockerCount.
531+
# Falls back to canonical BUDDY_FACE when hud_state is empty.
532+
buddy_face = _select_face_from_state(hud_state) or BUDDY_FACE
533+
534+
# Wave 2-E: render context as a visual bar instead of "Ctx:NN%".
535+
# format_context_bar_segment returns "" when context_window is
536+
# absent — keep the legacy text segment as a fallback in that
537+
# case so a stripped-down stdin still shows a percentage.
538+
ctx_segment = _format_context_bar(stdin_data) or f"{ctx_pct:.0f}%"
539+
540+
# Wave 2-E helper returns e.g. "[██░░░░░░░░] 20%" — the block
541+
# glyphs widen the segment, so assign a dedicated priority slot
542+
# (see SEGMENT_PRIORITY in hud_layout) so adaptive layout can
543+
# drop it first when terminal is tight.
544+
545+
# Plain-text mode label for layout width calculation. Rainbow
546+
# coloring is applied to the FINAL string after fit_segments
547+
# has assembled it (see below) — injecting ANSI into segments
548+
# would break visible_len accounting.
549+
mode_health_plain = f"{mode_label} {health}"
550+
551+
# Compact model label: trim the "(1M context)" suffix so the
552+
# segment can fit into mid-width terminals without being dropped
553+
# by fit_segments.
554+
model_segment = _shorten_model_label(display_name) if display_name else ""
555+
556+
# Build the (name, priority, text) segments consumed by
557+
# hud_layout.fit_segments. Priorities mirror SEGMENT_PRIORITY —
558+
# face_version (0) and mode_health (1) are sacred.
559+
layout_segments = [
560+
("face_version", 0, f"{buddy_face} CB{ver_str}"),
561+
("mode_health", 1, mode_health_plain),
562+
("cost", 2, f"{cost_prefix}{cost:.2f}{velocity_suffix}{savings_suffix}"),
563+
("duration", 3, duration),
564+
("ctx", 4, ctx_segment),
482565
]
483566
if cache_segment:
484-
segments.append(cache_segment)
485-
segments.append(f"Ctx:{ctx_pct:.0f}%")
486-
567+
layout_segments.append(("cache", 5, cache_segment))
568+
if model_segment:
569+
layout_segments.append(("model", 6, model_segment))
487570
rl = format_rate_limits(stdin_data)
488571
if rl:
489-
segments.append(rl)
490-
572+
layout_segments.append(("rate_limits", 7, rl))
491573
wt = format_worktree(stdin_data)
492574
if wt:
493-
segments.append(wt)
494-
495-
if display_name:
496-
segments.append(display_name)
497-
498-
line1 = " | ".join(segments)
575+
layout_segments.append(("worktree", 8, wt))
576+
577+
# Wave 1-D: priority-driven adaptive layout. fit_segments drops
578+
# the highest-priority-number segments first until the line fits
579+
# within the terminal width. Sacred segments (priority ≤ 1) are
580+
# never dropped; the face_version + mode_health pair therefore
581+
# always renders even in an 80-col terminal.
582+
line1 = _fit_segments(layout_segments, _terminal_width())
583+
584+
# Wave 2-D: opt-in ANSI rainbow coloring for the mode label.
585+
# Gated on ``CODINGBUDDY_HUD_RAINBOW=1`` because Claude Code's
586+
# statusLine renderer's support for ANSI is environment-dependent
587+
# — default OFF keeps existing terminals clean. NO_COLOR (per the
588+
# https://no-color.org standard) is honoured transitively via
589+
# hud_rainbow.is_color_enabled even when the opt-in flag is set.
590+
# Only real modes (PLAN/ACT/EVAL/AUTO) are colored — the "Ready"
591+
# fallback label stays plain so existing tests and logs don't
592+
# see it uppercased.
593+
if (
594+
mode
595+
and os.environ.get("CODINGBUDDY_HUD_RAINBOW", "") == "1"
596+
and _rainbow_color_enabled()
597+
and mode_health_plain in line1
598+
):
599+
colored_mode = _render_mode_rainbow(mode, enabled=True)
600+
rainbow_mode_health = f"{colored_mode} {health}"
601+
line1 = line1.replace(mode_health_plain, rainbow_mode_health, 1)
499602

500603
focus = hud_state.get("focus") or ""
501604
blocker_count = hud_state.get("blockerCount", 0) or 0

0 commit comments

Comments
 (0)