diff --git a/cli.py b/cli.py index bc4f4a76bef..4ca07fa0bf5 100644 --- a/cli.py +++ b/cli.py @@ -12024,6 +12024,7 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin): # Create the input area with multiline (Alt+Enter), autocomplete, and paste handling from prompt_toolkit.auto_suggest import AutoSuggestFromHistory + from prompt_toolkit.completion import ThreadedCompleter _completer = SlashCommandCompleter( @@ -12039,7 +12040,13 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin): wrap_lines=True, read_only=Condition(lambda: bool(cli_ref._command_running)), history=FileHistory(str(self._history_file)), - completer=_completer, + # complete_while_typing fires the completer on every keystroke. The + # completer does blocking work — fuzzy @-file indexing shells out to + # rg/fd (up to a 2s timeout) and path completion hits os.listdir/stat + # — so running it inline would stall the render loop on each key (very + # noticeable on WSL2/slow filesystems). ThreadedCompleter moves it off + # the UI event loop, keeping typing responsive. + completer=ThreadedCompleter(_completer), complete_while_typing=True, auto_suggest=SlashCommandAutoSuggest( history_suggest=AutoSuggestFromHistory(), diff --git a/hermes_cli/commands.py b/hermes_cli/commands.py index a1e20dabc08..f81d50eace9 100644 --- a/hermes_cli/commands.py +++ b/hermes_cli/commands.py @@ -1280,6 +1280,10 @@ class SlashCommandCompleter(Completer): current word doesn't look like a path. A word is path-like when it starts with ``./``, ``../``, ``~/``, ``/``, or contains a ``/`` separator (e.g. ``src/main.py``). + + Tokens containing a ``://`` scheme separator (e.g. URLs like + ``https://example.com/x``) are excluded even though they contain a + ``/`` — they are never useful local-path completions. """ if not text: return None @@ -1291,6 +1295,12 @@ class SlashCommandCompleter(Completer): word = text[i + 1:] if not word: return None + # URLs contain "/" but are not local paths. Treating them as paths fires + # os.listdir on every keystroke while typing/pasting a link (e.g. an + # https:// URL becomes a listdir of "https:") — pure latency, never a + # useful completion. Skip any token with a scheme separator. + if "://" in word: + return None # Only trigger path completion for path-like tokens if word.startswith(("./", "../", "~/", "/")) or "/" in word: return word diff --git a/tests/hermes_cli/test_path_completion.py b/tests/hermes_cli/test_path_completion.py index b41a36e2ec6..549d8d6a2c1 100644 --- a/tests/hermes_cli/test_path_completion.py +++ b/tests/hermes_cli/test_path_completion.py @@ -59,6 +59,32 @@ class TestExtractPathWord: def test_just_tilde_slash(self): assert SlashCommandCompleter._extract_path_word("~/") == "~/" + def test_url_is_not_treated_as_path(self): + # A URL contains "/" so the bare slash heuristic would otherwise return + # it as a path word, firing os.listdir("https:") on every keystroke. + assert SlashCommandCompleter._extract_path_word("see https://paste.rs/abc") is None + + def test_http_url_is_not_treated_as_path(self): + assert SlashCommandCompleter._extract_path_word("ref http://example.com/x") is None + + def test_scheme_alone_is_enough_to_reject(self): + # The "://" scheme separator is the signal, even before any path part + # has been typed. + assert SlashCommandCompleter._extract_path_word("ssh://host") is None + + def test_path_word_with_colon_but_no_scheme_still_resolves(self): + # Only the "://" scheme separator should reject; a bare colon inside a + # real path token must not regress path detection. + assert ( + SlashCommandCompleter._extract_path_word("open ./a:b/c.py") == "./a:b/c.py" + ) + + def test_ordinary_path_unaffected_by_url_guard(self): + assert ( + SlashCommandCompleter._extract_path_word("edit src/pkg/mod.py") + == "src/pkg/mod.py" + ) + class TestPathCompletions: def test_lists_current_directory(self, tmp_path): @@ -155,6 +181,23 @@ class TestIntegration: completions = list(completer.get_completions(doc, event)) assert completions == [] + def test_url_does_not_touch_filesystem(self, completer, monkeypatch): + # Regression for laggy typing: a URL token contains "/", so before the + # scheme guard it reached _path_completions and called os.listdir on + # every keystroke. Assert no completions AND that the filesystem is + # never touched while a URL is under the cursor. + import hermes_cli.commands as commands_mod + + def _fail(*_args, **_kwargs): + raise AssertionError("os.listdir must not run for a URL token") + + monkeypatch.setattr(commands_mod.os, "listdir", _fail) + + text = "open https://paste.rs/abc" + doc = Document(text, cursor_position=len(text)) + event = MagicMock() + assert list(completer.get_completions(doc, event)) == [] + def test_absolute_path_triggers_completion(self, completer): doc = Document("check /etc/hos", cursor_position=14) event = MagicMock()