Skip to content

Commit 37f618c

Browse files
committed
feat(plugin): show compact live council badges in PreToolUse statusMessage (#1367)
Add council_badge.py formatter that reads HUD state and builds compact badge strings like [◮ secu] [🧪 auth] [⚠1] during active council sessions. Integrated into pre-tool-use.py so badges appear in the spinner statusMessage alongside existing agent status and TDD indicators.
1 parent 88bd097 commit 37f618c

4 files changed

Lines changed: 472 additions & 0 deletions

File tree

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
"""Compact council badge formatter for PreToolUse statusMessage (#1367).
2+
3+
Reads HUD state and builds a short, badge-style string that conveys
4+
which agent is acting, current focus, and blocker status.
5+
6+
Examples:
7+
[◮ secu] [🧪 auth] [⚠1]
8+
[⊙ test] [🔍 retry] [✓]
9+
"""
10+
11+
import os
12+
from typing import Optional
13+
14+
from hud_state import read_hud_state
15+
16+
# Stage-specific icons for the focus badge.
17+
_STAGE_ICON = {
18+
"opening": "\U0001f50d", # 🔍
19+
"reviewing": "\U0001f9ea", # 🧪
20+
"consensus": "\U0001f91d", # 🤝
21+
"done": "\u2705", # ✅
22+
}
23+
24+
_DEFAULT_ICON = "\U0001f50d" # 🔍
25+
26+
# Common suffixes stripped before shortening.
27+
_STRIP_SUFFIXES = ("-specialist", "-developer", "-engineer", "-agent")
28+
29+
# Fallback eye when agent visual is unavailable.
30+
_FALLBACK_EYE = "\u25c6" # ◆
31+
32+
# Maximum focus label length in badge.
33+
_MAX_FOCUS_LEN = 12
34+
35+
36+
def shorten_agent_name(name: str) -> str:
37+
"""Shorten an agent name to a compact label (max 4 chars).
38+
39+
Strips common suffixes and takes the first 4 characters of the
40+
first hyphen-separated segment.
41+
42+
Examples:
43+
"security-specialist" -> "secu"
44+
"frontend-developer" -> "fron"
45+
"auto-mode" -> "auto"
46+
"""
47+
if not name:
48+
return ""
49+
shortened = name
50+
for suffix in _STRIP_SUFFIXES:
51+
shortened = shortened.replace(suffix, "")
52+
shortened = shortened.strip("-")
53+
first_segment = shortened.split("-")[0]
54+
return first_segment[:4]
55+
56+
57+
def format_council_badge(
58+
*,
59+
agent_eye: str,
60+
agent_short: str,
61+
focus: Optional[str] = None,
62+
stage: str = "",
63+
blocker_count: int = 0,
64+
) -> str:
65+
"""Pure formatter: build a compact badge string.
66+
67+
Args:
68+
agent_eye: Single eye character from agent visual (e.g. ◮).
69+
agent_short: Shortened agent name (e.g. "secu").
70+
focus: Current focus label (truncated to 12 chars).
71+
stage: Council stage (opening/reviewing/consensus/done).
72+
blocker_count: Number of outstanding blockers.
73+
74+
Returns:
75+
A single-line string like ``[◮ secu] [🧪 auth] [⚠1]``.
76+
"""
77+
parts = [f"[{agent_eye} {agent_short}]"]
78+
79+
if focus:
80+
icon = _STAGE_ICON.get(stage, _DEFAULT_ICON)
81+
truncated = focus[:_MAX_FOCUS_LEN]
82+
parts.append(f"[{icon} {truncated}]")
83+
84+
if blocker_count > 0:
85+
parts.append(f"[\u26a0{blocker_count}]")
86+
else:
87+
parts.append("[\u2713]")
88+
89+
return " ".join(parts)
90+
91+
92+
def build_council_badge(
93+
*,
94+
state_file: Optional[str] = None,
95+
project_root: Optional[str] = None,
96+
) -> Optional[str]:
97+
"""Read HUD state and build a council badge if the council is active.
98+
99+
Returns None when the council is inactive, no active agent is set,
100+
or the state file is unreadable.
101+
102+
Respects ``CODINGBUDDY_HUD_STATE`` env var as an override for the
103+
default state file path.
104+
"""
105+
kwargs = {}
106+
if state_file:
107+
kwargs["state_file"] = state_file
108+
elif os.environ.get("CODINGBUDDY_HUD_STATE"):
109+
kwargs["state_file"] = os.environ["CODINGBUDDY_HUD_STATE"]
110+
111+
state = read_hud_state(fill_defaults=True, **kwargs)
112+
if not state.get("councilActive"):
113+
return None
114+
115+
active_agent = state.get("activeAgent") or ""
116+
if not active_agent:
117+
return None
118+
119+
eye = _get_agent_eye(active_agent, project_root)
120+
short = shorten_agent_name(active_agent)
121+
122+
return format_council_badge(
123+
agent_eye=eye,
124+
agent_short=short,
125+
focus=state.get("focus"),
126+
stage=state.get("councilStage", ""),
127+
blocker_count=state.get("blockerCount", 0),
128+
)
129+
130+
131+
def _get_agent_eye(agent_name: str, project_root: Optional[str] = None) -> str:
132+
"""Look up the single eye character for an agent.
133+
134+
Falls back to ◆ if the agent JSON is missing or unreadable.
135+
"""
136+
try:
137+
from agent_status import _load_agent_visual
138+
139+
if project_root is None:
140+
project_root = os.environ.get(
141+
"CLAUDE_PROJECT_DIR",
142+
os.environ.get("CLAUDE_CWD", os.getcwd()),
143+
)
144+
visual = _load_agent_visual(agent_name, project_root)
145+
if visual:
146+
return visual.get("eye", _FALLBACK_EYE)
147+
return _FALLBACK_EYE
148+
except Exception:
149+
return _FALLBACK_EYE

packages/claude-code-plugin/hooks/pre-tool-use.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,19 @@ def _handle(data: dict) -> Optional[dict]:
201201
else:
202202
status_msg = tdd_indicator
203203

204+
# Append compact council badge when council is active (#1367)
205+
try:
206+
from council_badge import build_council_badge
207+
208+
badge = build_council_badge()
209+
if badge:
210+
if status_msg:
211+
status_msg = f"{status_msg} {badge}"
212+
else:
213+
status_msg = badge
214+
except Exception:
215+
pass
216+
204217
tool_name = data.get("tool_name", "")
205218
contexts = []
206219

0 commit comments

Comments
 (0)