diff --git a/EvoScientist/commands/_completion_engine.py b/EvoScientist/commands/_completion_engine.py index 2b52b22..ac094e2 100644 --- a/EvoScientist/commands/_completion_engine.py +++ b/EvoScientist/commands/_completion_engine.py @@ -58,12 +58,20 @@ def compute_completions(text: str, cursor_pos: int) -> CompletionResult: commands = cmd_manager.list_commands() matches = [(c, d) for c, d in commands if c.startswith(prefix)] + # Whether the typed prefix is itself a complete command name. Checked + # via membership rather than ``len(matches) == 1`` so a command whose + # name is a strict prefix of another (e.g. ``/model`` vs + # ``/model-fallback``) still counts as an exact match — otherwise the + # popup never hides on the shorter command and Enter completes instead + # of submitting it. + exact = any(c == prefix for c, _ in matches) + # Exact match with no trailing space → hide - if len(matches) == 1 and matches[0][0] == prefix and not has_trailing_space: + if exact and not has_trailing_space: return CompletionResult(CompletionKind.EMPTY, []) # Exact match + trailing space + has subcommands → show subcommands - if len(matches) == 1 and matches[0][0] == prefix and has_trailing_space: + if exact and has_trailing_space: if not cmd_manager.get_subcommands(prefix): return CompletionResult(CompletionKind.EMPTY, []) sub_items = cmd_manager.list_subcommands(cmd_name) diff --git a/tests/test_cli_completion.py b/tests/test_cli_completion.py index a9224d1..606ae22 100644 --- a/tests/test_cli_completion.py +++ b/tests/test_cli_completion.py @@ -29,6 +29,20 @@ class TestSlashCommandCompleter: completions = list(completer.get_completions(_doc("/help"), None)) assert completions == [] + def test_exact_command_hides_even_when_prefix_of_another(self): + """Regression for #293: ``/model`` must hide on exact match so Enter + submits it, even though ``/model-fallback`` shares the prefix. Before + the fix the popup stayed visible (two prefix matches) and the TUI's + Enter handler completed the text instead of executing the command. + """ + completer = SlashCommandCompleter() + # Sanity: both commands share the prefix, so a partial prefix lists both. + partial = {c.text for c in completer.get_completions(_doc("/mode"), None)} + assert {"/model", "/model-fallback"} <= partial + # Exact ``/model`` with no trailing space → hide. + completions = list(completer.get_completions(_doc("/model"), None)) + assert completions == [] + def test_non_slash_returns_empty(self): completer = SlashCommandCompleter() completions = list(completer.get_completions(_doc("hello"), None))