From 847d7cbea582cf6d15f6c280bdf28990b1369df5 Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Tue, 14 Apr 2026 16:58:23 -0700 Subject: [PATCH] fix: improve CLI text padding, word-wrap for responses and verbose tool output (#9920) * feat(skills): add fitness-nutrition skill to optional-skills Cherry-picked from PR #9177 by @haileymarshall. Adds a fitness and nutrition skill for gym-goers and health-conscious users: - Exercise search via wger API (690+ exercises, free, no auth) - Nutrition lookup via USDA FoodData Central (380K+ foods, DEMO_KEY fallback) - Offline body composition calculators (BMI, TDEE, 1RM, macros, body fat %) - Pure stdlib Python, no pip dependencies Changes from original PR: - Moved from skills/ to optional-skills/health/ (correct location) - Fixed BMR formula in FORMULAS.md (removed confusing -5+10, now just +5) - Fixed author attribution to match PR submitter - Marked USDA_API_KEY as optional (DEMO_KEY works without signup) Also adds optional env var support to the skill readiness checker: - New 'optional: true' field in required_environment_variables entries - Optional vars are preserved in metadata but don't block skill readiness - Optional vars skip the CLI capture prompt flow - Skills with only optional missing vars show as 'available' not 'setup_needed' * fix: increase CLI response text padding to 4-space tab indent Increases horizontal padding on all response display paths: - Rich Panel responses (main, background, /btw): padding (1,2) -> (1,4) - Streaming text: add 4-space indent prefix to each line - Streaming TTS: add 4-space indent prefix to sentences Gives response text proper breathing room with a tab-width indent. Rich Panel word wrapping automatically adjusts for the wider padding. Requested by AriesTheCoder. * fix: word-wrap verbose tool call args and results to terminal width Verbose mode (tool_progress: verbose) printed tool args and results as single unwrapped lines that could be thousands of characters long. Adds _wrap_verbose() helper that: - Pretty-prints JSON args with indent=2 instead of one-line dumps - Splits text on existing newlines (preserves JSON/structured output) - Wraps lines exceeding terminal width with 5-char continuation indent - Uses break_long_words=True for URLs and paths without spaces Applied to all 4 verbose print sites: - Concurrent tool call args - Concurrent tool results - Sequential tool call args - Sequential tool results --------- Co-authored-by: haileymarshall --- cli.py | 13 +++++++------ run_agent.py | 33 +++++++++++++++++++++++++++++---- 2 files changed, 36 insertions(+), 10 deletions(-) diff --git a/cli.py b/cli.py index ebc8b7637b..d679b24b5c 100644 --- a/cli.py +++ b/cli.py @@ -989,6 +989,7 @@ def _prune_orphaned_branches(repo_root: str) -> None: _ACCENT_ANSI_DEFAULT = "\033[1;38;2;255;215;0m" # True-color #FFD700 bold — fallback _BOLD = "\033[1m" _RST = "\033[0m" +_STREAM_PAD = " " # 4-space indent for streamed response text (matches Panel padding) def _hex_to_ansi(hex_color: str, *, bold: bool = False) -> str: @@ -2580,7 +2581,7 @@ class HermesCLI: _tc = getattr(self, "_stream_text_ansi", "") while "\n" in self._stream_buf: line, self._stream_buf = self._stream_buf.split("\n", 1) - _cprint(f"{_tc}{line}{_RST}" if _tc else line) + _cprint(f"{_STREAM_PAD}{_tc}{line}{_RST}" if _tc else f"{_STREAM_PAD}{line}") def _flush_stream(self) -> None: """Emit any remaining partial line from the stream buffer and close the box.""" @@ -2597,7 +2598,7 @@ class HermesCLI: if self._stream_buf: _tc = getattr(self, "_stream_text_ansi", "") - _cprint(f"{_tc}{self._stream_buf}{_RST}" if _tc else self._stream_buf) + _cprint(f"{_STREAM_PAD}{_tc}{self._stream_buf}{_RST}" if _tc else f"{_STREAM_PAD}{self._stream_buf}") self._stream_buf = "" # Close the response box @@ -5761,7 +5762,7 @@ class HermesCLI: border_style=_resp_color, style=_resp_text, box=rich_box.HORIZONTALS, - padding=(1, 2), + padding=(1, 4), )) else: _cprint(" (No response generated)") @@ -5885,7 +5886,7 @@ class HermesCLI: title_align="left", border_style=_resp_color, box=rich_box.HORIZONTALS, - padding=(1, 2), + padding=(1, 4), )) else: _cprint(" 💬 /btw: (no response)") @@ -7648,7 +7649,7 @@ class HermesCLI: label = " ⚕ Hermes " fill = w - 2 - len(label) _cprint(f"\n{_ACCENT}╭─{label}{'─' * max(fill - 1, 0)}╮{_RST}") - _cprint(sentence.rstrip()) + _cprint(f"{_STREAM_PAD}{sentence.rstrip()}") tts_thread = threading.Thread( target=stream_tts_to_speaker, @@ -7879,7 +7880,7 @@ class HermesCLI: border_style=_resp_color, style=_resp_text, box=rich_box.HORIZONTALS, - padding=(1, 2), + padding=(1, 4), )) diff --git a/run_agent.py b/run_agent.py index 626951b276..5f4ac68dcd 100644 --- a/run_agent.py +++ b/run_agent.py @@ -6975,6 +6975,31 @@ class AIAgent: skip_pre_tool_call_hook=True, ) + @staticmethod + def _wrap_verbose(label: str, text: str, indent: str = " ") -> str: + """Word-wrap verbose tool output to fit the terminal width. + + Splits *text* on existing newlines and wraps each line individually, + preserving intentional line breaks (e.g. pretty-printed JSON). + Returns a ready-to-print string with *label* on the first line and + continuation lines indented. + """ + import shutil as _shutil + import textwrap as _tw + cols = _shutil.get_terminal_size((120, 24)).columns + wrap_width = max(40, cols - len(indent)) + out_lines: list[str] = [] + for raw_line in text.split("\n"): + if len(raw_line) <= wrap_width: + out_lines.append(raw_line) + else: + wrapped = _tw.wrap(raw_line, width=wrap_width, + break_long_words=True, + break_on_hyphens=False) + out_lines.extend(wrapped or [raw_line]) + body = ("\n" + indent).join(out_lines) + return f"{indent}{label}{body}" + def _execute_tool_calls_concurrent(self, assistant_message, messages: list, effective_task_id: str, api_call_count: int = 0) -> None: """Execute multiple tool calls concurrently using a thread pool. @@ -7045,7 +7070,7 @@ class AIAgent: args_str = json.dumps(args, ensure_ascii=False) if self.verbose_logging: print(f" 📞 Tool {i}: {name}({list(args.keys())})") - print(f" Args: {args_str}") + print(self._wrap_verbose("Args: ", json.dumps(args, indent=2, ensure_ascii=False))) else: args_preview = args_str[:self.log_prefix_chars] + "..." if len(args_str) > self.log_prefix_chars else args_str print(f" 📞 Tool {i}: {name}({list(args.keys())}) - {args_preview}") @@ -7143,7 +7168,7 @@ class AIAgent: elif not self.quiet_mode: if self.verbose_logging: print(f" ✅ Tool {i+1} completed in {tool_duration:.2f}s") - print(f" Result: {function_result}") + print(self._wrap_verbose("Result: ", function_result)) else: response_preview = function_result[:self.log_prefix_chars] + "..." if len(function_result) > self.log_prefix_chars else function_result print(f" ✅ Tool {i+1} completed in {tool_duration:.2f}s - {response_preview}") @@ -7236,7 +7261,7 @@ class AIAgent: args_str = json.dumps(function_args, ensure_ascii=False) if self.verbose_logging: print(f" 📞 Tool {i}: {function_name}({list(function_args.keys())})") - print(f" Args: {args_str}") + print(self._wrap_verbose("Args: ", json.dumps(function_args, indent=2, ensure_ascii=False))) else: args_preview = args_str[:self.log_prefix_chars] + "..." if len(args_str) > self.log_prefix_chars else args_str print(f" 📞 Tool {i}: {function_name}({list(function_args.keys())}) - {args_preview}") @@ -7524,7 +7549,7 @@ class AIAgent: if not self.quiet_mode: if self.verbose_logging: print(f" ✅ Tool {i} completed in {tool_duration:.2f}s") - print(f" Result: {function_result}") + print(self._wrap_verbose("Result: ", function_result)) else: response_preview = function_result[:self.log_prefix_chars] + "..." if len(function_result) > self.log_prefix_chars else function_result print(f" ✅ Tool {i} completed in {tool_duration:.2f}s - {response_preview}")