Skip to content

Commit 0f6c477

Browse files
committed
feat(plugin): derive Tiny Actor faces from agent visual.eye metadata (#1301)
Load real visual.eye glyphs from agent JSON definitions into Tiny Actor cards instead of using the generic default glyph. Buddy moderator keeps its hardcoded identity. Falls back gracefully for missing/malformed agents.
1 parent fe65cdc commit 0f6c477

2 files changed

Lines changed: 150 additions & 3 deletions

File tree

packages/claude-code-plugin/hooks/lib/tiny_actor_preview.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
"""
66
from __future__ import annotations
77

8+
import json
89
import os
10+
from pathlib import Path
911
from typing import Optional
1012

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

2830

31+
# ---------------------------------------------------------------------------
32+
# Agent visual.eye loader
33+
# ---------------------------------------------------------------------------
34+
35+
# Resolve agents directory relative to this file:
36+
# hooks/lib/ -> ../../ -> packages/claude-code-plugin/ -> ../../ -> repo root
37+
# -> packages/rules/.ai-rules/agents/
38+
_AGENTS_DIR: Path = (
39+
Path(__file__).resolve().parent.parent.parent.parent.parent
40+
/ "packages"
41+
/ "rules"
42+
/ ".ai-rules"
43+
/ "agents"
44+
)
45+
46+
47+
def _load_agent_eye(agent_id: str) -> Optional[str]:
48+
"""Load ``visual.eye`` glyph from the agent JSON definition.
49+
50+
Returns ``None`` when the file doesn't exist, is malformed, or has no
51+
``visual.eye`` field — the caller should fall back to the default glyph.
52+
"""
53+
try:
54+
agent_file = _AGENTS_DIR / f"{agent_id}.json"
55+
if not agent_file.is_file():
56+
return None
57+
data = json.loads(agent_file.read_text(encoding="utf-8"))
58+
eye = data.get("visual", {}).get("eye")
59+
if isinstance(eye, str) and eye:
60+
return eye
61+
return None
62+
except Exception:
63+
return None
64+
65+
2966
# ---------------------------------------------------------------------------
3067
# Public API
3168
# ---------------------------------------------------------------------------
@@ -68,6 +105,7 @@ def render_actor_preview(
68105
agent_id=preset["primary"],
69106
label=_agent_id_to_label(preset["primary"]),
70107
mood="proposing",
108+
eye_glyph=_load_agent_eye(preset["primary"]),
71109
)
72110
cards.append(primary)
73111

@@ -77,6 +115,7 @@ def render_actor_preview(
77115
agent_id=spec_id,
78116
label=_agent_id_to_label(spec_id),
79117
mood="reviewing",
118+
eye_glyph=_load_agent_eye(spec_id),
80119
)
81120
cards.append(card)
82121

packages/claude-code-plugin/tests/test_tiny_actor_preview.py

Lines changed: 111 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
"""Tests for feature-flagged Tiny Actor Grid preview (#1271)."""
2+
import json
23
import os
34
import sys
45

@@ -11,7 +12,12 @@
1112
sys.path.insert(0, _lib_dir)
1213

1314
from buddy_renderer import display_width
14-
from tiny_actor_preview import is_tiny_actors_enabled, render_actor_preview
15+
from tiny_actor_preview import (
16+
is_tiny_actors_enabled,
17+
render_actor_preview,
18+
_load_agent_eye,
19+
_AGENTS_DIR,
20+
)
1521

1622
# ---------------------------------------------------------------------------
1723
# is_tiny_actors_enabled
@@ -80,8 +86,9 @@ def test_unknown_mode_returns_none(self):
8086
def test_preview_contains_agent_faces(self):
8187
result = render_actor_preview("PLAN")
8288
assert result is not None
83-
# Should contain face-like patterns (eye+mouth+eye)
84-
assert "\u25cf" in result or "o" in result # default eye glyphs
89+
# Should contain real agent eye glyphs loaded from JSON
90+
# security-specialist has visual.eye = "◮"
91+
assert "\u25ee" in result or "\u25cf" in result # real or default eye glyphs
8592

8693
def test_preview_contains_moderator(self):
8794
result = render_actor_preview("PLAN")
@@ -133,3 +140,104 @@ def _boom(mode):
133140
monkeypatch.setattr(tiny_actor_preview, "get_cast_preset", _boom)
134141
result = render_actor_preview("PLAN")
135142
assert result is None
143+
144+
145+
# ---------------------------------------------------------------------------
146+
# _load_agent_eye — agent visual.eye loading
147+
# ---------------------------------------------------------------------------
148+
149+
150+
class TestLoadAgentEye:
151+
"""Loading visual.eye glyphs from agent JSON files (#1301)."""
152+
153+
def test_known_agent_returns_eye_glyph(self):
154+
"""A known agent like security-specialist returns its visual.eye."""
155+
eye = _load_agent_eye("security-specialist")
156+
assert eye is not None
157+
# Verify it matches the actual JSON file
158+
agent_file = _AGENTS_DIR / "security-specialist.json"
159+
data = json.loads(agent_file.read_text(encoding="utf-8"))
160+
assert eye == data["visual"]["eye"]
161+
162+
def test_unknown_agent_returns_none(self):
163+
"""An agent ID with no matching JSON falls back to None."""
164+
eye = _load_agent_eye("nonexistent-agent-xyz")
165+
assert eye is None
166+
167+
def test_malformed_json_returns_none(self, tmp_path, monkeypatch):
168+
"""A malformed agent JSON doesn't break — returns None."""
169+
import tiny_actor_preview
170+
171+
bad_dir = tmp_path / "agents"
172+
bad_dir.mkdir()
173+
(bad_dir / "broken-agent.json").write_text("{invalid json", encoding="utf-8")
174+
monkeypatch.setattr(tiny_actor_preview, "_AGENTS_DIR", bad_dir)
175+
eye = _load_agent_eye("broken-agent")
176+
assert eye is None
177+
178+
def test_missing_visual_block_returns_none(self, tmp_path, monkeypatch):
179+
"""An agent JSON without visual block returns None."""
180+
import tiny_actor_preview
181+
182+
no_visual_dir = tmp_path / "agents"
183+
no_visual_dir.mkdir()
184+
(no_visual_dir / "no-visual.json").write_text(
185+
json.dumps({"name": "Test Agent"}), encoding="utf-8"
186+
)
187+
monkeypatch.setattr(tiny_actor_preview, "_AGENTS_DIR", no_visual_dir)
188+
eye = _load_agent_eye("no-visual")
189+
assert eye is None
190+
191+
def test_empty_eye_string_returns_none(self, tmp_path, monkeypatch):
192+
"""An agent with empty visual.eye string returns None."""
193+
import tiny_actor_preview
194+
195+
empty_dir = tmp_path / "agents"
196+
empty_dir.mkdir()
197+
(empty_dir / "empty-eye.json").write_text(
198+
json.dumps({"visual": {"eye": ""}}), encoding="utf-8"
199+
)
200+
monkeypatch.setattr(tiny_actor_preview, "_AGENTS_DIR", empty_dir)
201+
eye = _load_agent_eye("empty-eye")
202+
assert eye is None
203+
204+
205+
# ---------------------------------------------------------------------------
206+
# render_actor_preview — real eye glyphs (#1301)
207+
# ---------------------------------------------------------------------------
208+
209+
210+
class TestRenderActorPreviewEyeGlyphs:
211+
"""Rendered preview uses real agent eye glyphs from JSON."""
212+
213+
@pytest.fixture(autouse=True)
214+
def _enable_flag(self, monkeypatch):
215+
monkeypatch.setenv("CODINGBUDDY_TINY_ACTORS", "1")
216+
217+
def test_buddy_still_uses_moderator_face(self):
218+
"""Buddy moderator card keeps its hardcoded ◕ eye, not loaded from JSON."""
219+
result = render_actor_preview("PLAN")
220+
assert result is not None
221+
assert "\u25d5\u203f\u25d5" in result # ◕‿◕
222+
223+
def test_specialist_uses_real_eye_glyph(self):
224+
"""Security specialist card uses ◮ from its visual.eye, not default ●."""
225+
result = render_actor_preview("PLAN")
226+
assert result is not None
227+
# security-specialist visual.eye = "◮"
228+
sec_eye = _load_agent_eye("security-specialist")
229+
assert sec_eye is not None
230+
assert sec_eye in result
231+
232+
def test_all_modes_load_real_glyphs(self):
233+
"""All preset modes produce output with non-default eye glyphs."""
234+
from tiny_actor_card import DEFAULT_EYE
235+
236+
for mode in ("PLAN", "EVAL", "AUTO", "SHIP"):
237+
result = render_actor_preview(mode)
238+
assert result is not None, f"Mode {mode} should render"
239+
# At least one real eye glyph should appear (not just default ●)
240+
# Buddy uses ◕ which is not DEFAULT_EYE, so that alone satisfies this
241+
# but primary/specialist agents should also have their own glyphs
242+
lines = result.split("\n")
243+
assert len(lines) > 0

0 commit comments

Comments
 (0)