From 0227cf7d9c443b6230ddbc79716bc1d37c22c5ad Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Wed, 2 Sep 2026 11:18:54 -0700 Subject: [PATCH] refactor(cli): unify the 3 copies of the TUI panel helpers into module-level functions _panel_box_width/_wrap_panel_text/_append_panel_line/_append_blank_panel_line were defined identically inside run(), _get_approval_display_fragments and _get_slash_confirm_display_fragments (the latter two with whitespace-preserving wrap and different width defaults). One module-level set; the two fragment renderers alias _wrap_panel_text_keep_ws and pass their width defaults explicitly. --- cli.py | 114 +++++++++++++++++++-------------------------------------- 1 file changed, 37 insertions(+), 77 deletions(-) diff --git a/cli.py b/cli.py index 83663c4817..06f376a673 100644 --- a/cli.py +++ b/cli.py @@ -26,6 +26,7 @@ except ModuleNotFoundError: import logging import copy import os +import functools import shutil import sys import json @@ -5193,6 +5194,38 @@ def _should_seed_interactive(query, image, quiet: bool, oneshot: bool) -> bool: return False +def _panel_box_width(title: str, content_lines: list[str], min_width: int = 46, max_width: int = 76) -> int: + """Stable TUI panel width wide enough for the title and content (incl. borders).""" + term_cols = shutil.get_terminal_size((100, 20)).columns + longest = max([len(title)] + [len(line) for line in content_lines] + [min_width - 4]) + inner = min(max(longest + 4, min_width - 2), max_width - 2, max(24, term_cols - 6)) + return inner + 2 # account for the single leading/trailing spaces inside borders + + +def _wrap_panel_text(text: str, width: int, subsequent_indent: str = "", *, keep_ws: bool = False) -> list[str]: + """Wrap panel text; ``keep_ws`` preserves whitespace (command/detail previews).""" + if keep_ws: + kw = dict(replace_whitespace=False, drop_whitespace=False) + else: + kw = dict(break_long_words=False, break_on_hyphens=False) + wrapped = textwrap.wrap(text, width=max(8, width), subsequent_indent=subsequent_indent, **kw) + return wrapped or [""] + + +_wrap_panel_text_keep_ws = functools.partial(_wrap_panel_text, keep_ws=True) + + +def _append_panel_line(lines, border_style: str, content_style: str, text: str, box_width: int) -> None: + inner_width = max(0, box_width - 2) + lines.append((border_style, "│ ")) + lines.append((content_style, text.ljust(inner_width))) + lines.append((border_style, " │\n")) + + +def _append_blank_panel_line(lines, border_style: str, box_width: int) -> None: + lines.append((border_style, "│" + (" " * box_width) + "│\n")) + + class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin): """ Interactive CLI for the Hermes Agent. @@ -11121,8 +11154,7 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin): def _prompt_text_input(self, prompt_text: str) -> str | None: """Prompt for free-text input safely inside or outside prompt_toolkit. - ``run_in_terminal`` - returns a coroutine that must be awaited by the prompt_toolkit event loop, + ``run_in_terminal`` returns a coroutine that must be awaited by the prompt_toolkit event loop, which only exists on the main thread. Slash commands are dispatched from the ``process_loop`` daemon thread (see issue #23185), so calling ``run_in_terminal`` from there orphans the coroutine — ``_ask`` never runs, @@ -11351,30 +11383,7 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin): choices = state.get("choices") or [] selected = state.get("selected", 0) - def _panel_box_width(title_text: str, content_lines: list[str], min_width: int = 56, max_width: int = 86) -> int: - term_cols = shutil.get_terminal_size((100, 20)).columns - longest = max([len(title_text)] + [len(line) for line in content_lines] + [min_width - 4]) - inner = min(max(longest + 4, min_width - 2), max_width - 2, max(24, term_cols - 6)) - return inner + 2 - - def _wrap_panel_text(text: str, width: int, subsequent_indent: str = "") -> list[str]: - wrapped = textwrap.wrap( - text, - width=max(8, width), - replace_whitespace=False, - drop_whitespace=False, - subsequent_indent=subsequent_indent, - ) - return wrapped or [""] - - def _append_panel_line(lines, border_style: str, content_style: str, text: str, box_width: int) -> None: - inner_width = max(0, box_width - 2) - lines.append((border_style, "│ ")) - lines.append((content_style, text.ljust(inner_width))) - lines.append((border_style, " │\n")) - - def _append_blank_panel_line(lines, border_style: str, box_width: int) -> None: - lines.append((border_style, "│" + (" " * box_width) + "│\n")) + _wrap_panel_text = _wrap_panel_text_keep_ws preview_lines = [] for line in detail.splitlines(): @@ -11384,7 +11393,7 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin): preview_lines.extend(_wrap_panel_text(f"{marker} [{idx + 1}] {label} — {desc}", 72, subsequent_indent=" ")) preview_lines.append("Type 1/2/3 or use ↑/↓ then Enter. ESC/Ctrl+C cancels.") - box_width = _panel_box_width(title, preview_lines) + box_width = _panel_box_width(title, preview_lines, min_width=56, max_width=86) inner_text_width = max(8, box_width - 2) detail_wrapped = [] for line in detail.splitlines(): @@ -16574,30 +16583,7 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin): if not state: return [] - def _panel_box_width(title_text: str, content_lines: list[str], min_width: int = 46, max_width: int = 76) -> int: - term_cols = shutil.get_terminal_size((100, 20)).columns - longest = max([len(title_text)] + [len(line) for line in content_lines] + [min_width - 4]) - inner = min(max(longest + 4, min_width - 2), max_width - 2, max(24, term_cols - 6)) - return inner + 2 - - def _wrap_panel_text(text: str, width: int, subsequent_indent: str = "") -> list[str]: - wrapped = textwrap.wrap( - text, - width=max(8, width), - replace_whitespace=False, - drop_whitespace=False, - subsequent_indent=subsequent_indent, - ) - return wrapped or [""] - - def _append_panel_line(lines, border_style: str, content_style: str, text: str, box_width: int) -> None: - inner_width = max(0, box_width - 2) - lines.append((border_style, "│ ")) - lines.append((content_style, text.ljust(inner_width))) - lines.append((border_style, " │\n")) - - def _append_blank_panel_line(lines, border_style: str, box_width: int) -> None: - lines.append((border_style, "│" + (" " * box_width) + "│\n")) + _wrap_panel_text = _wrap_panel_text_keep_ws command = state["command"] description = state["description"] @@ -19999,32 +19985,6 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin): # --- Clarify tool: dynamic display widget for questions + choices --- - def _panel_box_width(title: str, content_lines: list[str], min_width: int = 46, max_width: int = 76) -> int: - """Choose a stable panel width wide enough for the title and content.""" - term_cols = shutil.get_terminal_size((100, 20)).columns - longest = max([len(title)] + [len(line) for line in content_lines] + [min_width - 4]) - inner = min(max(longest + 4, min_width - 2), max_width - 2, max(24, term_cols - 6)) - return inner + 2 # account for the single leading/trailing spaces inside borders - - def _wrap_panel_text(text: str, width: int, subsequent_indent: str = "") -> list[str]: - wrapped = textwrap.wrap( - text, - width=max(8, width), - break_long_words=False, - break_on_hyphens=False, - subsequent_indent=subsequent_indent, - ) - return wrapped or [""] - - def _append_panel_line(lines, border_style: str, content_style: str, text: str, box_width: int) -> None: - inner_width = max(0, box_width - 2) - lines.append((border_style, "│ ")) - lines.append((content_style, text.ljust(inner_width))) - lines.append((border_style, " │\n")) - - def _append_blank_panel_line(lines, border_style: str, box_width: int) -> None: - lines.append((border_style, "│" + (" " * box_width) + "│\n")) - def _get_clarify_batch_display(state): """Build styled text for the batch (multi-question) clarify panel.