Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 29 additions & 7 deletions src/kimi_cli/ui/shell/prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ def __init__(
self._limit = limit
self._cache_time: float = 0.0
self._cached_paths: list[str] = []
self._cache_scope: str = ""
self._top_cache_time: float = 0.0
self._top_cached_paths: list[str] = []
self._fragment_hint: str | None = None
Expand Down Expand Up @@ -253,7 +254,7 @@ def _get_paths(self) -> list[str]:
fragment = self._fragment_hint or ""
if "/" not in fragment and len(fragment) < 3:
return self._get_top_level_paths()
return self._get_deep_paths()
return self._get_deep_paths(fragment)

def _get_top_level_paths(self) -> list[str]:
now = time.monotonic()
Expand All @@ -276,15 +277,35 @@ def _get_top_level_paths(self) -> list[str]:
self._top_cache_time = now
return self._top_cached_paths

def _get_deep_paths(self) -> list[str]:
def _get_deep_paths(self, fragment: str = "") -> list[str]:
now = time.monotonic()
if now - self._cache_time <= self._refresh_interval:

# Scope the walk to the directory prefix from the fragment.
# For "src/utils/he" → walk from <root>/src/utils/ instead of <root>/
# This avoids exhausting the limit on unrelated directories in large repos.
scope_prefix = ""
walk_root = self._root
if "/" in fragment:
dir_part = fragment.rsplit("/", 1)[0]
candidate = self._root / dir_part
if candidate.is_dir():
walk_root = candidate
scope_prefix = dir_part + "/"
Comment on lines +288 to +293
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Fragment starting with / produces empty dir_part, corrupting all completion paths with a leading / prefix

When a user types @/something, _extract_fragment returns the fragment "/something". In _get_deep_paths, fragment.rsplit("/", 1)[0] yields dir_part = "". Then self._root / "" resolves to self._root itself (which is a directory), so candidate.is_dir() is True and scope_prefix is set to "" + "/" = "/". Every path emitted by the walk is then prefixed with "/", producing completions like "/nested/" and "/file.py" instead of "nested/" and "file.py". The scope directory entry itself is also the bare string "/". These corrupted paths won't match any real relative path from the project root, so completions become unusable for this input. The old code (before this PR) had no scope_prefix logic and would have returned correct relative paths for this edge case.

Suggested change
if "/" in fragment:
dir_part = fragment.rsplit("/", 1)[0]
candidate = self._root / dir_part
if candidate.is_dir():
walk_root = candidate
scope_prefix = dir_part + "/"
if "/" in fragment:
dir_part = fragment.rsplit("/", 1)[0]
if dir_part:
candidate = self._root / dir_part
if candidate.is_dir():
walk_root = candidate
scope_prefix = dir_part + "/"
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.


cache_key = scope_prefix
if (
now - self._cache_time <= self._refresh_interval
and getattr(self, "_cache_scope", "") == cache_key
):
return self._cached_paths

paths: list[str] = []
# When scoped, include the scope directory itself so callers see it.
if scope_prefix:
paths.append(scope_prefix)
try:
for current_root, dirs, files in os.walk(self._root):
relative_root = Path(current_root).relative_to(self._root)
for current_root, dirs, files in os.walk(walk_root):
relative_root = Path(current_root).relative_to(walk_root)

# Prevent descending into ignored directories.
dirs[:] = sorted(d for d in dirs if not self._is_ignored(d))
Expand All @@ -296,7 +317,7 @@ def _get_deep_paths(self) -> list[str]:
continue

if relative_root.parts:
paths.append(relative_root.as_posix() + "/")
paths.append(scope_prefix + relative_root.as_posix() + "/")
if len(paths) >= self._limit:
break

Expand All @@ -306,7 +327,7 @@ def _get_deep_paths(self) -> list[str]:
relative = (relative_root / file_name).as_posix()
if not relative:
continue
paths.append(relative)
paths.append(scope_prefix + relative)
if len(paths) >= self._limit:
break

Expand All @@ -317,6 +338,7 @@ def _get_deep_paths(self) -> list[str]:

self._cached_paths = paths
self._cache_time = now
self._cache_scope = cache_key
return self._cached_paths

@staticmethod
Expand Down
47 changes: 47 additions & 0 deletions tests/ui_and_conv/test_file_completer.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,53 @@ def test_at_guard_prevents_email_like_fragments(tmp_path: Path):
assert not texts


def test_scoped_walk_finds_deep_files_in_large_repos(tmp_path: Path):
"""In large repos, scoped walk should find files under the typed directory.

Regression test for #1375: when the total file count exceeds the limit,
files in later directories (alphabetically) become unreachable. Scoping
the walk to the typed directory prefix avoids this.
"""
limit = 20

# Create many files in early directories to exhaust a small limit
for i in range(15):
d = tmp_path / f"aaa_dir_{i:02d}"
d.mkdir()
(d / "file.txt").write_text("x")

# Target files are in a directory that would be unreachable with flat walk
target = tmp_path / "zzz_target" / "nested"
target.mkdir(parents=True)
(target / "important.py").write_text("# important")
(target / "helper.py").write_text("# helper")

completer = LocalFileMentionCompleter(tmp_path, limit=limit)

# Without scoping, "zzz_target/" would never appear because the limit
# is exhausted by aaa_dir_* entries. With scoping, typing the directory
# prefix narrows the walk to just that subtree.
texts = _completion_texts(completer, "@zzz_target/")

assert "zzz_target/nested/" in texts
assert "zzz_target/nested/important.py" in texts
assert "zzz_target/nested/helper.py" in texts


def test_scoped_walk_with_partial_filename(tmp_path: Path):
"""Scoped walk works when fragment includes a partial filename after slash."""
src = tmp_path / "src" / "utils"
src.mkdir(parents=True)
(src / "helpers.py").write_text("")
(src / "config.py").write_text("")

completer = LocalFileMentionCompleter(tmp_path)

texts = _completion_texts(completer, "@src/utils/hel")

assert "src/utils/helpers.py" in texts


def test_basename_prefix_is_ranked_first(tmp_path: Path):
"""Prefer basename prefix matches over cross-segment fuzzy matches.

Expand Down
Loading