feat: config-gated /verbose command for messaging gateway (#3262)
* feat: config-gated /verbose command for messaging gateway Add gateway_config_gate field to CommandDef, allowing cli_only commands to be conditionally available in the gateway based on a config value. - CommandDef gains gateway_config_gate: str | None — a config dotpath that, when truthy, overrides cli_only for gateway surfaces - /verbose uses gateway_config_gate='display.tool_progress_command' - Default is off (cli_only behavior preserved) - When enabled, /verbose cycles tool_progress mode (off/new/all/verbose) in the gateway, saving to config.yaml — same cycle as the CLI - Gateway helpers (help, telegram menus, slack mapping) dynamically check config to include/exclude config-gated commands - GATEWAY_KNOWN_COMMANDS always includes config-gated commands so the gateway recognizes them and can respond appropriately - Handles YAML 1.1 bool coercion (bare 'off' parses as False) - 8 new tests for the config gate mechanism + gateway handler * docs: document gateway_config_gate and /verbose messaging support - AGENTS.md: add gateway_config_gate to CommandDef fields - slash-commands.md: note /verbose can be enabled for messaging, update Notes - configuration.md: add tool_progress_command to display section + usage note - cli.md: cross-link to config docs for messaging enablement - messaging/index.md: show tool_progress_command in config snippet - plugins.md: add gateway_config_gate to register_command parameter table
This commit is contained in:
@@ -173,6 +173,7 @@ if canonical == "mycommand":
|
||||
- `args_hint` — argument placeholder shown in help (e.g. `"<prompt>"`, `"[name]"`)
|
||||
- `cli_only` — only available in the interactive CLI
|
||||
- `gateway_only` — only available in messaging platforms
|
||||
- `gateway_config_gate` — config dotpath (e.g. `"display.tool_progress_command"`); when set on a `cli_only` command, the command becomes available in the gateway if the config value is truthy. `GATEWAY_KNOWN_COMMANDS` always includes config-gated commands so the gateway can dispatch them; help/menus only show them when the gate is open.
|
||||
|
||||
**Adding an alias** requires only adding it to the `aliases` tuple on the existing `CommandDef`. No other file changes needed — dispatch, help text, Telegram menu, Slack mapping, and autocomplete all update automatically.
|
||||
|
||||
|
||||
@@ -1716,6 +1716,9 @@ class GatewayRunner:
|
||||
if canonical == "reasoning":
|
||||
return await self._handle_reasoning_command(event)
|
||||
|
||||
if canonical == "verbose":
|
||||
return await self._handle_verbose_command(event)
|
||||
|
||||
if canonical == "provider":
|
||||
return await self._handle_provider_command(event)
|
||||
|
||||
@@ -3784,6 +3787,68 @@ class GatewayRunner:
|
||||
else:
|
||||
return f"🧠 ✓ Reasoning effort set to `{effort}` (this session only)"
|
||||
|
||||
async def _handle_verbose_command(self, event: MessageEvent) -> str:
|
||||
"""Handle /verbose command — cycle tool progress display mode.
|
||||
|
||||
Gated by ``display.tool_progress_command`` in config.yaml (default off).
|
||||
When enabled, cycles the tool progress mode through off → new → all →
|
||||
verbose → off, same as the CLI.
|
||||
"""
|
||||
import yaml
|
||||
|
||||
config_path = _hermes_home / "config.yaml"
|
||||
|
||||
# --- check config gate ------------------------------------------------
|
||||
try:
|
||||
user_config = {}
|
||||
if config_path.exists():
|
||||
with open(config_path, encoding="utf-8") as f:
|
||||
user_config = yaml.safe_load(f) or {}
|
||||
gate_enabled = user_config.get("display", {}).get("tool_progress_command", False)
|
||||
except Exception:
|
||||
gate_enabled = False
|
||||
|
||||
if not gate_enabled:
|
||||
return (
|
||||
"The `/verbose` command is not enabled for messaging platforms.\n\n"
|
||||
"Enable it in `config.yaml`:\n```yaml\n"
|
||||
"display:\n tool_progress_command: true\n```"
|
||||
)
|
||||
|
||||
# --- cycle mode -------------------------------------------------------
|
||||
cycle = ["off", "new", "all", "verbose"]
|
||||
descriptions = {
|
||||
"off": "⚙️ Tool progress: **OFF** — no tool activity shown.",
|
||||
"new": "⚙️ Tool progress: **NEW** — shown when tool changes.",
|
||||
"all": "⚙️ Tool progress: **ALL** — every tool call shown.",
|
||||
"verbose": "⚙️ Tool progress: **VERBOSE** — full args and results.",
|
||||
}
|
||||
|
||||
raw_progress = user_config.get("display", {}).get("tool_progress", "all")
|
||||
# YAML 1.1 parses bare "off" as boolean False — normalise back
|
||||
if raw_progress is False:
|
||||
current = "off"
|
||||
elif raw_progress is True:
|
||||
current = "all"
|
||||
else:
|
||||
current = str(raw_progress).lower()
|
||||
if current not in cycle:
|
||||
current = "all"
|
||||
idx = (cycle.index(current) + 1) % len(cycle)
|
||||
new_mode = cycle[idx]
|
||||
|
||||
# Save to config.yaml
|
||||
try:
|
||||
if "display" not in user_config or not isinstance(user_config.get("display"), dict):
|
||||
user_config["display"] = {}
|
||||
user_config["display"]["tool_progress"] = new_mode
|
||||
with open(config_path, "w", encoding="utf-8") as f:
|
||||
yaml.dump(user_config, f, default_flow_style=False, sort_keys=False)
|
||||
return f"{descriptions[new_mode]}\n_(saved to config — takes effect on next message)_"
|
||||
except Exception as e:
|
||||
logger.warning("Failed to save tool_progress mode: %s", e)
|
||||
return f"{descriptions[new_mode]}\n_(could not save to config: {e})_"
|
||||
|
||||
async def _handle_compress_command(self, event: MessageEvent) -> str:
|
||||
"""Handle /compress command -- manually compress conversation context."""
|
||||
source = event.source
|
||||
|
||||
@@ -36,6 +36,7 @@ class CommandDef:
|
||||
subcommands: tuple[str, ...] = () # tab-completable subcommands
|
||||
cli_only: bool = False # only available in CLI
|
||||
gateway_only: bool = False # only available in gateway/messaging
|
||||
gateway_config_gate: str | None = None # config dotpath; when truthy, overrides cli_only for gateway
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -87,7 +88,8 @@ COMMAND_REGISTRY: list[CommandDef] = [
|
||||
CommandDef("statusbar", "Toggle the context/model status bar", "Configuration",
|
||||
cli_only=True, aliases=("sb",)),
|
||||
CommandDef("verbose", "Cycle tool progress display: off -> new -> all -> verbose",
|
||||
"Configuration", cli_only=True),
|
||||
"Configuration", cli_only=True,
|
||||
gateway_config_gate="display.tool_progress_command"),
|
||||
CommandDef("reasoning", "Manage reasoning effort and display", "Configuration",
|
||||
args_hint="[level|show|hide]",
|
||||
subcommands=("none", "low", "minimal", "medium", "high", "xhigh", "show", "hide", "on", "off")),
|
||||
@@ -205,7 +207,7 @@ def rebuild_lookups() -> None:
|
||||
GATEWAY_KNOWN_COMMANDS = frozenset(
|
||||
name
|
||||
for cmd in COMMAND_REGISTRY
|
||||
if not cmd.cli_only
|
||||
if not cmd.cli_only or cmd.gateway_config_gate
|
||||
for name in (cmd.name, *cmd.aliases)
|
||||
)
|
||||
|
||||
@@ -259,20 +261,76 @@ for _cmd in COMMAND_REGISTRY:
|
||||
# Gateway helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Set of all command names + aliases recognized by the gateway
|
||||
# Set of all command names + aliases recognized by the gateway.
|
||||
# Includes config-gated commands so the gateway can dispatch them
|
||||
# (the handler checks the config gate at runtime).
|
||||
GATEWAY_KNOWN_COMMANDS: frozenset[str] = frozenset(
|
||||
name
|
||||
for cmd in COMMAND_REGISTRY
|
||||
if not cmd.cli_only
|
||||
if not cmd.cli_only or cmd.gateway_config_gate
|
||||
for name in (cmd.name, *cmd.aliases)
|
||||
)
|
||||
|
||||
|
||||
def _resolve_config_gates() -> set[str]:
|
||||
"""Return canonical names of commands whose ``gateway_config_gate`` is truthy.
|
||||
|
||||
Reads ``config.yaml`` and walks the dot-separated key path for each
|
||||
config-gated command. Returns an empty set on any error so callers
|
||||
degrade gracefully.
|
||||
"""
|
||||
gated = [c for c in COMMAND_REGISTRY if c.gateway_config_gate]
|
||||
if not gated:
|
||||
return set()
|
||||
try:
|
||||
import yaml
|
||||
config_path = os.path.join(
|
||||
os.getenv("HERMES_HOME", os.path.expanduser("~/.hermes")),
|
||||
"config.yaml",
|
||||
)
|
||||
if os.path.exists(config_path):
|
||||
with open(config_path, encoding="utf-8") as f:
|
||||
cfg = yaml.safe_load(f) or {}
|
||||
else:
|
||||
cfg = {}
|
||||
except Exception:
|
||||
return set()
|
||||
result: set[str] = set()
|
||||
for cmd in gated:
|
||||
val: Any = cfg
|
||||
for key in cmd.gateway_config_gate.split("."):
|
||||
if isinstance(val, dict):
|
||||
val = val.get(key)
|
||||
else:
|
||||
val = None
|
||||
break
|
||||
if val:
|
||||
result.add(cmd.name)
|
||||
return result
|
||||
|
||||
|
||||
def _is_gateway_available(cmd: CommandDef, config_overrides: set[str] | None = None) -> bool:
|
||||
"""Check if *cmd* should appear in gateway surfaces (help, menus, mappings).
|
||||
|
||||
Unconditionally available when ``cli_only`` is False. When ``cli_only``
|
||||
is True but ``gateway_config_gate`` is set, the command is available only
|
||||
when the config value is truthy. Pass *config_overrides* (from
|
||||
``_resolve_config_gates()``) to avoid re-reading config for every command.
|
||||
"""
|
||||
if not cmd.cli_only:
|
||||
return True
|
||||
if cmd.gateway_config_gate:
|
||||
overrides = config_overrides if config_overrides is not None else _resolve_config_gates()
|
||||
return cmd.name in overrides
|
||||
return False
|
||||
|
||||
|
||||
def gateway_help_lines() -> list[str]:
|
||||
"""Generate gateway help text lines from the registry."""
|
||||
overrides = _resolve_config_gates()
|
||||
lines: list[str] = []
|
||||
for cmd in COMMAND_REGISTRY:
|
||||
if cmd.cli_only:
|
||||
if not _is_gateway_available(cmd, overrides):
|
||||
continue
|
||||
args = f" {cmd.args_hint}" if cmd.args_hint else ""
|
||||
alias_parts: list[str] = []
|
||||
@@ -293,9 +351,10 @@ def telegram_bot_commands() -> list[tuple[str, str]]:
|
||||
underscores. Aliases are skipped -- Telegram shows one menu entry per
|
||||
canonical command.
|
||||
"""
|
||||
overrides = _resolve_config_gates()
|
||||
result: list[tuple[str, str]] = []
|
||||
for cmd in COMMAND_REGISTRY:
|
||||
if cmd.cli_only:
|
||||
if not _is_gateway_available(cmd, overrides):
|
||||
continue
|
||||
tg_name = cmd.name.replace("-", "_")
|
||||
result.append((tg_name, cmd.description))
|
||||
@@ -308,9 +367,10 @@ def slack_subcommand_map() -> dict[str, str]:
|
||||
Maps both canonical names and aliases so /hermes bg do stuff works
|
||||
the same as /hermes background do stuff.
|
||||
"""
|
||||
overrides = _resolve_config_gates()
|
||||
mapping: dict[str, str] = {}
|
||||
for cmd in COMMAND_REGISTRY:
|
||||
if cmd.cli_only:
|
||||
if not _is_gateway_available(cmd, overrides):
|
||||
continue
|
||||
mapping[cmd.name] = f"/{cmd.name}"
|
||||
for alias in cmd.aliases:
|
||||
|
||||
@@ -269,6 +269,7 @@ DEFAULT_CONFIG = {
|
||||
"streaming": False,
|
||||
"show_cost": False, # Show $ cost in the status bar (off by default)
|
||||
"skin": "default",
|
||||
"tool_progress_command": False, # Enable /verbose command in messaging gateway
|
||||
},
|
||||
|
||||
# Privacy settings
|
||||
|
||||
146
tests/gateway/test_verbose_command.py
Normal file
146
tests/gateway/test_verbose_command.py
Normal file
@@ -0,0 +1,146 @@
|
||||
"""Tests for gateway /verbose command (config-gated tool progress cycling)."""
|
||||
|
||||
import asyncio
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
|
||||
import gateway.run as gateway_run
|
||||
from gateway.config import Platform
|
||||
from gateway.platforms.base import MessageEvent
|
||||
from gateway.session import SessionSource
|
||||
|
||||
|
||||
def _make_event(text="/verbose", platform=Platform.TELEGRAM, user_id="12345", chat_id="67890"):
|
||||
"""Build a MessageEvent for testing."""
|
||||
source = SessionSource(
|
||||
platform=platform,
|
||||
user_id=user_id,
|
||||
chat_id=chat_id,
|
||||
user_name="testuser",
|
||||
)
|
||||
return MessageEvent(text=text, source=source)
|
||||
|
||||
|
||||
def _make_runner():
|
||||
"""Create a bare GatewayRunner without calling __init__."""
|
||||
runner = object.__new__(gateway_run.GatewayRunner)
|
||||
runner.adapters = {}
|
||||
runner._ephemeral_system_prompt = ""
|
||||
runner._prefill_messages = []
|
||||
runner._reasoning_config = None
|
||||
runner._show_reasoning = False
|
||||
runner._provider_routing = {}
|
||||
runner._fallback_model = None
|
||||
runner._running_agents = {}
|
||||
runner.hooks = MagicMock()
|
||||
runner.hooks.emit = AsyncMock()
|
||||
runner.hooks.loaded_hooks = []
|
||||
runner._session_db = None
|
||||
runner._get_or_create_gateway_honcho = lambda session_key: (None, None)
|
||||
return runner
|
||||
|
||||
|
||||
class TestVerboseCommand:
|
||||
"""Tests for _handle_verbose_command in the gateway."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_disabled_by_default(self, tmp_path, monkeypatch):
|
||||
"""When tool_progress_command is false, /verbose returns an info message."""
|
||||
hermes_home = tmp_path / "hermes"
|
||||
hermes_home.mkdir()
|
||||
config_path = hermes_home / "config.yaml"
|
||||
config_path.write_text("display:\n tool_progress: all\n", encoding="utf-8")
|
||||
|
||||
monkeypatch.setattr(gateway_run, "_hermes_home", hermes_home)
|
||||
|
||||
runner = _make_runner()
|
||||
result = await runner._handle_verbose_command(_make_event())
|
||||
|
||||
assert "not enabled" in result.lower()
|
||||
assert "tool_progress_command" in result
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_enabled_cycles_mode(self, tmp_path, monkeypatch):
|
||||
"""When enabled, /verbose cycles tool_progress mode."""
|
||||
hermes_home = tmp_path / "hermes"
|
||||
hermes_home.mkdir()
|
||||
config_path = hermes_home / "config.yaml"
|
||||
config_path.write_text(
|
||||
"display:\n tool_progress_command: true\n tool_progress: all\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
monkeypatch.setattr(gateway_run, "_hermes_home", hermes_home)
|
||||
|
||||
runner = _make_runner()
|
||||
result = await runner._handle_verbose_command(_make_event())
|
||||
|
||||
# all -> verbose
|
||||
assert "VERBOSE" in result
|
||||
|
||||
# Verify config was saved
|
||||
saved = yaml.safe_load(config_path.read_text(encoding="utf-8"))
|
||||
assert saved["display"]["tool_progress"] == "verbose"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_cycles_through_all_modes(self, tmp_path, monkeypatch):
|
||||
"""Calling /verbose repeatedly cycles through all four modes."""
|
||||
hermes_home = tmp_path / "hermes"
|
||||
hermes_home.mkdir()
|
||||
config_path = hermes_home / "config.yaml"
|
||||
config_path.write_text(
|
||||
"display:\n tool_progress_command: true\n tool_progress: 'off'\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
monkeypatch.setattr(gateway_run, "_hermes_home", hermes_home)
|
||||
runner = _make_runner()
|
||||
|
||||
# off -> new -> all -> verbose -> off
|
||||
expected = ["new", "all", "verbose", "off"]
|
||||
for mode in expected:
|
||||
result = await runner._handle_verbose_command(_make_event())
|
||||
saved = yaml.safe_load(config_path.read_text(encoding="utf-8"))
|
||||
assert saved["display"]["tool_progress"] == mode, \
|
||||
f"Expected {mode}, got {saved['display']['tool_progress']}"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_defaults_to_all_when_no_tool_progress_set(self, tmp_path, monkeypatch):
|
||||
"""When tool_progress is not in config, defaults to 'all' then cycles to verbose."""
|
||||
hermes_home = tmp_path / "hermes"
|
||||
hermes_home.mkdir()
|
||||
config_path = hermes_home / "config.yaml"
|
||||
config_path.write_text(
|
||||
"display:\n tool_progress_command: true\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
monkeypatch.setattr(gateway_run, "_hermes_home", hermes_home)
|
||||
|
||||
runner = _make_runner()
|
||||
result = await runner._handle_verbose_command(_make_event())
|
||||
|
||||
# default "all" -> verbose
|
||||
assert "VERBOSE" in result
|
||||
saved = yaml.safe_load(config_path.read_text(encoding="utf-8"))
|
||||
assert saved["display"]["tool_progress"] == "verbose"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_no_config_file_returns_disabled(self, tmp_path, monkeypatch):
|
||||
"""When config.yaml doesn't exist, command reports disabled."""
|
||||
hermes_home = tmp_path / "hermes"
|
||||
hermes_home.mkdir()
|
||||
# No config.yaml
|
||||
|
||||
monkeypatch.setattr(gateway_run, "_hermes_home", hermes_home)
|
||||
|
||||
runner = _make_runner()
|
||||
result = await runner._handle_verbose_command(_make_event())
|
||||
assert "not enabled" in result.lower()
|
||||
|
||||
def test_verbose_is_in_gateway_known_commands(self):
|
||||
"""The /verbose command is recognized by the gateway dispatch."""
|
||||
from hermes_cli.commands import GATEWAY_KNOWN_COMMANDS
|
||||
assert "verbose" in GATEWAY_KNOWN_COMMANDS
|
||||
@@ -134,12 +134,19 @@ class TestDerivedDicts:
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestGatewayKnownCommands:
|
||||
def test_excludes_cli_only(self):
|
||||
def test_excludes_cli_only_without_config_gate(self):
|
||||
for cmd in COMMAND_REGISTRY:
|
||||
if cmd.cli_only:
|
||||
if cmd.cli_only and not cmd.gateway_config_gate:
|
||||
assert cmd.name not in GATEWAY_KNOWN_COMMANDS, \
|
||||
f"cli_only command '{cmd.name}' should not be in GATEWAY_KNOWN_COMMANDS"
|
||||
|
||||
def test_includes_config_gated_cli_only(self):
|
||||
"""Commands with gateway_config_gate are always in GATEWAY_KNOWN_COMMANDS."""
|
||||
for cmd in COMMAND_REGISTRY:
|
||||
if cmd.gateway_config_gate:
|
||||
assert cmd.name in GATEWAY_KNOWN_COMMANDS, \
|
||||
f"config-gated command '{cmd.name}' should be in GATEWAY_KNOWN_COMMANDS"
|
||||
|
||||
def test_includes_gateway_commands(self):
|
||||
for cmd in COMMAND_REGISTRY:
|
||||
if not cmd.cli_only:
|
||||
@@ -160,11 +167,11 @@ class TestGatewayHelpLines:
|
||||
lines = gateway_help_lines()
|
||||
assert len(lines) > 10
|
||||
|
||||
def test_excludes_cli_only_commands(self):
|
||||
def test_excludes_cli_only_commands_without_config_gate(self):
|
||||
lines = gateway_help_lines()
|
||||
joined = "\n".join(lines)
|
||||
for cmd in COMMAND_REGISTRY:
|
||||
if cmd.cli_only:
|
||||
if cmd.cli_only and not cmd.gateway_config_gate:
|
||||
assert f"`/{cmd.name}" not in joined, \
|
||||
f"cli_only command /{cmd.name} should not be in gateway help"
|
||||
|
||||
@@ -188,10 +195,10 @@ class TestTelegramBotCommands:
|
||||
for name, _ in telegram_bot_commands():
|
||||
assert "-" not in name, f"Telegram command '{name}' contains a hyphen"
|
||||
|
||||
def test_excludes_cli_only(self):
|
||||
def test_excludes_cli_only_without_config_gate(self):
|
||||
names = {name for name, _ in telegram_bot_commands()}
|
||||
for cmd in COMMAND_REGISTRY:
|
||||
if cmd.cli_only:
|
||||
if cmd.cli_only and not cmd.gateway_config_gate:
|
||||
tg_name = cmd.name.replace("-", "_")
|
||||
assert tg_name not in names
|
||||
|
||||
@@ -211,13 +218,84 @@ class TestSlackSubcommandMap:
|
||||
assert "bg" in mapping
|
||||
assert "reset" in mapping
|
||||
|
||||
def test_excludes_cli_only(self):
|
||||
def test_excludes_cli_only_without_config_gate(self):
|
||||
mapping = slack_subcommand_map()
|
||||
for cmd in COMMAND_REGISTRY:
|
||||
if cmd.cli_only:
|
||||
if cmd.cli_only and not cmd.gateway_config_gate:
|
||||
assert cmd.name not in mapping
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Config-gated gateway commands
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestGatewayConfigGate:
|
||||
"""Tests for the gateway_config_gate mechanism on CommandDef."""
|
||||
|
||||
def test_verbose_has_config_gate(self):
|
||||
cmd = resolve_command("verbose")
|
||||
assert cmd is not None
|
||||
assert cmd.cli_only is True
|
||||
assert cmd.gateway_config_gate == "display.tool_progress_command"
|
||||
|
||||
def test_verbose_in_gateway_known_commands(self):
|
||||
"""Config-gated commands are always recognized by the gateway."""
|
||||
assert "verbose" in GATEWAY_KNOWN_COMMANDS
|
||||
|
||||
def test_config_gate_excluded_from_help_when_off(self, tmp_path, monkeypatch):
|
||||
"""When the config gate is falsy, the command should not appear in help."""
|
||||
# Write a config with the gate off (default)
|
||||
config_file = tmp_path / "config.yaml"
|
||||
config_file.write_text("display:\n tool_progress_command: false\n")
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
|
||||
lines = gateway_help_lines()
|
||||
joined = "\n".join(lines)
|
||||
assert "`/verbose" not in joined
|
||||
|
||||
def test_config_gate_included_in_help_when_on(self, tmp_path, monkeypatch):
|
||||
"""When the config gate is truthy, the command should appear in help."""
|
||||
config_file = tmp_path / "config.yaml"
|
||||
config_file.write_text("display:\n tool_progress_command: true\n")
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
|
||||
lines = gateway_help_lines()
|
||||
joined = "\n".join(lines)
|
||||
assert "`/verbose" in joined
|
||||
|
||||
def test_config_gate_excluded_from_telegram_when_off(self, tmp_path, monkeypatch):
|
||||
config_file = tmp_path / "config.yaml"
|
||||
config_file.write_text("display:\n tool_progress_command: false\n")
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
|
||||
names = {name for name, _ in telegram_bot_commands()}
|
||||
assert "verbose" not in names
|
||||
|
||||
def test_config_gate_included_in_telegram_when_on(self, tmp_path, monkeypatch):
|
||||
config_file = tmp_path / "config.yaml"
|
||||
config_file.write_text("display:\n tool_progress_command: true\n")
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
|
||||
names = {name for name, _ in telegram_bot_commands()}
|
||||
assert "verbose" in names
|
||||
|
||||
def test_config_gate_excluded_from_slack_when_off(self, tmp_path, monkeypatch):
|
||||
config_file = tmp_path / "config.yaml"
|
||||
config_file.write_text("display:\n tool_progress_command: false\n")
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
|
||||
mapping = slack_subcommand_map()
|
||||
assert "verbose" not in mapping
|
||||
|
||||
def test_config_gate_included_in_slack_when_on(self, tmp_path, monkeypatch):
|
||||
config_file = tmp_path / "config.yaml"
|
||||
config_file.write_text("display:\n tool_progress_command: true\n")
|
||||
monkeypatch.setenv("HERMES_HOME", str(tmp_path))
|
||||
|
||||
mapping = slack_subcommand_map()
|
||||
assert "verbose" in mapping
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Autocomplete (SlashCommandCompleter)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -46,7 +46,7 @@ Type `/` in the CLI to open the autocomplete menu. Built-in commands are case-in
|
||||
| `/provider` | Show available providers and current provider |
|
||||
| `/prompt` | View/set custom system prompt |
|
||||
| `/personality` | Set a predefined personality |
|
||||
| `/verbose` | Cycle tool progress display: off → new → all → verbose |
|
||||
| `/verbose` | Cycle tool progress display: off → new → all → verbose. Can be [enabled for messaging](#notes) via config. |
|
||||
| `/reasoning` | Manage reasoning effort and display (usage: /reasoning [level\|show\|hide]) |
|
||||
| `/skin` | Show or change the display skin/theme |
|
||||
| `/voice [on\|off\|tts\|status]` | Toggle CLI voice mode and spoken playback. Recording uses `voice.record_key` (default: `Ctrl+B`). |
|
||||
@@ -125,7 +125,8 @@ The messaging gateway supports the following built-in commands inside Telegram,
|
||||
|
||||
## Notes
|
||||
|
||||
- `/skin`, `/tools`, `/toolsets`, `/browser`, `/config`, `/prompt`, `/cron`, `/skills`, `/platforms`, `/paste`, `/verbose`, `/statusbar`, and `/plugins` are **CLI-only** commands.
|
||||
- `/skin`, `/tools`, `/toolsets`, `/browser`, `/config`, `/prompt`, `/cron`, `/skills`, `/platforms`, `/paste`, `/statusbar`, and `/plugins` are **CLI-only** commands.
|
||||
- `/verbose` is **CLI-only by default**, but can be enabled for messaging platforms by setting `display.tool_progress_command: true` in `config.yaml`. When enabled, it cycles the `display.tool_progress` mode and saves to config.
|
||||
- `/status`, `/sethome`, `/update`, `/approve`, and `/deny` are **messaging-only** commands.
|
||||
- `/background`, `/voice`, `/reload-mcp`, and `/rollback` work in **both** the CLI and the messaging gateway.
|
||||
- `/voice join`, `/voice channel`, and `/voice leave` are only meaningful on Discord.
|
||||
|
||||
@@ -230,7 +230,7 @@ The CLI shows animated feedback as the agent works:
|
||||
┊ 📄 web_extract (2.1s)
|
||||
```
|
||||
|
||||
Cycle through display modes with `/verbose`: `off → new → all → verbose`.
|
||||
Cycle through display modes with `/verbose`: `off → new → all → verbose`. This command can also be enabled for messaging platforms — see [configuration](/docs/user-guide/configuration#display-settings).
|
||||
|
||||
## Session Management
|
||||
|
||||
|
||||
@@ -1163,6 +1163,7 @@ This controls both the `text_to_speech` tool and spoken replies in voice mode (`
|
||||
```yaml
|
||||
display:
|
||||
tool_progress: all # off | new | all | verbose
|
||||
tool_progress_command: false # Enable /verbose slash command in messaging gateway
|
||||
skin: default # Built-in or custom CLI skin (see user-guide/features/skins)
|
||||
theme_mode: auto # auto | light | dark — color scheme for skin-aware rendering
|
||||
personality: "kawaii" # Legacy cosmetic field still surfaced in some summaries
|
||||
@@ -1194,6 +1195,8 @@ This works with any skin — built-in or custom. Skin authors can provide `color
|
||||
| `all` | Every tool call with a short preview (default) |
|
||||
| `verbose` | Full args, results, and debug logs |
|
||||
|
||||
In the CLI, cycle through these modes with `/verbose`. To use `/verbose` in messaging platforms (Telegram, Discord, Slack, etc.), set `tool_progress_command: true` in the `display` section above. The command will then cycle the mode and save to config.
|
||||
|
||||
## Privacy
|
||||
|
||||
```yaml
|
||||
|
||||
@@ -83,6 +83,7 @@ The handler receives the argument string (everything after `/greet`) and returns
|
||||
| `aliases` | Tuple of alternative names |
|
||||
| `cli_only` | Only available in CLI |
|
||||
| `gateway_only` | Only available in messaging platforms |
|
||||
| `gateway_config_gate` | Config dotpath (e.g. `"display.my_option"`). When set on a `cli_only` command, the command becomes available in the gateway if the config value is truthy. |
|
||||
|
||||
## Managing plugins
|
||||
|
||||
|
||||
@@ -188,6 +188,7 @@ Control how much tool activity is displayed in `~/.hermes/config.yaml`:
|
||||
```yaml
|
||||
display:
|
||||
tool_progress: all # off | new | all | verbose
|
||||
tool_progress_command: false # set to true to enable /verbose in messaging
|
||||
```
|
||||
|
||||
When enabled, the bot sends status messages as it works:
|
||||
|
||||
Reference in New Issue
Block a user