diff --git a/packages/claude-code-plugin/hooks/lib/tiny_actor_grid.py b/packages/claude-code-plugin/hooks/lib/tiny_actor_grid.py index 62aaba09..6a94699a 100644 --- a/packages/claude-code-plugin/hooks/lib/tiny_actor_grid.py +++ b/packages/claude-code-plugin/hooks/lib/tiny_actor_grid.py @@ -7,6 +7,8 @@ from itertools import zip_longest +from buddy_renderer import display_width, pad_to_display_width + def layout_grid( cards: list[list[str]], @@ -55,7 +57,7 @@ def layout_grid( padded_cards.append(padded) for line_parts in zip_longest(*padded_cards, fillvalue=" " * card_width): - parts = [part.ljust(card_width) for part in line_parts] + parts = [pad_to_display_width(part, card_width) for part in line_parts] # Pad row to full width when fewer cards than max_columns while len(parts) < max_columns: parts.append(" " * card_width) diff --git a/packages/claude-code-plugin/tests/test_tiny_actor_grid.py b/packages/claude-code-plugin/tests/test_tiny_actor_grid.py index 3c4d4b04..77126867 100644 --- a/packages/claude-code-plugin/tests/test_tiny_actor_grid.py +++ b/packages/claude-code-plugin/tests/test_tiny_actor_grid.py @@ -10,6 +10,7 @@ if _lib_dir not in sys.path: sys.path.insert(0, _lib_dir) +from buddy_renderer import display_width from tiny_actor_grid import layout_grid # --------------------------------------------------------------------------- @@ -79,6 +80,65 @@ def test_all_lines_consistent_width(self): widths = {len(line) for line in lines} assert len(widths) == 1, f"Expected uniform width, got {widths}" + def test_cjk_labels_consistent_display_width(self): + """Cards with CJK text must produce uniform display width across rows.""" + from buddy_renderer import pad_to_display_width + + def _cjk_card() -> list[str]: + return [ + pad_to_display_width("한국어", _CARD_WIDTH), + pad_to_display_width("테스트", _CARD_WIDTH), + pad_to_display_width("라벨", _CARD_WIDTH), + ] + + cards = [_cjk_card(), _make_card(label="A"), _cjk_card()] + lines = layout_grid(cards, available_width=80, card_width=_CARD_WIDTH, gap=2) + widths = {display_width(line) for line in lines} + assert len(widths) == 1, f"CJK grid display widths not uniform: {widths}" + + def test_ansi_colored_text_consistent_display_width(self): + """Cards with ANSI escape codes must produce uniform display width.""" + from buddy_renderer import pad_to_display_width + + RED = "\033[31m" + RESET = "\033[0m" + + def _ansi_card() -> list[str]: + return [ + pad_to_display_width(f"{RED}colored{RESET}", _CARD_WIDTH), + pad_to_display_width(f"{RED}face{RESET}", _CARD_WIDTH), + pad_to_display_width(f"{RED}text{RESET}", _CARD_WIDTH), + ] + + cards = [_ansi_card(), _make_card(label="B"), _ansi_card()] + lines = layout_grid(cards, available_width=80, card_width=_CARD_WIDTH, gap=2) + widths = {display_width(line) for line in lines} + assert len(widths) == 1, f"ANSI grid display widths not uniform: {widths}" + + def test_mixed_cjk_ansi_ascii_consistent(self): + """Mixed CJK, ANSI, and ASCII cards in one grid stay aligned.""" + from buddy_renderer import pad_to_display_width + + GREEN = "\033[32m" + RESET = "\033[0m" + + cjk_card = [ + pad_to_display_width("보안전문가", _CARD_WIDTH), + pad_to_display_width("분석중", _CARD_WIDTH), + pad_to_display_width("완료", _CARD_WIDTH), + ] + ansi_card = [ + pad_to_display_width(f"{GREEN}status{RESET}", _CARD_WIDTH), + pad_to_display_width(f"{GREEN}ok{RESET}", _CARD_WIDTH), + pad_to_display_width(f"{GREEN}done{RESET}", _CARD_WIDTH), + ] + ascii_card = _make_card(label="Z") + + cards = [cjk_card, ansi_card, ascii_card] + lines = layout_grid(cards, available_width=80, card_width=_CARD_WIDTH, gap=2) + widths = {display_width(line) for line in lines} + assert len(widths) == 1, f"Mixed grid display widths not uniform: {widths}" + # --------------------------------------------------------------------------- # Edge cases