Skip to content

Commit de622cc

Browse files
committed
refactor(hud): extract 9 modules for parallel statusbar work
Infrastructure Wave: establish lib/ layout + re-export pattern, move 3 low-risk items (BUDDY_FACE, format_rate_limits, get_fresh_version) and reserve 6 skeleton paths for Wave 1-D and Wave 2-A-E with docstring contracts. - lib/hud_buddy.py re-exports canonical BUDDY_FACE from tiny_actor_presets - lib/hud_version.py renames _get_fresh_version -> get_fresh_version (public) - lib/hud_rate_limits.py moves format_rate_limits verbatim - 6 skeleton modules for future waves with API contracts in docstrings - codingbuddy-hud.py adds sys.path bootstrap + re-exports (no noqa) - 9 new test_hud_*.py with tmp_path fixture + re-export identity locks Golden Rule: 133 tests in test_hud.py/test_hud_state.py/test_mode_detect_hud.py remain 100% passing (behavior-preserving). Total 155/155 tests pass. Design decisions from 4-reviewer PLAN panel feedback: 1. BUDDY_FACE re-exported from canonical tiny_actor_presets (avoids 4-way dup) 2. sys.path bootstrap follows sibling hook convention (no noqa: E402) 3. Skeletons retained with explicit docstring contracts 4. costHistory field deferred to Wave 2-B owner 5. get_fresh_version public rename + _get_fresh_version backcompat alias 6. Optional dead import removed from hud_version.py 7. Framed as Infrastructure Wave (layout + re-export, not LOC reduction) Closes #1464
1 parent 57e37a1 commit de622cc

19 files changed

Lines changed: 466 additions & 42 deletions

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

Lines changed: 30 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,36 @@
1515
import sys
1616
from datetime import datetime, timezone
1717

18-
BUDDY_FACE = "\u25d5\u203f\u25d5" # ◕‿◕
18+
# --- lib import bootstrap ---
19+
# statusLine entry script: sys.path insertion here is intentional so
20+
# lib/* imports work when Claude Code invokes `python codingbuddy-hud.py`.
21+
_LIB_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "lib")
22+
if _LIB_DIR not in sys.path:
23+
sys.path.insert(0, _LIB_DIR)
24+
25+
# === test_hud.py compatibility re-exports — DO NOT REMOVE without coordinated test update ===
26+
# Defensive fallback: statusLine is a hot path invoked by Claude Code on
27+
# every render. If any lib module is temporarily broken (e.g. mid-wave
28+
# refactor), fall back to minimal inline implementations so the status
29+
# bar still renders instead of crashing the Claude Code subprocess.
30+
try:
31+
from hud_buddy import BUDDY_FACE # canonical SSoT via tiny_actor_presets
32+
except Exception: # pragma: no cover - defensive
33+
BUDDY_FACE = "\u25d5\u203f\u25d5" # ◕‿◕
34+
35+
try:
36+
from hud_rate_limits import format_rate_limits
37+
except Exception: # pragma: no cover - defensive
38+
def format_rate_limits(stdin_data: dict) -> str: # type: ignore[misc]
39+
return ""
40+
41+
try:
42+
from hud_version import get_fresh_version as _get_fresh_version # backcompat alias
43+
except Exception: # pragma: no cover - defensive
44+
def _get_fresh_version( # type: ignore[misc]
45+
hud_state: dict, *, plugins_file: str = ""
46+
) -> str:
47+
return hud_state.get("version", "")
1948

2049
# Agent eye glyphs from .ai-rules agent definitions.
2150
AGENT_GLYPHS = {
@@ -303,25 +332,6 @@ def resolve_model_label(stdin_data: dict) -> tuple:
303332
return (model_id, display_name)
304333

305334

306-
def format_rate_limits(stdin_data: dict) -> str:
307-
"""Format rate-limit info if present. Returns '' when absent."""
308-
rl = stdin_data.get("rate_limits")
309-
if not rl:
310-
return ""
311-
parts = []
312-
five = rl.get("five_hour")
313-
if five:
314-
pct = five.get("used_percentage", 0)
315-
parts.append(f"5h:{pct:.0f}%")
316-
seven = rl.get("seven_day")
317-
if seven:
318-
pct = seven.get("used_percentage", 0)
319-
parts.append(f"7d:{pct:.0f}%")
320-
if not parts:
321-
return ""
322-
return "RL:" + ",".join(parts)
323-
324-
325335
def format_worktree(stdin_data: dict) -> str:
326336
"""Format worktree name if present. Returns '' when absent."""
327337
wt = stdin_data.get("worktree")
@@ -395,28 +405,6 @@ def format_badge_line(agent: str, focus: str, blocker_count) -> str:
395405
return " ".join(badges)
396406

397407

398-
def _get_fresh_version(hud_state: dict, *, plugins_file: str = "") -> str:
399-
"""Return the most current plugin version.
400-
401-
Prefers installed_plugins.json (authoritative after updates)
402-
over the hud-state snapshot written at session start.
403-
Pass *plugins_file* explicitly for testing.
404-
"""
405-
try:
406-
lib_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "lib")
407-
if lib_dir not in sys.path:
408-
sys.path.insert(0, lib_dir)
409-
from hud_helpers import read_installed_version
410-
411-
kwargs = {"plugins_file": plugins_file} if plugins_file else {}
412-
fresh = read_installed_version(**kwargs)
413-
if fresh:
414-
return fresh
415-
except Exception:
416-
pass
417-
return hud_state.get("version", "")
418-
419-
420408
def format_status_line(
421409
stdin_data: dict,
422410
hud_state: dict,
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
"""Buddy face re-export for CodingBuddy statusLine (#1326).
2+
3+
``BUDDY_FACE`` is canonically defined in
4+
``tiny_actor_presets.BUDDY_FACE`` and already covered by
5+
``tests/test_tiny_actor_presets.py`` for value/type assertions. This
6+
module re-exports it so statusLine helpers that conceptually belong to
7+
the HUD layer can depend on a ``hud_*`` module instead of reaching into
8+
``tiny_actor_presets``.
9+
10+
Wave 0 establishes the re-export only. Wave 2-A will extend this file
11+
with breathing Buddy face state logic (e.g., ``get_buddy_face(phase)``).
12+
"""
13+
from __future__ import annotations
14+
15+
from tiny_actor_presets import BUDDY_FACE # canonical SSoT
16+
17+
__all__ = ["BUDDY_FACE"]
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
"""Cache-savings badge for CodingBuddy statusLine (#1326).
2+
3+
Wave 0 skeleton — reserved for **Wave 2-C**.
4+
5+
Planned contents (Wave 2-C owner fills):
6+
* ``compute_cache_savings(cost_breakdown: dict) -> float`` — USD
7+
avoided by cache hits
8+
* ``format_cache_savings_badge(savings_usd: float) -> str``
9+
10+
Source of truth for the computation is the stdin ``cost`` payload
11+
(cached-input vs non-cached-input token counts combined with
12+
``MODEL_PRICING`` — both already available in ``codingbuddy-hud``).
13+
This module will be the single import target for Wave 3 assembly.
14+
"""
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
"""Smart context bar visualization for CodingBuddy statusLine (#1326).
2+
3+
Wave 0 skeleton — reserved for **Wave 2-E**.
4+
5+
Planned contents (Wave 2-E owner fills):
6+
* ``CONTEXT_BAR_WIDTH: int`` — segment count
7+
* ``CONTEXT_BAR_THRESHOLDS: tuple[float, float, float]`` — warning
8+
/ danger / critical cut-offs
9+
* ``render_context_bar(used_tokens: int, total_tokens: int) -> str``
10+
11+
Wave 2-E will render the bar from the ``context`` payload already
12+
parsed in ``codingbuddy-hud``. This file is a reserved import target
13+
so Wave 3 integration can depend on ``hud_context_bar`` without
14+
creating the module mid-merge.
15+
"""
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
"""Adaptive layout engine for CodingBuddy statusLine (#1326).
2+
3+
Wave 0 skeleton — reserved for **Wave 1-D**.
4+
5+
Planned contents (Wave 1-D owner fills):
6+
* ``SEGMENT_PRIORITY: list[tuple[str, int]]`` — drop order when
7+
width-constrained
8+
* ``_visible_len(s: str) -> int`` — ANSI-aware length
9+
* ``_shorten_model_label(name: str, *, compact: bool = False) -> str``
10+
* ``_fit_segments(segments: list[str], width: int, *, separator: str) -> str``
11+
12+
Wave 1-D will also migrate the segment-assembly logic currently inline
13+
in ``codingbuddy-hud.format_status_line`` to these helpers. Until then,
14+
this file is a reserved import target so Wave workers downstream
15+
(Wave 2-E, Wave 3) can reference ``hud_layout`` without creating it.
16+
"""
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
"""Mode rainbow ANSI colouring for CodingBuddy statusLine (#1326).
2+
3+
Wave 0 skeleton — reserved for **Wave 2-D**.
4+
5+
Planned contents (Wave 2-D owner fills):
6+
* ``MODE_PALETTE: dict[str, tuple[int, int, int]]`` — per-mode RGB
7+
gradient anchors (PLAN/ACT/EVAL/AUTO)
8+
* ``gradient_ansi(text: str, palette: tuple) -> str``
9+
* ``render_mode_rainbow(mode: str, text: str) -> str``
10+
11+
Wave 2-D will wire the rainbow into ``format_status_line`` (or its
12+
``hud_layout`` successor) in place of the plain text mode label.
13+
"""
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
"""Rate-limit formatting for CodingBuddy statusLine (#1326).
2+
3+
Extracted verbatim from codingbuddy-hud.py as part of the Wave 0 refactor.
4+
Behavior-preserving — see tests/test_hud.py for the contract.
5+
"""
6+
from __future__ import annotations
7+
8+
from typing import Any, Dict
9+
10+
11+
def format_rate_limits(stdin_data: Dict[str, Any]) -> str:
12+
"""Format Claude Code rate-limit badge.
13+
14+
Returns an empty string when no rate-limit data is supplied so the
15+
badge can be dropped from the status line silently.
16+
"""
17+
rl = stdin_data.get("rate_limits")
18+
if not rl:
19+
return ""
20+
parts = []
21+
five = rl.get("five_hour")
22+
if five:
23+
pct = five.get("used_percentage", 0)
24+
parts.append(f"5h:{pct:.0f}%")
25+
seven = rl.get("seven_day")
26+
if seven:
27+
pct = seven.get("used_percentage", 0)
28+
parts.append(f"7d:{pct:.0f}%")
29+
if not parts:
30+
return ""
31+
return "RL:" + ",".join(parts)
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
"""Session self-heal and stale state detection (#1326).
2+
3+
Wave 0 skeleton — reserved for **Wave 1-B**.
4+
5+
Planned contents (Wave 1-B owner fills):
6+
* ``detect_stale_session(state: dict, *, now: datetime | None = None) -> bool``
7+
* ``reset_stale_session(state_file: str) -> None``
8+
* ``SESSION_STALE_SECONDS`` constant
9+
10+
The current monolith embeds no session self-heal logic; Wave 1-B will
11+
introduce both the helpers and their call site in
12+
``codingbuddy-hud.format_status_line`` (or its Wave 1-D successor in
13+
``hud_layout``). This module exists as a placeholder so Wave 1-B can
14+
commit to its own sub-branch without racing other Wave workers to
15+
create the file.
16+
"""
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
"""Cost velocity indicator for CodingBuddy statusLine (#1326).
2+
3+
Wave 0 skeleton — reserved for **Wave 2-B**.
4+
5+
Planned contents (Wave 2-B owner fills):
6+
* ``record_cost_sample(state_file: str, cost_usd: float, *, now=None) -> None``
7+
* ``compute_velocity(history: list[dict]) -> float`` — $/hour
8+
* ``format_velocity_badge(velocity_usd_per_hour: float) -> str``
9+
* ``MAX_COST_HISTORY_ENTRIES`` constant
10+
11+
Wave 2-B will ALSO extend ``lib/hud_state.py`` with a
12+
``"costHistory": []`` entry in both ``_EXTENDED_DEFAULTS`` and
13+
``init_hud_state()`` (this is deliberately NOT done in Wave 0 — schema
14+
design belongs with the feature owner).
15+
"""
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
"""Version resolution for CodingBuddy statusLine (#1326).
2+
3+
Wave 0 extracts the plugin-version fallback logic from
4+
``codingbuddy-hud.py`` so Wave 1-A can extend the resolution chain
5+
without touching the monolith.
6+
7+
The public entry point is :func:`get_fresh_version`. ``codingbuddy-hud``
8+
calls it internally from ``format_status_line``; callers pass the
9+
current ``hud_state`` dict and an optional ``plugins_file`` override
10+
used by the test-suite to point at a fixture path.
11+
12+
Behavior-preserving contract (mirrors the original monolith helper):
13+
14+
1. Attempt to read the freshest version from
15+
``installed_plugins.json`` via
16+
:func:`hud_helpers.read_installed_version`.
17+
2. On success, return that value.
18+
3. On any failure (missing file, parse error, unexpected exception),
19+
fall back to ``hud_state.get("version", "")``.
20+
"""
21+
from __future__ import annotations
22+
23+
from typing import Any, Dict
24+
25+
26+
def get_fresh_version(
27+
hud_state: Dict[str, Any],
28+
*,
29+
plugins_file: str = "",
30+
) -> str:
31+
"""Return the freshest known plugin version string.
32+
33+
Args:
34+
hud_state: Current HUD state dict (supplies the fallback
35+
``version`` field).
36+
plugins_file: Optional override for the
37+
``installed_plugins.json`` path, used by tests.
38+
39+
Notes:
40+
``hud_helpers`` is imported lazily inside the function body to
41+
preserve the hot-path resilience of the original monolith. If
42+
``hud_helpers`` is temporarily broken (e.g. mid-wave refactor),
43+
the statusLine still renders via the ``hud_state`` fallback
44+
instead of crashing at module load.
45+
"""
46+
try:
47+
from hud_helpers import read_installed_version # lazy for resilience
48+
kwargs = {"plugins_file": plugins_file} if plugins_file else {}
49+
fresh = read_installed_version(**kwargs)
50+
if fresh:
51+
return fresh
52+
except Exception:
53+
pass
54+
return hud_state.get("version", "")

0 commit comments

Comments
 (0)