Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/landing-page/__tests__/setup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ describe('Next.js 16 Project Setup', () => {
});

test('Next.js 16.x is installed and locked', () => {
expect(pkg.dependencies.next).toBe('16.1.6');
expect(pkg.dependencies.next).toBe('16.2.3');
});

test('React 19.x is installed and locked', () => {
Expand Down
4 changes: 2 additions & 2 deletions apps/landing-page/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.563.0",
"next": "16.1.6",
"next": "16.2.3",
"next-intl": "^4.8.2",
"next-themes": "^0.4.6",
"prism-react-renderer": "^2.4.1",
Expand All @@ -56,7 +56,7 @@
"axe-core": "^4.11.1",
"babel-plugin-react-compiler": "1.0.0",
"eslint": "^9",
"eslint-config-next": "16.1.6",
"eslint-config-next": "16.2.3",
"happy-dom": "^20.8.8",
"jest-axe": "^10.0.0",
"madge": "^8.0.0",
Expand Down
42 changes: 27 additions & 15 deletions packages/claude-code-plugin/hooks/codingbuddy-hud.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,28 +23,40 @@
sys.path.insert(0, _LIB_DIR)

# === test_hud.py compatibility re-exports — DO NOT REMOVE without coordinated test update ===
# Defensive fallback: statusLine is a hot path invoked by Claude Code on
# every render. If any lib module is temporarily broken (e.g. mid-wave
# refactor), fall back to minimal inline implementations so the status
# bar still renders instead of crashing the Claude Code subprocess.
# Narrow the fallback to ImportError only: real logic bugs in lib modules
# (SyntaxError, NameError, AttributeError) must surface immediately instead
# of being silently swallowed by a catch-all. If a lib module fails to import
# entirely, the outer main() try/except at the bottom of this file still
# emits the minimal safe output via the BUDDY_FACE constant.
try:
from hud_buddy import BUDDY_FACE # canonical SSoT via tiny_actor_presets
except Exception: # pragma: no cover - defensive
BUDDY_FACE = "\u25d5\u203f\u25d5" # ◕‿◕
except ImportError: # pragma: no cover - defensive
BUDDY_FACE = "◕‿◕" # minimal constant for safe-output path

try:
from hud_rate_limits import format_rate_limits
except Exception: # pragma: no cover - defensive
def format_rate_limits(stdin_data: dict) -> str: # type: ignore[misc]
return ""
from hud_rate_limits import format_rate_limits # noqa: F401 re-exported for test_hud.py
except ImportError: # pragma: no cover - defensive
pass # main() catch-all handles absence

try:
from hud_version import get_fresh_version as _get_fresh_version # backcompat alias
except Exception: # pragma: no cover - defensive
def _get_fresh_version( # type: ignore[misc]
hud_state: dict, *, plugins_file: str = ""
) -> str:
return hud_state.get("version", "")
except ImportError: # pragma: no cover - defensive
pass # main() catch-all handles absence

# Wave 2-B velocity + Wave 2-C cache savings hot-path suffixes for the cost segment.
# Hoisted to module top per perf-1485 H1 so format_status_line avoids a
# sys.modules lookup on every render (~0.47μs saved per call).
try:
from hud_velocity import format_velocity_segment as _format_velocity_segment
except ImportError: # pragma: no cover - defensive
def _format_velocity_segment(stdin_data, hud_state=None): # type: ignore[misc]
return ""

try:
from hud_cache_savings import format_cache_savings as _format_cache_savings
except ImportError: # pragma: no cover - defensive
def _format_cache_savings(stdin_data): # type: ignore[misc]
return ""

# Agent eye glyphs from .ai-rules agent definitions.
AGENT_GLYPHS = {
Expand Down
153 changes: 142 additions & 11 deletions packages/claude-code-plugin/hooks/lib/hud_buddy.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,148 @@
"""Buddy face re-export for CodingBuddy statusLine (#1326).
"""Buddy face state engine for CodingBuddy statusLine (#1326, Wave 2-A).

``BUDDY_FACE`` is canonically defined in
``tiny_actor_presets.BUDDY_FACE`` and already covered by
``tests/test_tiny_actor_presets.py`` for value/type assertions. This
module re-exports it so statusLine helpers that conceptually belong to
the HUD layer can depend on a ``hud_*`` module instead of reaching into
``tiny_actor_presets``.
``BUDDY_FACE`` is still canonically defined in
``tiny_actor_presets.BUDDY_FACE`` and re-exported here for import
clarity at the HUD layer. Wave 2-A adds a **state-aware** face
picker so the Buddy appears to "breathe" as the session moves
through its lifecycle phases:

Wave 0 establishes the re-export only. Wave 2-A will extend this file
with breathing Buddy face state logic (e.g., ``get_buddy_face(phase)``).
- **Idle / Ready** → ``◕‿◕`` — resting, calm
- **Thinking / Planning** → ``◔‿◔`` — half-closed eyes, pondering
- **Active / Executing** → ``◕◡◕`` — smiling, working
- **Error / Blocked** → ``◕︵◕`` — concerned, something broke
- **Victory / Completed**→ ``◕ᴗ◕`` — beaming, success

Priority rules:

1. ``blocker_count > 0`` always wins (error face) regardless of phase.
2. ``recent_event == "victory"`` wins over phase when set.
3. Otherwise the phase mapping decides.
4. Unknown/empty phase falls back to the canonical idle face.

Primary entry points:

- :data:`BUDDY_FACE` — canonical idle glyph (re-exported)
- :data:`FACE_*` — individual state glyphs as constants
- :func:`get_buddy_face` — state-to-glyph lookup
- :func:`select_face_from_state` — HUD-state-dict convenience wrapper
"""
from __future__ import annotations

from tiny_actor_presets import BUDDY_FACE # canonical SSoT
from typing import Any, Dict, Optional

from tiny_actor_presets import BUDDY_FACE # canonical SSoT (idle face)

# ------------------------------------------------------------------------
# Face glyph constants
# ------------------------------------------------------------------------

#: Default / idle face — ``◕‿◕`` (U+25D5 U+203F U+25D5).
FACE_IDLE: str = BUDDY_FACE

#: Thinking face — ``◔‿◔`` (half-closed eyes).
FACE_THINKING: str = "\u25d4\u203f\u25d4"

#: Active face — ``◕◡◕`` (smiling, working).
FACE_ACTIVE: str = "\u25d5\u25e1\u25d5"

#: Error / blocked face — ``◕︵◕`` (concerned).
FACE_ERROR: str = "\u25d5\ufe35\u25d5"

#: Victory / completed face — ``◕ᴗ◕`` (beaming).
FACE_VICTORY: str = "\u25d5\u1d17\u25d5"


# ------------------------------------------------------------------------
# Phase → face mapping
# ------------------------------------------------------------------------

_PHASE_FACE_MAP: Dict[str, str] = {
"ready": FACE_IDLE,
"planning": FACE_THINKING,
"executing": FACE_ACTIVE,
"evaluating": FACE_THINKING,
"cycling": FACE_ACTIVE,
"completed": FACE_VICTORY,
}


# ------------------------------------------------------------------------
# Public API
# ------------------------------------------------------------------------


def get_buddy_face(
phase: Optional[str] = None,
*,
blocker_count: int = 0,
recent_event: Optional[str] = None,
) -> str:
"""Return the buddy face glyph for the given state.

Priority resolution (highest wins):

1. ``blocker_count > 0`` → :data:`FACE_ERROR`
2. ``recent_event == "victory"`` → :data:`FACE_VICTORY`
3. Phase lookup against :data:`_PHASE_FACE_MAP`
4. Fallback: :data:`FACE_IDLE`

Args:
phase: HUD state phase. One of
``ready``, ``planning``, ``executing``, ``evaluating``,
``cycling``, ``completed`` (case-insensitive). Unknown
or empty values fall back to the idle face.
blocker_count: Number of blockers detected. Any positive
value triggers the error face.
recent_event: Optional one-off event marker. Currently only
``"victory"`` is recognised.

Returns:
A 3-character glyph string, never empty.
"""
# (1) Error: blockers take precedence
try:
if int(blocker_count) > 0:
return FACE_ERROR
except (TypeError, ValueError):
pass # ignore malformed counter, fall through

# (2) Victory event beats phase
if recent_event and recent_event.lower() == "victory":
return FACE_VICTORY

# (3) Phase mapping
if phase:
return _PHASE_FACE_MAP.get(phase.lower(), FACE_IDLE)

# (4) Fallback
return FACE_IDLE


def select_face_from_state(hud_state: Dict[str, Any]) -> str:
"""Convenience wrapper that extracts face inputs from a hud_state dict.

Reads:

- ``phase`` — HUD phase string
- ``blockerCount`` — integer blocker count (default 0)
- ``lastEvent`` — optional one-off event marker
"""
if not hud_state:
return FACE_IDLE
return get_buddy_face(
phase=hud_state.get("phase"),
blocker_count=hud_state.get("blockerCount", 0) or 0,
recent_event=hud_state.get("lastEvent"),
)


__all__ = ["BUDDY_FACE"]
__all__ = [
"BUDDY_FACE",
"FACE_IDLE",
"FACE_THINKING",
"FACE_ACTIVE",
"FACE_ERROR",
"FACE_VICTORY",
"get_buddy_face",
"select_face_from_state",
]
Loading
Loading