From 0b5bd14d1c65eff586489e7eaa01b5a0e06c0baa Mon Sep 17 00:00:00 2001 From: exiao <2093036+exiao@users.noreply.github.com> Date: Thu, 7 May 2026 16:19:07 -0400 Subject: [PATCH 1/2] feat: suppress raw previews for friendly-named tools Tools with a friendly name now hide their preview by default (shell commands, file paths, etc. are implementation noise for end users). New config: display.tool_show_preview (list) opts specific tools back in so e.g. WebSearch can still show the query string. Patch note: ~/.hermes/plans/hermes-patches/tool-friendly-names.md --- gateway/run.py | 16 ++++++++-- tests/test_tool_friendly_names.py | 50 +++++++++++++++++++++++++++++-- 2 files changed, 62 insertions(+), 4 deletions(-) diff --git a/gateway/run.py b/gateway/run.py index 5dcd6801bb08..fb633cb1eb82 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -11958,6 +11958,12 @@ def _run_still_current() -> bool: if not isinstance(_tool_friendly_names, dict): _tool_friendly_names = {} + # Tools whose preview (query/argument) should still be shown even when + # a friendly name is active. By default, tools with a friendly name + # suppress previews (raw shell commands, file paths, etc. are noise for + # end users). List tool names here to opt back in. + _tool_show_preview = set(display_config.get("tool_show_preview") or []) + def progress_callback(event_type: str, tool_name: str = None, preview: str = None, args: dict = None, **kwargs): """Callback invoked by agent on tool lifecycle events.""" if not progress_queue or not _run_still_current(): @@ -12022,10 +12028,14 @@ def progress_callback(event_type: str, tool_name: str = None, preview: str = Non from agent.display import get_tool_emoji emoji = get_tool_emoji(tool_name, default="⚙️") display_name = _tool_friendly_names.get(tool_name, tool_name or "") + _has_friendly = tool_name in _tool_friendly_names + _suppress_preview = _has_friendly and tool_name not in _tool_show_preview # Verbose mode: show detailed arguments, respects tool_preview_length if progress_mode == "verbose": - if args: + if _suppress_preview: + msg = f"{emoji} {display_name}..." + elif args: from agent.display import get_tool_preview_max_len _pl = get_tool_preview_max_len() args_str = json.dumps(args, ensure_ascii=False, default=str) @@ -12045,7 +12055,9 @@ def progress_callback(event_type: str, tool_name: str = None, preview: str = Non # "all" / "new" modes: short preview, respects tool_preview_length # config (defaults to 40 chars when unset to keep gateway messages # compact — unlike CLI spinners, these persist as permanent messages). - if preview: + if _suppress_preview: + msg = f"{emoji} {display_name}..." + elif preview: from agent.display import get_tool_preview_max_len _pl = get_tool_preview_max_len() _cap = _pl if _pl > 0 else 40 diff --git a/tests/test_tool_friendly_names.py b/tests/test_tool_friendly_names.py index d43b3c7fd935..5c53c5c94b42 100644 --- a/tests/test_tool_friendly_names.py +++ b/tests/test_tool_friendly_names.py @@ -18,12 +18,18 @@ def _build_progress_callback(display_config=None, progress_queue=None): if not isinstance(_tool_friendly_names, dict): _tool_friendly_names = {} + _tool_show_preview = set(display_config.get("tool_show_preview") or []) + def progress_callback(tool_name, preview=None, args=None): from agent.display import get_tool_emoji emoji = get_tool_emoji(tool_name, default="⚙️") - display_name = _tool_friendly_names.get(tool_name, tool_name) + display_name = _tool_friendly_names.get(tool_name, tool_name or "") + _has_friendly = tool_name in _tool_friendly_names + _suppress_preview = _has_friendly and tool_name not in _tool_show_preview - if preview: + if _suppress_preview: + msg = f"{emoji} {display_name}..." + elif preview: msg = f'{emoji} {display_name}: "{preview}"' else: msg = f"{emoji} {display_name}..." @@ -93,3 +99,43 @@ def test_non_dict_friendly_names_ignored(self): cb, q = _build_progress_callback(display_config=config) msg = cb("WebSearch", preview="test") assert "WebSearch" in msg + + # --- Preview suppression tests --- + + def test_friendly_name_suppresses_preview_by_default(self): + """Tools with a friendly name hide their preview (raw args are noise).""" + config = {"tool_friendly_names": {"terminal": "Looking up data"}} + cb, q = _build_progress_callback(display_config=config) + msg = cb("terminal", preview="scripts/serper.sh 'AAPL'") + assert "Looking up data..." in msg + assert "serper" not in msg + + def test_show_preview_opts_tool_back_in(self): + """tool_show_preview list re-enables preview for specific tools.""" + config = { + "tool_friendly_names": {"WebSearch": "Searching the web"}, + "tool_show_preview": ["WebSearch"], + } + cb, q = _build_progress_callback(display_config=config) + msg = cb("WebSearch", preview="AAPL earnings") + assert "Searching the web" in msg + assert "AAPL earnings" in msg + + def test_unmapped_tool_still_shows_preview(self): + """Tools without a friendly name always show preview (no suppression).""" + config = {"tool_friendly_names": {"terminal": "Looking up data"}} + cb, q = _build_progress_callback(display_config=config) + msg = cb("WebSearch", preview="AAPL earnings") + assert "WebSearch" in msg + assert "AAPL earnings" in msg + + def test_show_preview_empty_list(self): + """Empty tool_show_preview = all friendly-named tools suppress preview.""" + config = { + "tool_friendly_names": {"terminal": "Looking up data"}, + "tool_show_preview": [], + } + cb, q = _build_progress_callback(display_config=config) + msg = cb("terminal", preview="/root/.hermes/scripts/foo.sh") + assert "Looking up data..." in msg + assert "/root" not in msg From df3f306be1ed60b9fc38e6268326dab93e000bf6 Mon Sep 17 00:00:00 2001 From: exiao <2093036+exiao@users.noreply.github.com> Date: Thu, 7 May 2026 16:25:56 -0400 Subject: [PATCH 2/2] fix: isinstance guard for non-list tool_show_preview values set('WebSearch') produces {'W','e','b',...} (character set) instead of {'WebSearch'}. Use isinstance check to reject non-list values safely. --- gateway/run.py | 3 ++- tests/test_tool_friendly_names.py | 15 ++++++++++++++- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/gateway/run.py b/gateway/run.py index fb633cb1eb82..96cdc8713261 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -11962,7 +11962,8 @@ def _run_still_current() -> bool: # a friendly name is active. By default, tools with a friendly name # suppress previews (raw shell commands, file paths, etc. are noise for # end users). List tool names here to opt back in. - _tool_show_preview = set(display_config.get("tool_show_preview") or []) + _raw_show = display_config.get("tool_show_preview") + _tool_show_preview = set(_raw_show) if isinstance(_raw_show, list) else set() def progress_callback(event_type: str, tool_name: str = None, preview: str = None, args: dict = None, **kwargs): """Callback invoked by agent on tool lifecycle events.""" diff --git a/tests/test_tool_friendly_names.py b/tests/test_tool_friendly_names.py index 5c53c5c94b42..20971bba917c 100644 --- a/tests/test_tool_friendly_names.py +++ b/tests/test_tool_friendly_names.py @@ -18,7 +18,8 @@ def _build_progress_callback(display_config=None, progress_queue=None): if not isinstance(_tool_friendly_names, dict): _tool_friendly_names = {} - _tool_show_preview = set(display_config.get("tool_show_preview") or []) + _raw_show = display_config.get("tool_show_preview") + _tool_show_preview = set(_raw_show) if isinstance(_raw_show, list) else set() def progress_callback(tool_name, preview=None, args=None): from agent.display import get_tool_emoji @@ -139,3 +140,15 @@ def test_show_preview_empty_list(self): msg = cb("terminal", preview="/root/.hermes/scripts/foo.sh") assert "Looking up data..." in msg assert "/root" not in msg + + def test_show_preview_string_value_ignored(self): + """If tool_show_preview is a string (not list), treat as empty.""" + config = { + "tool_friendly_names": {"WebSearch": "Searching the web"}, + "tool_show_preview": "WebSearch", + } + cb, q = _build_progress_callback(display_config=config) + msg = cb("WebSearch", preview="AAPL") + # String is not a list, so it's treated as empty = preview suppressed + assert "Searching the web..." in msg + assert "AAPL" not in msg