diff --git a/agent/utils/terminal_display.py b/agent/utils/terminal_display.py index a10ac33f..a3cb742b 100644 --- a/agent/utils/terminal_display.py +++ b/agent/utils/terminal_display.py @@ -6,6 +6,7 @@ import re from rich.console import Console +from rich.markup import escape from rich.markdown import Heading, Markdown from rich.panel import Panel from rich.theme import Theme @@ -446,23 +447,72 @@ def print_yolo_approve(count: int) -> None: # ── Help ─────────────────────────────────────────────────────────────── -HELP_TEXT = f"""\ -{_I}[bold]Commands[/bold] -{_I} [cyan]/help[/cyan] Show this help -{_I} [cyan]/undo[/cyan] Undo last turn -{_I} [cyan]/compact[/cyan] Compact context window -{_I} [cyan]/resume[/cyan] [index|id|path] Pick up from a log in ./session_logs -{_I} [cyan]/model[/cyan] [id] Show available models or switch -{_I} [cyan]/effort[/cyan] [level] Reasoning effort (minimal|low|medium|high|xhigh|max|off) -{_I} [cyan]/yolo[/cyan] Toggle auto-approve mode -{_I} [cyan]/status[/cyan] Current model & turn count -{_I} [cyan]/share-traces[/cyan] [public|private] Show/flip visibility of your HF trace dataset -{_I} [cyan]/quit[/cyan] Exit""" +HELP_ROWS: tuple[tuple[str, str, str], ...] = ( + ("/help", "", "Show this help"), + ("/undo", "", "Undo last turn"), + ("/compact", "", "Compact context window"), + ("/resume", "[index|id|path]", "Pick up from ./session_logs"), + ("/model", "[id]", "Show available models or switch"), + ( + "/effort", + "[level]", + "Set reasoning effort preference", + ), + ("/yolo", "", "Toggle auto-approve mode"), + ("/status", "", "Current model & turn count"), + ( + "/share-traces", + "[public|private]", + "Show or change HF trace visibility", + ), + ("/quit", "", "Exit"), +) + + +def _help_column_widths( + rows: tuple[tuple[str, str, str], ...], +) -> tuple[int, int]: + return ( + max(len(command) for command, _, _ in rows), + max(len(args) for _, args, _ in rows), + ) + + +def _format_help_row( + command: str, + args: str, + description: str, + command_width: int, + args_width: int, +) -> str: + command_gap = " " * (command_width - len(command) + 2) + args_gap = " " * (args_width - len(args) + 2) + command_markup = f"[cyan]{escape(command)}[/cyan]" + args_markup = f"[muted]{escape(args)}[/muted]" if args else "" + return f"{_I} {command_markup}{command_gap}{args_markup}{args_gap}{description}" + + +def format_help_text(rows: tuple[tuple[str, str, str], ...] | None = None) -> str: + help_rows = HELP_ROWS if rows is None else rows + command_width, args_width = _help_column_widths(help_rows) + return "\n".join( + [f"{_I}[bold]Commands[/bold]"] + + [ + _format_help_row( + command, + args, + description, + command_width, + args_width, + ) + for command, args, description in help_rows + ] + ) def print_help() -> None: _console.print() - _console.print(HELP_TEXT) + _console.print(format_help_text()) _console.print() diff --git a/tests/unit/test_cli_rendering.py b/tests/unit/test_cli_rendering.py index e94700bf..e56d8c20 100644 --- a/tests/unit/test_cli_rendering.py +++ b/tests/unit/test_cli_rendering.py @@ -5,6 +5,7 @@ from types import SimpleNamespace import pytest +from rich.console import Console import agent.main as main_mod from agent.tools.research_tool import _get_research_model @@ -29,6 +30,50 @@ def test_non_anthropic_research_model_is_unchanged(): assert _get_research_model("openai/gpt-5.4") == "openai/gpt-5.4" +def test_help_output_keeps_descriptions_aligned(monkeypatch): + output = StringIO() + console = Console( + file=output, + color_system=None, + theme=terminal_display._THEME, + width=120, + ) + monkeypatch.setattr(terminal_display, "_console", console) + + terminal_display.print_help() + + lines = [line.rstrip() for line in output.getvalue().splitlines() if line.strip()] + description_columns = [] + for command, args, description in terminal_display.HELP_ROWS: + line = next(line for line in lines if command in line) + if args: + assert args in line + description_columns.append(line.index(description)) + + assert len(set(description_columns)) == 1 + + +def test_help_output_recomputes_widths_from_rows(): + rows = terminal_display.HELP_ROWS + ( + ("/longer-command", "[longer-args]", "Synthetic help row"), + ) + output = StringIO() + Console( + file=output, + color_system=None, + theme=terminal_display._THEME, + width=140, + ).print(terminal_display.format_help_text(rows)) + + lines = [line.rstrip() for line in output.getvalue().splitlines() if line.strip()] + description_columns = [ + next(line for line in lines if command in line).index(description) + for command, _args, description in rows + ] + + assert len(set(description_columns)) == 1 + + def test_subagent_display_does_not_spawn_background_redraw(monkeypatch): calls: list[object] = []