From 0f6c477577f43c8ec4e863fc8fae72e6a2c24498 Mon Sep 17 00:00:00 2001 From: JeremyDev87 Date: Sat, 4 Apr 2026 19:04:17 +0900 Subject: [PATCH] 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. --- .../hooks/lib/tiny_actor_preview.py | 39 ++++++ .../tests/test_tiny_actor_preview.py | 114 +++++++++++++++++- 2 files changed, 150 insertions(+), 3 deletions(-) diff --git a/packages/claude-code-plugin/hooks/lib/tiny_actor_preview.py b/packages/claude-code-plugin/hooks/lib/tiny_actor_preview.py index 8727fa76..cff9053d 100644 --- a/packages/claude-code-plugin/hooks/lib/tiny_actor_preview.py +++ b/packages/claude-code-plugin/hooks/lib/tiny_actor_preview.py @@ -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 @@ -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 # --------------------------------------------------------------------------- @@ -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) @@ -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) diff --git a/packages/claude-code-plugin/tests/test_tiny_actor_preview.py b/packages/claude-code-plugin/tests/test_tiny_actor_preview.py index 0471be10..aa8ec29b 100644 --- a/packages/claude-code-plugin/tests/test_tiny_actor_preview.py +++ b/packages/claude-code-plugin/tests/test_tiny_actor_preview.py @@ -1,4 +1,5 @@ """Tests for feature-flagged Tiny Actor Grid preview (#1271).""" +import json import os import sys @@ -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 @@ -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") @@ -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