Skip to content
Merged
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
39 changes: 39 additions & 0 deletions packages/claude-code-plugin/hooks/lib/tiny_actor_preview.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
"""
from __future__ import annotations

import json
import os
from pathlib import Path
from typing import Optional

from tiny_actor_card import create_actor_card, TinyActorCard
Expand All @@ -26,6 +28,41 @@ def is_tiny_actors_enabled() -> bool:
return os.environ.get(_FLAG_ENV, "").lower() in _TRUTHY


# ---------------------------------------------------------------------------
# Agent visual.eye loader
# ---------------------------------------------------------------------------

# Resolve agents directory relative to this file:
# hooks/lib/ -> ../../ -> packages/claude-code-plugin/ -> ../../ -> repo root
# -> packages/rules/.ai-rules/agents/
_AGENTS_DIR: Path = (
Path(__file__).resolve().parent.parent.parent.parent.parent
/ "packages"
/ "rules"
/ ".ai-rules"
/ "agents"
)


def _load_agent_eye(agent_id: str) -> Optional[str]:
"""Load ``visual.eye`` glyph from the agent JSON definition.

Returns ``None`` when the file doesn't exist, is malformed, or has no
``visual.eye`` field — the caller should fall back to the default glyph.
"""
try:
agent_file = _AGENTS_DIR / f"{agent_id}.json"
if not agent_file.is_file():
return None
data = json.loads(agent_file.read_text(encoding="utf-8"))
eye = data.get("visual", {}).get("eye")
if isinstance(eye, str) and eye:
return eye
return None
except Exception:
return None


# ---------------------------------------------------------------------------
# Public API
# ---------------------------------------------------------------------------
Expand Down Expand Up @@ -68,6 +105,7 @@ def render_actor_preview(
agent_id=preset["primary"],
label=_agent_id_to_label(preset["primary"]),
mood="proposing",
eye_glyph=_load_agent_eye(preset["primary"]),
)
cards.append(primary)

Expand All @@ -77,6 +115,7 @@ def render_actor_preview(
agent_id=spec_id,
label=_agent_id_to_label(spec_id),
mood="reviewing",
eye_glyph=_load_agent_eye(spec_id),
)
cards.append(card)

Expand Down
114 changes: 111 additions & 3 deletions packages/claude-code-plugin/tests/test_tiny_actor_preview.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Tests for feature-flagged Tiny Actor Grid preview (#1271)."""
import json
import os
import sys

Expand All @@ -11,7 +12,12 @@
sys.path.insert(0, _lib_dir)

from buddy_renderer import display_width
from tiny_actor_preview import is_tiny_actors_enabled, render_actor_preview
from tiny_actor_preview import (
is_tiny_actors_enabled,
render_actor_preview,
_load_agent_eye,
_AGENTS_DIR,
)

# ---------------------------------------------------------------------------
# is_tiny_actors_enabled
Expand Down Expand Up @@ -80,8 +86,9 @@ def test_unknown_mode_returns_none(self):
def test_preview_contains_agent_faces(self):
result = render_actor_preview("PLAN")
assert result is not None
# Should contain face-like patterns (eye+mouth+eye)
assert "\u25cf" in result or "o" in result # default eye glyphs
# Should contain real agent eye glyphs loaded from JSON
# security-specialist has visual.eye = "◮"
assert "\u25ee" in result or "\u25cf" in result # real or default eye glyphs

def test_preview_contains_moderator(self):
result = render_actor_preview("PLAN")
Expand Down Expand Up @@ -133,3 +140,104 @@ def _boom(mode):
monkeypatch.setattr(tiny_actor_preview, "get_cast_preset", _boom)
result = render_actor_preview("PLAN")
assert result is None


# ---------------------------------------------------------------------------
# _load_agent_eye — agent visual.eye loading
# ---------------------------------------------------------------------------


class TestLoadAgentEye:
"""Loading visual.eye glyphs from agent JSON files (#1301)."""

def test_known_agent_returns_eye_glyph(self):
"""A known agent like security-specialist returns its visual.eye."""
eye = _load_agent_eye("security-specialist")
assert eye is not None
# Verify it matches the actual JSON file
agent_file = _AGENTS_DIR / "security-specialist.json"
data = json.loads(agent_file.read_text(encoding="utf-8"))
assert eye == data["visual"]["eye"]

def test_unknown_agent_returns_none(self):
"""An agent ID with no matching JSON falls back to None."""
eye = _load_agent_eye("nonexistent-agent-xyz")
assert eye is None

def test_malformed_json_returns_none(self, tmp_path, monkeypatch):
"""A malformed agent JSON doesn't break — returns None."""
import tiny_actor_preview

bad_dir = tmp_path / "agents"
bad_dir.mkdir()
(bad_dir / "broken-agent.json").write_text("{invalid json", encoding="utf-8")
monkeypatch.setattr(tiny_actor_preview, "_AGENTS_DIR", bad_dir)
eye = _load_agent_eye("broken-agent")
assert eye is None

def test_missing_visual_block_returns_none(self, tmp_path, monkeypatch):
"""An agent JSON without visual block returns None."""
import tiny_actor_preview

no_visual_dir = tmp_path / "agents"
no_visual_dir.mkdir()
(no_visual_dir / "no-visual.json").write_text(
json.dumps({"name": "Test Agent"}), encoding="utf-8"
)
monkeypatch.setattr(tiny_actor_preview, "_AGENTS_DIR", no_visual_dir)
eye = _load_agent_eye("no-visual")
assert eye is None

def test_empty_eye_string_returns_none(self, tmp_path, monkeypatch):
"""An agent with empty visual.eye string returns None."""
import tiny_actor_preview

empty_dir = tmp_path / "agents"
empty_dir.mkdir()
(empty_dir / "empty-eye.json").write_text(
json.dumps({"visual": {"eye": ""}}), encoding="utf-8"
)
monkeypatch.setattr(tiny_actor_preview, "_AGENTS_DIR", empty_dir)
eye = _load_agent_eye("empty-eye")
assert eye is None


# ---------------------------------------------------------------------------
# render_actor_preview — real eye glyphs (#1301)
# ---------------------------------------------------------------------------


class TestRenderActorPreviewEyeGlyphs:
"""Rendered preview uses real agent eye glyphs from JSON."""

@pytest.fixture(autouse=True)
def _enable_flag(self, monkeypatch):
monkeypatch.setenv("CODINGBUDDY_TINY_ACTORS", "1")

def test_buddy_still_uses_moderator_face(self):
"""Buddy moderator card keeps its hardcoded ◕ eye, not loaded from JSON."""
result = render_actor_preview("PLAN")
assert result is not None
assert "\u25d5\u203f\u25d5" in result # ◕‿◕

def test_specialist_uses_real_eye_glyph(self):
"""Security specialist card uses ◮ from its visual.eye, not default ●."""
result = render_actor_preview("PLAN")
assert result is not None
# security-specialist visual.eye = "◮"
sec_eye = _load_agent_eye("security-specialist")
assert sec_eye is not None
assert sec_eye in result

def test_all_modes_load_real_glyphs(self):
"""All preset modes produce output with non-default eye glyphs."""
from tiny_actor_card import DEFAULT_EYE

for mode in ("PLAN", "EVAL", "AUTO", "SHIP"):
result = render_actor_preview(mode)
assert result is not None, f"Mode {mode} should render"
# At least one real eye glyph should appear (not just default ●)
# Buddy uses ◕ which is not DEFAULT_EYE, so that alone satisfies this
# but primary/specialist agents should also have their own glyphs
lines = result.split("\n")
assert len(lines) > 0
Loading