mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-17 09:41:58 +00:00
Merge pull request #47701 from kshitijk4poor/salvage/cli-completer-keystroke-latency
fix(cli): keep typing responsive by running completion off the UI event loop
This commit is contained in:
commit
9901141d64
3 changed files with 61 additions and 1 deletions
9
cli.py
9
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(),
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue