chore: limit generic status phrases to long-running notifications

This commit is contained in:
devatnull 2026-06-28 22:02:39 +03:00 committed by Teknium
parent 4bf5b563bd
commit fddc95f4c2
12 changed files with 121 additions and 317 deletions

5
cli.py
View file

@ -3698,7 +3698,7 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin):
self.console = Console()
self.config = CLI_CONFIG
self.compact = compact if compact is not None else CLI_CONFIG["display"].get("compact", False)
# tool_progress: "off", "generic", "new", "all", "verbose" (from config.yaml display section)
# tool_progress: "off", "new", "all", "verbose" (from config.yaml display section)
# YAML 1.1 parses bare `off` as boolean False — normalise to string.
_raw_tp = CLI_CONFIG["display"].get("tool_progress", "all")
self.tool_progress_mode = "off" if _raw_tp is False else str(_raw_tp)
@ -9191,7 +9191,7 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin):
explicit ``-v``/``--verbose`` flag and the ``/verbose-logging``
toggle. See PR #6a1aa420e for the history that decoupled them.
"""
cycle = ["off", "generic", "new", "all", "verbose"]
cycle = ["off", "new", "all", "verbose"]
try:
idx = cycle.index(self.tool_progress_mode)
except ValueError:
@ -9212,7 +9212,6 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin):
from hermes_cli.colors import Colors as _Colors
labels = {
"off": f"{_Colors.DIM}Tool progress: OFF{_Colors.RESET} — silent mode, just the final response.",
"generic": f"{_Colors.YELLOW}Tool progress: GENERIC{_Colors.RESET} — show friendly placeholders instead of tool details.",
"new": f"{_Colors.YELLOW}Tool progress: NEW{_Colors.RESET} — show each new tool (skip repeats).",
"all": f"{_Colors.GREEN}Tool progress: ALL{_Colors.RESET} — show every tool call.",
"verbose": f"{_Colors.BOLD}{_Colors.GREEN}Tool progress: VERBOSE{_Colors.RESET} — full args, results, and think blocks.",

View file

@ -1,198 +1,52 @@
# Default phrases for generic gateway visibility surfaces.
#
# These are Hermes UI/status surfaces, not app/vendor/domain buckets. Keep them
# generic: do not mention Jira, Confluence, Gmail, local paths, model names,
# secrets, or tool arguments. User config can append/replace these via:
#
# display:
# status_phrases:
# mode: append # append | replace
# thinking:
# - one sec, thinking this through
# platforms:
# whatsapp:
# status_phrases:
# status:
# - still on it
thinking:
- one sec, thinking it through
- thinking this through
- hmm, working through it
- one sec, lining it up
- checking the shape of it
- following the thread
- sorting the pieces
- thinking through the tradeoffs
- one sec, making sure this fits
- working out the clean version
- holding the thought for a second
- checking the logic
- one sec, theres a detail here
- turning it over quickly
- making sure Im not missing a bit
- mapping the moving pieces
- letting the idea settle for a sec
- checking the edge cases
- narrowing the answer down
- one sec, this has a wrinkle
- tracing the dependency chain
- sorting signal from noise
- making the answer less wrong
- checking the assumption
- building the mental model
- sanity-checking it
- working through the awkward part
- finding the clean path
- one sec, connecting the dots
- checking whether that actually follows
tool:
- checking that now
- looking into it
- one sec, checking the details
- working through the lookup
- checking the actual thing
- pulling the thread
- one sec, doing the lookup
- checking the source
- looking at the live data
- grabbing the relevant bit
- checking whats there
- one sec, verifying it
- reading through it
- checking the current state
- looking for the signal
- checking the record
- digging through the source
- fetching the useful bit
- seeing what it says
- looking under the hood
- checking the real state
- pulling current context
- scanning the relevant output
- checking the available info
- one sec, comparing the pieces
- checking the latest version
- confirming against the source
- looking for the exact bit
- checking where this lands
- one sec, validating it
command:
- running it now
- checking what the machine says
- one sec, running the command
- running the check
- asking the machine directly
- letting it run for a sec
- checking the output
- running the useful bit
- one sec, getting a real result
- testing it locally
- checking the logs
- running the verification
- waiting on the command
- checking the system state
- letting the process finish
- running the test
- asking the shell
- waiting for the output
- checking the result
- letting the command complete
- running the boring bit
- checking the terminal output
- one sec, the machine is thinking
- running the local check
- waiting on the process
- checking whether it passes
- letting the job finish
- watching the output
- verifying it directly
- checking the exit status
interim:
- one sec, theres a bit more here
- still shaping the answer
- stitching this together
- almost got the useful version
- one sec, connecting the pieces
- working this into a clean answer
- still narrowing it down
- pulling this into shape
- one sec, checking the last part
- getting the answer into a useful form
- almost there
- one more bit to check
- turning this into something readable
- still pulling it together
- one sec, making this less messy
- checking the last thread
- shaping the final answer
- joining the dots
- just tightening the answer
- one sec, cleaning this up
- still organizing it
- almost got the shape
- one more pass
- making the useful part clearer
- still stitching
- checking the last detail
- making sure this lands cleanly
- one sec, summarizing the useful bit
- pulling the answer together
- final bit of cleanup
status:
- still on it
- still working through it
- this is taking a bit, still running
- still checking
- not stuck, just still running
- waiting on the slow part
- still alive, still working
- letting it finish
- still processing this
- one sec, this is still going
- still making progress
- waiting for the result
- still here, checking
- the slow bit is still running
- still working on the useful answer
- no result yet, still waiting
- still running the check
- waiting on the backend bit
- this one needs a minute
- still moving
- still going, not frozen
- waiting for the useful output
- still chewing through it
- one sec, long bit is still running
- still waiting on the result
- working through the slow step
- this is taking longer than usual
- still checking, no panic
- still in progress
- letting the slow thing finish
- still on it
- still working through it
- this is taking a bit, still running
- still checking
- not stuck, just still running
- waiting on the slow part
- still alive, still working
- letting it finish
- still processing this
- one sec, this is still going
- still making progress
- waiting for the result
- still here, checking
- the slow bit is still running
- still working on the useful answer
- no result yet, still waiting
- still running the check
- waiting on the backend bit
- this one needs a minute
- still moving
- still going, not frozen
- waiting for the useful output
- still chewing through it
- one sec, long bit is still running
- still waiting on the result
- working through the slow step
- this is taking longer than usual
- still checking, no panic
- still in progress
- letting the slow thing finish
generic:
- on it
- one sec
- checking that now
- give me a sec
- looking into it
- working through it
- checking
- still working
- one moment
- taking a look
- got it, checking
- give me a moment
- looking now
- sorting it
- checking the useful bit
- one sec, working on it
- taking a proper look
- checking the details
- working it out
- still looking
- on it
- one sec
- checking that now
- give me a sec
- looking into it
- working through it
- checking
- still working
- one moment
- taking a look
- got it, checking
- give me a moment
- looking now
- sorting it
- checking the useful bit
- one sec, working on it
- taking a proper look
- checking the details
- working it out
- still looking

View file

@ -234,7 +234,7 @@ def _normalise(setting: str, value: Any) -> Any:
if value is True:
return "all"
val = str(value).lower()
return val if val in {"off", "generic", "new", "all", "verbose"} else "all"
return val if val in {"off", "new", "all", "verbose"} else "all"
if setting in {
"show_reasoning",
"streaming",
@ -245,11 +245,7 @@ def _normalise(setting: str, value: Any) -> Any:
}:
if isinstance(value, str):
val = value.strip().lower()
if val == "generic" and setting in {
"thinking_progress",
"interim_assistant_messages",
"long_running_notifications",
}:
if val == "generic" and setting == "long_running_notifications":
return "generic"
return val in {"true", "1", "yes", "on", "raw", "verbose"}
return bool(value)

View file

@ -16499,6 +16499,7 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
*,
default: bool = False,
require_platform_override_for: set[Any] | None = None,
allow_generic: bool = False,
) -> str:
"""Return off|raw|generic for a gateway visibility surface."""
if require_platform_override_for:
@ -16514,7 +16515,7 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
return "off"
value = resolve_display_setting(user_config, platform_key, setting, default)
if isinstance(value, str) and value.strip().lower() == "generic":
return "generic"
return "generic" if allow_generic else "off"
return "raw" if bool(value) else "off"
def _generic_status_phrase(kind: str, *, tool_name: str | None = None, preview: str | None = None, args: Any = None) -> str:
@ -16689,10 +16690,7 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
if not _thinking_enabled:
return
thinking_text = preview if tool_name == "_thinking" else tool_name
if _thinking_mode == "generic":
msg = _generic_status_phrase("thinking", preview=str(thinking_text or ""))
else:
msg = f"💬 {thinking_text}" if thinking_text else None
msg = f"💬 {thinking_text}" if thinking_text else None
if msg:
progress_queue.put(msg)
return
@ -16727,17 +16725,6 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
return
last_tool[0] = tool_name
if tool_progress_generic:
progress_queue.put(
_generic_status_phrase(
"tool",
tool_name=str(tool_name or ""),
preview=str(preview or ""),
args=args,
)
)
return
# Build progress message with primary argument preview
from agent.display import get_tool_emoji
emoji = get_tool_emoji(tool_name, default="⚙️")
@ -17537,11 +17524,7 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
def _interim_assistant_cb(text: str, *, already_streamed: bool = False) -> None:
if not _run_still_current():
return
display_text = (
_generic_status_phrase("interim", preview=str(text or ""))
if interim_assistant_messages_mode == "generic"
else text
)
display_text = text
if _stream_consumer is not None:
if already_streamed:
_stream_consumer.on_segment_break()
@ -18727,6 +18710,7 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
_long_running_mode = _display_surface_mode(
"long_running_notifications",
default=True,
allow_generic=True,
)
if _long_running_mode == "off":
_NOTIFY_INTERVAL = None

View file

@ -2936,7 +2936,6 @@ class GatewaySlashCommandsMixin:
cycle = ["off", "new", "all", "verbose", "log"]
descriptions = {
"off": t("gateway.verbose.mode_off"),
"generic": "GENERIC — show human-friendly placeholders instead of tool details.",
"new": t("gateway.verbose.mode_new"),
"all": t("gateway.verbose.mode_all"),
"verbose": t("gateway.verbose.mode_verbose"),

View file

@ -1,8 +1,8 @@
"""Human-friendly generic gateway status phrases.
These helpers deliberately avoid relaying raw model scratch text. They turn
Hermes' own gateway visibility surfaces (thinking/tool/command/interim/status)
into short status lines suitable for chat surfaces.
Hermes' long-running gateway status surface into short status lines suitable
for chat surfaces.
Built-in defaults live in ``gateway/assets/status_phrases.yaml``. Users can add
portable, profile-relative phrase catalogs under ``HERMES_HOME`` either by using
@ -36,20 +36,15 @@ import yaml
from hermes_constants import get_hermes_home
# These are Hermes UI surfaces, not app/vendor/domain buckets. Do not add
# Jira/Confluence/Gmail/etc. categories here; tool names are only used to choose
# between the built-in "tool" and "command" surfaces.
_STATUS_SURFACES = ("thinking", "tool", "command", "interim", "status", "generic")
_COMMAND_TOOL_NAMES = {"terminal", "execute_code", "process"}
# These are Hermes UI surfaces, not app/vendor/domain buckets. Keep this
# long-running-only: regular tool/thinking/interim chatter is intentionally not
# rewritten into generic placeholders because that gets noisy fast in chat.
_STATUS_SURFACES = ("status", "generic")
_MAX_CUSTOM_PHRASES_PER_SURFACE = 80
_MAX_PHRASE_CHARS = 160
_CONVENTIONAL_RELATIVE_PATHS = ("status_phrases.yaml", "status_phrases")
_FALLBACK_PHRASES: dict[str, list[str]] = {
"thinking": ["one sec, thinking it through", "thinking this through", "checking the logic"],
"tool": ["checking that now", "looking into it", "checking the source"],
"command": ["running it now", "checking what the machine says", "checking the output"],
"interim": ["still shaping the answer", "stitching this together", "almost there"],
"status": ["still on it", "still working through it", "waiting for the result"],
"generic": ["on it", "one sec", "checking that now"],
}
@ -198,15 +193,6 @@ def classify_status_context(
normalized = str(kind or "").strip().lower()
if normalized in {"heartbeat", "waiting", "long_running", "status"}:
return "status"
if normalized in {"thinking", "_thinking", "reasoning"}:
return "thinking"
if normalized in {"interim", "interim_assistant"}:
return "interim"
if normalized in {"command", "terminal"}:
return "command"
if normalized == "tool":
name = (tool_name or "").strip().lower()
return "command" if name in _COMMAND_TOOL_NAMES else "tool"
return "generic"

View file

@ -1773,11 +1773,11 @@ DEFAULT_CONFIG = {
# applies where tool_progress is already enabled. Per-platform override
# via display.platforms.<platform>.tool_progress_grouping.
"tool_progress_grouping": "accumulate",
# Optional custom phrases for display.* generic visibility modes.
# Optional custom phrases for generic long-running status messages.
# Built-in defaults live in gateway/assets/status_phrases.yaml. Users
# can set `path`/`paths` to HERMES_HOME-relative YAML files/directories
# (or rely on conventional status_phrases.yaml / status_phrases/*.yaml).
# Keys: thinking, tool, command, interim, status, generic. Use
# Keys: status, generic. Use
# mode: "append" (default) to add phrases, or "replace" to fully
# replace configured surfaces. Per-platform overrides live under
# display.platforms.<platform>.status_phrases.
@ -5346,7 +5346,7 @@ def migrate_config(interactive: bool = True, quiet: bool = False) -> Dict[str, A
if old_enabled and old_enabled.lower() in {"false", "0", "no"}:
display["tool_progress"] = "off"
results["config_added"].append("display.tool_progress=off (from HERMES_TOOL_PROGRESS=false)")
elif old_mode and old_mode.lower() in {"generic", "new", "all", "verbose"}:
elif old_mode and old_mode.lower() in {"new", "all", "verbose"}:
display["tool_progress"] = old_mode.lower()
results["config_added"].append(f"display.tool_progress={old_mode.lower()} (from HERMES_TOOL_PROGRESS_MODE)")
else:

View file

@ -99,7 +99,7 @@ class TestVerboseAndToolProgress:
def test_tool_progress_mode_is_string(self):
cli = _make_cli()
assert isinstance(cli.tool_progress_mode, str)
assert cli.tool_progress_mode in {"off", "generic", "new", "all", "verbose"}
assert cli.tool_progress_mode in {"off", "new", "all", "verbose"}
class TestFallbackChainInit:

View file

@ -149,13 +149,13 @@ class TestYAMLNormalisation:
config = {"display": {"tool_progress": True}}
assert resolve_display_setting(config, "telegram", "tool_progress") == "all"
def test_tool_progress_generic_is_first_class_mode(self):
def test_tool_progress_generic_is_not_a_mode(self):
from gateway.display_config import resolve_display_setting
config = {"display": {"platforms": {"whatsapp": {"tool_progress": "generic"}}}}
assert resolve_display_setting(config, "whatsapp", "tool_progress") == "generic"
assert resolve_display_setting(config, "whatsapp", "tool_progress") == "all"
def test_chatter_visibility_surfaces_accept_generic_mode(self):
def test_only_long_running_visibility_accepts_generic_mode(self):
from gateway.display_config import resolve_display_setting
config = {
@ -169,8 +169,8 @@ class TestYAMLNormalisation:
}
}
}
assert resolve_display_setting(config, "whatsapp", "thinking_progress") == "generic"
assert resolve_display_setting(config, "whatsapp", "interim_assistant_messages") == "generic"
assert resolve_display_setting(config, "whatsapp", "thinking_progress") is False
assert resolve_display_setting(config, "whatsapp", "interim_assistant_messages") is False
assert resolve_display_setting(config, "whatsapp", "long_running_notifications") == "generic"
def test_thinking_progress_string_false_normalised_to_false(self):

View file

@ -7,72 +7,59 @@ from gateway.status_phrases import (
)
def test_terminal_tool_uses_command_surface_bucket():
assert classify_status_context("tool", tool_name="terminal") == "command"
assert classify_status_context("command") == "command"
def test_long_running_context_uses_status_bucket():
assert classify_status_context("status") == "status"
assert classify_status_context("heartbeat") == "status"
assert classify_status_context("long_running") == "status"
def test_regular_tool_uses_tool_surface_bucket_not_domain_bucket():
assert classify_status_context("tool", tool_name="web_search", preview="weather") == "tool"
assert classify_status_context("tool", tool_name="mcp_atlassian_infohealth_getJiraIssue") == "tool"
def test_non_status_context_falls_back_to_generic_bucket():
assert classify_status_context("tool", tool_name="terminal") == "generic"
assert classify_status_context("thinking") == "generic"
assert classify_status_context("interim_assistant") == "generic"
def test_interim_uses_interim_surface_bucket():
assert classify_status_context("interim_assistant") == "interim"
def test_generic_phrase_does_not_leak_raw_thinking_text():
def test_status_phrase_does_not_leak_raw_preview_or_args():
msg = choose_status_phrase(
"thinking",
"status",
preview="actual private scratch text should not be sent",
args={"secret": "SECRET-123"},
rng=random.Random(4),
)
assert "actual private scratch" not in msg
assert "SECRET-123" not in msg
assert msg
def test_generic_tool_phrase_does_not_leak_vendor_or_args():
msg = choose_status_phrase(
"tool",
tool_name="mcp_atlassian_infohealth_getJiraIssue",
args={"issueIdOrKey": "SECRET-123"},
rng=random.Random(1),
)
assert "jira" not in msg.lower()
assert "confluence" not in msg.lower()
assert "atlassian" not in msg.lower()
assert "SECRET-123" not in msg
def test_generic_phrase_avoids_recent_repetition():
def test_status_phrase_avoids_recent_repetition():
recent: list[str] = []
first = choose_status_phrase("tool", tool_name="web_search", rng=random.Random(2), recent=recent)
second = choose_status_phrase("tool", tool_name="web_search", rng=random.Random(2), recent=recent)
first = choose_status_phrase("status", rng=random.Random(2), recent=recent)
second = choose_status_phrase("status", rng=random.Random(2), recent=recent)
assert first != second
assert recent[-2:] == [first, second]
def test_builtin_catalog_is_loaded_from_external_asset_and_is_not_tiny():
def test_builtin_catalog_is_loaded_from_external_asset_and_is_status_only():
catalog = resolve_status_phrase_catalog({}, "whatsapp")
for surface in ("thinking", "tool", "command", "interim", "status"):
assert len(catalog[surface]) >= 25, surface
assert set(catalog) == {"status", "generic"}
assert len(catalog["status"]) >= 25
assert len(catalog["generic"]) >= 10
def test_relative_status_phrase_path_loads_from_hermes_home(tmp_path, monkeypatch):
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
phrase_file = tmp_path / "phrases.yaml"
phrase_file.write_text("mode: replace\ntool:\n - relative safe tool text\n", encoding="utf-8")
phrase_file.write_text("mode: replace\nstatus:\n - relative safe status text\n", encoding="utf-8")
catalog = resolve_status_phrase_catalog(
{"display": {"status_phrases": {"path": "phrases.yaml"}}},
"whatsapp",
)
assert catalog["tool"] == ["relative safe tool text"]
assert catalog["status"] == ["relative safe status text"]
def test_status_phrase_path_can_load_relative_directory(tmp_path, monkeypatch):
@ -92,7 +79,7 @@ def test_status_phrase_path_can_load_relative_directory(tmp_path, monkeypatch):
def test_absolute_or_parent_phrase_paths_are_ignored(tmp_path, monkeypatch):
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
outside = tmp_path.parent / "outside-phrases.yaml"
outside.write_text("mode: replace\ntool:\n - should not load\n", encoding="utf-8")
outside.write_text("mode: replace\nstatus:\n - should not load\n", encoding="utf-8")
catalog = resolve_status_phrase_catalog(
{"display": {"status_phrases": {"path": str(outside)}}},
@ -103,8 +90,8 @@ def test_absolute_or_parent_phrase_paths_are_ignored(tmp_path, monkeypatch):
"whatsapp",
)
assert catalog["tool"] != ["should not load"]
assert escaped["tool"] != ["should not load"]
assert catalog["status"] != ["should not load"]
assert escaped["status"] != ["should not load"]
def test_conventional_relative_status_phrase_file_is_loaded(tmp_path, monkeypatch):
@ -119,23 +106,23 @@ def test_conventional_relative_status_phrase_file_is_loaded(tmp_path, monkeypatc
assert catalog["status"] == ["conventional status text"]
def test_global_custom_phrase_catalog_appends_to_builtin():
def test_global_custom_status_phrase_catalog_appends_to_builtin():
catalog = resolve_status_phrase_catalog(
{
"display": {
"status_phrases": {
"thinking": ["custom thinking placeholder"],
"status": ["custom long-running placeholder"],
}
}
},
"whatsapp",
)
assert "custom thinking placeholder" in catalog["thinking"]
assert len(catalog["thinking"]) > 1
assert "custom long-running placeholder" in catalog["status"]
assert len(catalog["status"]) > 1
def test_platform_custom_phrase_catalog_can_replace_surface():
def test_platform_custom_status_phrase_catalog_can_replace_surface():
catalog = resolve_status_phrase_catalog(
{
"display": {
@ -143,7 +130,7 @@ def test_platform_custom_phrase_catalog_can_replace_surface():
"whatsapp": {
"status_phrases": {
"mode": "replace",
"tool": ["custom tool placeholder"],
"status": ["custom status placeholder"],
}
}
}
@ -152,22 +139,21 @@ def test_platform_custom_phrase_catalog_can_replace_surface():
"whatsapp",
)
assert catalog["tool"] == ["custom tool placeholder"]
assert len(catalog["thinking"]) > 1
assert catalog["status"] == ["custom status placeholder"]
assert len(catalog["generic"]) > 1
def test_choose_status_phrase_uses_custom_catalog_without_leaking_args():
catalog = resolve_status_phrase_catalog(
{"display": {"status_phrases": {"mode": "replace", "tool": ["custom safe tool text"]}}},
{"display": {"status_phrases": {"mode": "replace", "status": ["custom safe status text"]}}},
"whatsapp",
)
msg = choose_status_phrase(
"tool",
tool_name="web_search",
"status",
args={"query": "SECRET SEARCH"},
catalog=catalog,
)
assert msg == "custom safe tool text"
assert msg == "custom safe status text"
assert "SECRET" not in msg

View file

@ -132,7 +132,7 @@ class TestVerboseCommand:
Telegram's tier-1 preset overrides ``tool_progress`` to ``"off"`` so the
platform stays final-answer-first by default on mobile inboxes. The
first ``/verbose`` invocation therefore cycles ``off generic``.
first ``/verbose`` invocation therefore cycles ``off new``.
"""
hermes_home = tmp_path / "hermes"
hermes_home.mkdir()
@ -147,10 +147,10 @@ class TestVerboseCommand:
runner = _make_runner()
result = await runner._handle_verbose_command(_make_event())
# Telegram platform default is "off" → cycles to "generic"
assert "GENERIC" in result
# Telegram platform default is "off" → cycles to "new"
assert "NEW" in result
saved = yaml.safe_load(config_path.read_text(encoding="utf-8"))
assert saved["display"]["platforms"]["telegram"]["tool_progress"] == "generic"
assert saved["display"]["platforms"]["telegram"]["tool_progress"] == "new"
@pytest.mark.asyncio
async def test_per_platform_isolation(self, tmp_path, monkeypatch):
@ -158,7 +158,7 @@ class TestVerboseCommand:
Without a global tool_progress, each platform uses its built-in
default Telegram = 'off' (tier-1 inbox override), Slack = 'off'
(quiet Slack default). Both cycle to 'generic' on first /verbose.
(quiet Slack default). Both cycle to 'new' on first /verbose.
"""
hermes_home = tmp_path / "hermes"
hermes_home.mkdir()
@ -183,10 +183,10 @@ class TestVerboseCommand:
saved = yaml.safe_load(config_path.read_text(encoding="utf-8"))
platforms = saved["display"]["platforms"]
# Telegram: off -> generic (platform default = off, tier-1 inbox override)
assert platforms["telegram"]["tool_progress"] == "generic"
# Slack: off -> generic (first /verbose cycle from quiet default)
assert platforms["slack"]["tool_progress"] == "generic"
# Telegram: off -> new (platform default = off, tier-1 inbox override)
assert platforms["telegram"]["tool_progress"] == "new"
# Slack: off -> new (first /verbose cycle from quiet default)
assert platforms["slack"]["tool_progress"] == "new"
@pytest.mark.asyncio
async def test_no_config_file_returns_disabled(self, tmp_path, monkeypatch):

View file

@ -2385,7 +2385,7 @@ def _load_memory_notifications() -> str:
def _load_tool_progress_mode() -> str:
env = os.environ.get("HERMES_TUI_TOOL_PROGRESS", "").strip().lower()
if env in {"off", "generic", "new", "all", "verbose"}:
if env in {"off", "new", "all", "verbose"}:
return env
raw = (_load_cfg().get("display") or {}).get("tool_progress", "all")
if raw is False:
@ -2393,7 +2393,7 @@ def _load_tool_progress_mode() -> str:
if raw is True:
return "all"
mode = str(raw or "all").strip().lower()
return mode if mode in {"off", "generic", "new", "all", "verbose"} else "all"
return mode if mode in {"off", "new", "all", "verbose"} else "all"
def _load_enabled_toolsets() -> list[str] | None:
@ -9930,7 +9930,7 @@ def _(rid, params: dict) -> dict:
return _ok(rid, {"key": key, "value": raw})
if key == "verbose":
cycle = ["off", "generic", "new", "all", "verbose"]
cycle = ["off", "new", "all", "verbose"]
cur = (
session.get("tool_progress_mode", _load_tool_progress_mode())
if session