fix: add prefix matching to slash command dispatcher
Slash commands previously required exact full names. Typing /con returned 'Unknown command' even though /config was the only match. Add unambiguous prefix matching in process_command(): - Unique prefix (e.g. /con -> /config): dispatch immediately - Ambiguous prefix (e.g. /re -> /reset, /retry, /reasoning...): show 'Did you mean' suggestions - No match: existing 'Unknown command' error Prefix matching uses the COMMANDS dict from hermes_cli/commands.py (same source as SlashCommandCompleter) so it stays in sync with any new commands added there. Closes #928
This commit is contained in:
17
cli.py
17
cli.py
@@ -3094,8 +3094,21 @@ class HermesCLI:
|
||||
else:
|
||||
self.console.print(f"[bold red]Failed to load skill for {base_cmd}[/]")
|
||||
else:
|
||||
self.console.print(f"[bold red]Unknown command: {cmd_lower}[/]")
|
||||
self.console.print("[dim #B8860B]Type /help for available commands[/]")
|
||||
# Prefix matching: if input uniquely identifies one command, execute it
|
||||
from hermes_cli.commands import COMMANDS
|
||||
typed_base = cmd_lower.split()[0]
|
||||
matches = [c for c in COMMANDS if c.startswith(typed_base)]
|
||||
if len(matches) == 1:
|
||||
# Re-dispatch with the full command name, preserving any arguments
|
||||
remainder = cmd_original.strip()[len(typed_base):]
|
||||
full_cmd = matches[0] + remainder
|
||||
return self.process_command(full_cmd)
|
||||
elif len(matches) > 1:
|
||||
self.console.print(f"[bold yellow]Ambiguous command: {cmd_lower}[/]")
|
||||
self.console.print(f"[dim]Did you mean: {', '.join(sorted(matches))}?[/]")
|
||||
else:
|
||||
self.console.print(f"[bold red]Unknown command: {cmd_lower}[/]")
|
||||
self.console.print("[dim #B8860B]Type /help for available commands[/]")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
60
tests/test_cli_prefix_matching.py
Normal file
60
tests/test_cli_prefix_matching.py
Normal file
@@ -0,0 +1,60 @@
|
||||
"""Tests for slash command prefix matching in HermesCLI.process_command."""
|
||||
from unittest.mock import MagicMock, patch
|
||||
from cli import HermesCLI
|
||||
|
||||
|
||||
def _make_cli():
|
||||
cli_obj = HermesCLI.__new__(HermesCLI)
|
||||
cli_obj.config = {}
|
||||
cli_obj.console = MagicMock()
|
||||
cli_obj.agent = None
|
||||
cli_obj.conversation_history = []
|
||||
return cli_obj
|
||||
|
||||
|
||||
class TestSlashCommandPrefixMatching:
|
||||
def test_unique_prefix_dispatches_command(self):
|
||||
"""/con should dispatch to /config when it uniquely matches."""
|
||||
cli_obj = _make_cli()
|
||||
with patch.object(cli_obj, 'show_config') as mock_config:
|
||||
cli_obj.process_command("/con")
|
||||
mock_config.assert_called_once()
|
||||
|
||||
def test_unique_prefix_with_args_dispatches_command(self):
|
||||
"""/mo with argument should dispatch to /model."""
|
||||
cli_obj = _make_cli()
|
||||
with patch.object(cli_obj, 'process_command', wraps=cli_obj.process_command):
|
||||
with patch("hermes_cli.models.fetch_api_models", return_value=None), \
|
||||
patch("cli.save_config_value"):
|
||||
cli_obj.model = "current-model"
|
||||
cli_obj.provider = "openrouter"
|
||||
cli_obj.base_url = "https://openrouter.ai/api/v1"
|
||||
cli_obj.api_key = "test"
|
||||
cli_obj._explicit_api_key = None
|
||||
cli_obj._explicit_base_url = None
|
||||
cli_obj.requested_provider = "openrouter"
|
||||
# /mod uniquely matches /model
|
||||
result = cli_obj.process_command("/mod")
|
||||
assert result is True
|
||||
|
||||
def test_ambiguous_prefix_shows_suggestions(self):
|
||||
"""/re matches /reset, /retry, /reload-mcp, /reasoning, /rollback — should show suggestions."""
|
||||
cli_obj = _make_cli()
|
||||
cli_obj.process_command("/re")
|
||||
# Should print ambiguous message, not unknown command
|
||||
printed = " ".join(str(c) for c in cli_obj.console.print.call_args_list)
|
||||
assert "Ambiguous" in printed or "Did you mean" in printed
|
||||
|
||||
def test_unknown_command_shows_error(self):
|
||||
"""/xyz should show unknown command error."""
|
||||
cli_obj = _make_cli()
|
||||
cli_obj.process_command("/xyz")
|
||||
printed = " ".join(str(c) for c in cli_obj.console.print.call_args_list)
|
||||
assert "Unknown command" in printed
|
||||
|
||||
def test_exact_command_still_works(self):
|
||||
"""/help should still work as exact match."""
|
||||
cli_obj = _make_cli()
|
||||
with patch.object(cli_obj, 'show_help') as mock_help:
|
||||
cli_obj.process_command("/help")
|
||||
mock_help.assert_called_once()
|
||||
Reference in New Issue
Block a user