diff --git a/cli.py b/cli.py index ea43cdf5a..02c6bed63 100644 --- a/cli.py +++ b/cli.py @@ -6165,6 +6165,8 @@ class HermesCLI: self._handle_skin_command(cmd_original) elif canonical == "voice": self._handle_voice_command(cmd_original) + elif canonical == "busy": + self._handle_busy_command(cmd_original) else: # Check for user-defined quick commands (bypass agent loop, no LLM call) base_cmd = cmd_lower.split()[0] @@ -6901,6 +6903,36 @@ class HermesCLI: else: _cprint(f" {_ACCENT}✓ Reasoning effort set to '{arg}' (session only){_RST}") + def _handle_busy_command(self, cmd: str): + """Handle /busy — control what Enter does while Hermes is working. + + Usage: + /busy Show current busy input mode + /busy status Show current busy input mode + /busy queue Queue input for the next turn instead of interrupting + /busy interrupt Interrupt the current run on Enter (default) + """ + parts = cmd.strip().split(maxsplit=1) + if len(parts) < 2 or parts[1].strip().lower() == "status": + _cprint(f" {_ACCENT}Busy input mode: {self.busy_input_mode}{_RST}") + _cprint(f" {_DIM}Enter while busy: {'queues for next turn' if self.busy_input_mode == 'queue' else 'interrupts current run'}{_RST}") + _cprint(f" {_DIM}Usage: /busy [queue|interrupt|status]{_RST}") + return + + arg = parts[1].strip().lower() + if arg not in {"queue", "interrupt"}: + _cprint(f" {_DIM}(._.) Unknown argument: {arg}{_RST}") + _cprint(f" {_DIM}Usage: /busy [queue|interrupt|status]{_RST}") + return + + self.busy_input_mode = arg + if save_config_value("display.busy_input_mode", arg): + behavior = "Enter will queue follow-up input while Hermes is busy." if arg == "queue" else "Enter will interrupt the current run while Hermes is busy." + _cprint(f" {_ACCENT}✓ Busy input mode set to '{arg}' (saved to config){_RST}") + _cprint(f" {_DIM}{behavior}{_RST}") + else: + _cprint(f" {_ACCENT}✓ Busy input mode set to '{arg}' (session only){_RST}") + def _handle_fast_command(self, cmd: str): """Handle /fast — toggle fast mode (OpenAI Priority Processing / Anthropic Fast Mode).""" if not self._fast_command_available(): diff --git a/hermes_cli/commands.py b/hermes_cli/commands.py index e5e2a6c63..efff57180 100644 --- a/hermes_cli/commands.py +++ b/hermes_cli/commands.py @@ -126,6 +126,9 @@ COMMAND_REGISTRY: list[CommandDef] = [ cli_only=True, args_hint="[name]"), CommandDef("voice", "Toggle voice mode", "Configuration", args_hint="[on|off|tts|status]", subcommands=("on", "off", "tts", "status")), + CommandDef("busy", "Control what Enter does while Hermes is working", "Configuration", + cli_only=True, args_hint="[queue|interrupt|status]", + subcommands=("queue", "interrupt", "status")), # Tools & Skills CommandDef("tools", "Manage tools: /tools [list|disable|enable] [name...]", "Tools & Skills", diff --git a/tests/cli/test_busy_input_mode_command.py b/tests/cli/test_busy_input_mode_command.py new file mode 100644 index 000000000..6dd0afbc7 --- /dev/null +++ b/tests/cli/test_busy_input_mode_command.py @@ -0,0 +1,94 @@ +"""Tests for the /busy CLI command and busy-input-mode config handling.""" + +import unittest +from types import SimpleNamespace +from unittest.mock import patch + + +def _import_cli(): + import hermes_cli.config as config_mod + + if not hasattr(config_mod, "save_env_value_secure"): + config_mod.save_env_value_secure = lambda key, value: { + "success": True, + "stored_as": key, + "validated": False, + } + + import cli as cli_mod + + return cli_mod + + +class TestHandleBusyCommand(unittest.TestCase): + def _make_cli(self, busy_input_mode="interrupt"): + return SimpleNamespace( + busy_input_mode=busy_input_mode, + agent=None, + ) + + def test_no_args_shows_status(self): + cli_mod = _import_cli() + stub = self._make_cli("queue") + with ( + patch.object(cli_mod, "_cprint") as mock_cprint, + patch.object(cli_mod, "save_config_value") as mock_save, + ): + cli_mod.HermesCLI._handle_busy_command(stub, "/busy") + + mock_save.assert_not_called() + printed = " ".join(str(c) for c in mock_cprint.call_args_list) + self.assertIn("queue", printed) + self.assertIn("interrupt", printed) + + def test_queue_argument_sets_queue_mode_and_saves(self): + cli_mod = _import_cli() + stub = self._make_cli("interrupt") + with ( + patch.object(cli_mod, "_cprint"), + patch.object(cli_mod, "save_config_value", return_value=True) as mock_save, + ): + cli_mod.HermesCLI._handle_busy_command(stub, "/busy queue") + + self.assertEqual(stub.busy_input_mode, "queue") + mock_save.assert_called_once_with("display.busy_input_mode", "queue") + + def test_interrupt_argument_sets_interrupt_mode_and_saves(self): + cli_mod = _import_cli() + stub = self._make_cli("queue") + with ( + patch.object(cli_mod, "_cprint"), + patch.object(cli_mod, "save_config_value", return_value=True) as mock_save, + ): + cli_mod.HermesCLI._handle_busy_command(stub, "/busy interrupt") + + self.assertEqual(stub.busy_input_mode, "interrupt") + mock_save.assert_called_once_with("display.busy_input_mode", "interrupt") + + def test_invalid_argument_prints_usage(self): + cli_mod = _import_cli() + stub = self._make_cli() + with ( + patch.object(cli_mod, "_cprint") as mock_cprint, + patch.object(cli_mod, "save_config_value") as mock_save, + ): + cli_mod.HermesCLI._handle_busy_command(stub, "/busy nonsense") + + mock_save.assert_not_called() + printed = " ".join(str(c) for c in mock_cprint.call_args_list) + self.assertIn("Usage: /busy", printed) + + +class TestBusyCommandRegistry(unittest.TestCase): + def test_busy_in_registry(self): + from hermes_cli.commands import COMMAND_REGISTRY + + names = [c.name for c in COMMAND_REGISTRY] + assert "busy" in names + + def test_busy_subcommands_documented(self): + from hermes_cli.commands import COMMAND_REGISTRY + + busy = next(c for c in COMMAND_REGISTRY if c.name == "busy") + assert busy.args_hint == "[queue|interrupt|status]" + assert busy.category == "Configuration" diff --git a/website/docs/user-guide/cli.md b/website/docs/user-guide/cli.md index 62e70e3cc..90b571aa8 100644 --- a/website/docs/user-guide/cli.md +++ b/website/docs/user-guide/cli.md @@ -234,6 +234,14 @@ display: Queue mode is useful when you want to prepare follow-up messages without accidentally canceling in-flight work. Unknown values fall back to `"interrupt"`. +You can also change it inside the CLI: + +```text +/busy queue +/busy interrupt +/busy status +``` + ### Suspending to Background On Unix systems, press **`Ctrl+Z`** to suspend Hermes to the background — just like any terminal process. The shell prints a confirmation: