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.
This commit is contained in:
Teknium
2026-09-02 11:18:54 -07:00
parent 9d745e3162
commit 0227cf7d9c
+37 -77
View File
@@ -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.