Skip to content

Commit c0995a9

Browse files
committed
feat(plugin): add TDD cycle mini progress bar in PreToolUse (#1035)
Add build_tdd_indicator() that renders current TDD phase as a visual progress bar [RED ● GREEN ○ REFACTOR ○] in the spinner status message. Phase is read from CODINGBUDDY_TDD_PHASE env var. Supports optional cycle count display.
1 parent 761a406 commit c0995a9

3 files changed

Lines changed: 152 additions & 0 deletions

File tree

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
"""TDD cycle mini progress bar for status display (#1035).
2+
3+
Builds a visual indicator showing current TDD phase progress:
4+
[RED ● GREEN ○ REFACTOR ○]
5+
6+
Reads phase from CODINGBUDDY_TDD_PHASE env var or explicit argument.
7+
"""
8+
import os
9+
from typing import Optional
10+
11+
# TDD phases in sequential order
12+
TDD_PHASES = ["RED", "GREEN", "REFACTOR"]
13+
14+
# Display symbols
15+
ACTIVE = "\u25cf" # ●
16+
INACTIVE = "\u25cb" # ○
17+
18+
19+
def build_tdd_indicator(
20+
phase: Optional[str] = None,
21+
cycle_count: Optional[int] = None,
22+
) -> Optional[str]:
23+
"""Build a one-line TDD cycle progress indicator.
24+
25+
Args:
26+
phase: Current TDD phase (RED, GREEN, REFACTOR).
27+
If None, reads from CODINGBUDDY_TDD_PHASE env var.
28+
cycle_count: Number of completed TDD cycles. Appended as '#N'
29+
when > 0.
30+
31+
Returns:
32+
Formatted string like '[RED ● GREEN ○ REFACTOR ○]' or None
33+
if no valid phase is active.
34+
"""
35+
if phase is None:
36+
phase = os.environ.get("CODINGBUDDY_TDD_PHASE", "")
37+
38+
if not phase:
39+
return None
40+
41+
phase = phase.upper().strip()
42+
if phase not in TDD_PHASES:
43+
return None
44+
45+
phase_index = TDD_PHASES.index(phase)
46+
parts = []
47+
for i, p in enumerate(TDD_PHASES):
48+
symbol = ACTIVE if i <= phase_index else INACTIVE
49+
parts.append(f"{p} {symbol}")
50+
51+
indicator = f"[{' '.join(parts)}]"
52+
53+
if cycle_count and cycle_count > 0:
54+
indicator = f"{indicator} #{cycle_count}"
55+
56+
return indicator

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from config import get_config
2626
from agent_status import build_status_message
2727
from adaptive_perf import get_monitor
28+
from tdd_progress import build_tdd_indicator
2829

2930
# Pattern to detect git commit in a command string
3031
_GIT_COMMIT_RE = re.compile(r"\bgit\s+commit\b")
@@ -179,6 +180,14 @@ def _handle(data: dict) -> Optional[dict]:
179180
# Build agent status message for spinner (#974) — applies to ALL tools
180181
status_msg = build_status_message()
181182

183+
# Append TDD cycle progress indicator (#1035)
184+
tdd_indicator = build_tdd_indicator()
185+
if tdd_indicator:
186+
if status_msg:
187+
status_msg = f"{status_msg} {tdd_indicator}"
188+
else:
189+
status_msg = tdd_indicator
190+
182191
tool_name = data.get("tool_name", "")
183192
contexts = []
184193

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
"""Tests for tdd_progress module (#1035)."""
2+
import os
3+
import sys
4+
5+
import pytest
6+
7+
# Add lib to path
8+
_hooks_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
9+
_lib_dir = os.path.join(_hooks_dir, "lib")
10+
if _lib_dir not in sys.path:
11+
sys.path.insert(0, _lib_dir)
12+
13+
from tdd_progress import build_tdd_indicator
14+
15+
16+
class TestBuildTddIndicator:
17+
"""Test build_tdd_indicator function."""
18+
19+
def test_red_phase(self):
20+
result = build_tdd_indicator("RED")
21+
assert result == "[RED \u25cf GREEN \u25cb REFACTOR \u25cb]"
22+
23+
def test_green_phase(self):
24+
result = build_tdd_indicator("GREEN")
25+
assert result == "[RED \u25cf GREEN \u25cf REFACTOR \u25cb]"
26+
27+
def test_refactor_phase(self):
28+
result = build_tdd_indicator("REFACTOR")
29+
assert result == "[RED \u25cf GREEN \u25cf REFACTOR \u25cf]"
30+
31+
def test_none_returns_none(self):
32+
assert build_tdd_indicator(None) is None
33+
34+
def test_empty_string_returns_none(self):
35+
assert build_tdd_indicator("") is None
36+
37+
def test_invalid_phase_returns_none(self):
38+
assert build_tdd_indicator("INVALID") is None
39+
40+
def test_lowercase_phase(self):
41+
result = build_tdd_indicator("red")
42+
assert result == "[RED \u25cf GREEN \u25cb REFACTOR \u25cb]"
43+
44+
def test_mixed_case_phase(self):
45+
result = build_tdd_indicator("Green")
46+
assert result == "[RED \u25cf GREEN \u25cf REFACTOR \u25cb]"
47+
48+
def test_whitespace_trimmed(self):
49+
result = build_tdd_indicator(" RED ")
50+
assert result == "[RED \u25cf GREEN \u25cb REFACTOR \u25cb]"
51+
52+
53+
class TestBuildTddIndicatorFromEnv:
54+
"""Test build_tdd_indicator reads from environment when no arg given."""
55+
56+
def test_reads_env_var(self, monkeypatch):
57+
monkeypatch.setenv("CODINGBUDDY_TDD_PHASE", "GREEN")
58+
result = build_tdd_indicator()
59+
assert result == "[RED \u25cf GREEN \u25cf REFACTOR \u25cb]"
60+
61+
def test_no_env_var_returns_none(self, monkeypatch):
62+
monkeypatch.delenv("CODINGBUDDY_TDD_PHASE", raising=False)
63+
assert build_tdd_indicator() is None
64+
65+
def test_empty_env_var_returns_none(self, monkeypatch):
66+
monkeypatch.setenv("CODINGBUDDY_TDD_PHASE", "")
67+
assert build_tdd_indicator() is None
68+
69+
70+
class TestBuildTddIndicatorWithCycleCount:
71+
"""Test TDD indicator with cycle count display."""
72+
73+
def test_red_with_cycle_count(self):
74+
result = build_tdd_indicator("RED", cycle_count=3)
75+
assert result == "[RED \u25cf GREEN \u25cb REFACTOR \u25cb] #3"
76+
77+
def test_green_with_cycle_count(self):
78+
result = build_tdd_indicator("GREEN", cycle_count=1)
79+
assert result == "[RED \u25cf GREEN \u25cf REFACTOR \u25cb] #1"
80+
81+
def test_zero_cycle_count_omitted(self):
82+
result = build_tdd_indicator("RED", cycle_count=0)
83+
assert result == "[RED \u25cf GREEN \u25cb REFACTOR \u25cb]"
84+
85+
def test_none_cycle_count_omitted(self):
86+
result = build_tdd_indicator("RED", cycle_count=None)
87+
assert result == "[RED \u25cf GREEN \u25cb REFACTOR \u25cb]"

0 commit comments

Comments
 (0)