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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ Only write entries that are worth mentioning to users.

## Unreleased

- Shell: Improve @ file mention discovery with git integration — the shell now uses `git ls-files` as the primary file discovery mechanism, fixing large repositories (e.g., 65k+ files) where the previous 1000-file limit caused late-alphabetical directories to be unreachable; supports scoped search (e.g., `@src/utils/`) for both git and non-git repositories
- Shell: Prevent path traversal in file mention scope parameter — scope values containing `..` are now rejected to prevent `@../` from escaping the workspace root
- Web: Restore unfiltered directory listing in file browser API — the web file browser now shows all directory entries including `node_modules`, `build`, `dist`, etc.

## 1.27.0 (2026-03-28)

- Shell: Add `/feedback` command — submit feedback directly from the CLI session; the command falls back to opening GitHub Issues on network errors or timeouts
Expand Down
4 changes: 4 additions & 0 deletions docs/en/release-notes/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ This page documents the changes in each Kimi Code CLI release.

## Unreleased

- Shell: Improve @ file mention discovery with git integration — the shell now uses `git ls-files` as the primary file discovery mechanism, fixing large repositories (e.g., 65k+ files) where the previous 1000-file limit caused late-alphabetical directories to be unreachable; supports scoped search (e.g., `@src/utils/`) for both git and non-git repositories
- Shell: Prevent path traversal in file mention scope parameter — scope values containing `..` are now rejected to prevent `@../` from escaping the workspace root
- Web: Restore unfiltered directory listing in file browser API — the web file browser now shows all directory entries including `node_modules`, `build`, `dist`, etc.

## 1.27.0 (2026-03-28)

- Shell: Add `/feedback` command — submit feedback directly from the CLI session; the command falls back to opening GitHub Issues on network errors or timeouts
Expand Down
4 changes: 4 additions & 0 deletions docs/zh/release-notes/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

## 未发布

- Shell:改进 @ 文件提及发现,集成 git 支持——Shell 现在使用 `git ls-files` 作为主要文件发现机制,修复大仓库(如 65k+ 文件)中之前 1000 文件限制导致靠后字母顺序目录无法访问的问题;支持范围搜索(如 `@src/utils/`),同时适用于 git 和非 git 仓库
- Shell:防止文件提及范围参数的路径遍历——现在拒绝包含 `..` 的范围值,防止 `@../` 逃离工作区根目录
- Web:恢复文件浏览器 API 的未过滤目录列表——Web 文件浏览器现在显示所有目录条目,包括 `node_modules`、`build`、`dist` 等

## 1.27.0 (2026-03-28)

- Shell:新增 `/feedback` 命令——可直接在 CLI 会话中提交反馈,网络错误或超时时自动回退到打开 GitHub Issues 页面
Expand Down
156 changes: 43 additions & 113 deletions src/kimi_cli/ui/shell/prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -611,82 +611,15 @@ def _render_selected_item_lines(


class LocalFileMentionCompleter(Completer):
"""Offer fuzzy `@` path completion by indexing workspace files."""
"""Offer fuzzy `@` path completion by indexing workspace files.

File discovery and ignore rules are delegated to
:mod:`kimi_cli.utils.file_filter` so that the web backend can reuse
them.
"""

_FRAGMENT_PATTERN = re.compile(r"[^\s@]+")
_TRIGGER_GUARDS = frozenset((".", "-", "_", "`", "'", '"', ":", "@", "#", "~"))
_IGNORED_NAME_GROUPS: dict[str, tuple[str, ...]] = {
"vcs_metadata": (".DS_Store", ".bzr", ".git", ".hg", ".svn"),
"tooling_caches": (
".build",
".cache",
".coverage",
".fleet",
".gradle",
".idea",
".ipynb_checkpoints",
".pnpm-store",
".pytest_cache",
".pub-cache",
".ruff_cache",
".swiftpm",
".tox",
".venv",
".vs",
".vscode",
".yarn",
".yarn-cache",
),
"js_frontend": (
".next",
".nuxt",
".parcel-cache",
".svelte-kit",
".turbo",
".vercel",
"node_modules",
),
"python_packaging": (
"__pycache__",
"build",
"coverage",
"dist",
"htmlcov",
"pip-wheel-metadata",
"venv",
),
"java_jvm": (".mvn", "out", "target"),
"dotnet_native": ("bin", "cmake-build-debug", "cmake-build-release", "obj"),
"bazel_buck": ("bazel-bin", "bazel-out", "bazel-testlogs", "buck-out"),
"misc_artifacts": (
".dart_tool",
".serverless",
".stack-work",
".terraform",
".terragrunt-cache",
"DerivedData",
"Pods",
"deps",
"tmp",
"vendor",
),
}
_IGNORED_NAMES = frozenset(name for group in _IGNORED_NAME_GROUPS.values() for name in group)
_IGNORED_PATTERN_PARTS: tuple[str, ...] = (
r".*_cache$",
r".*-cache$",
r".*\.egg-info$",
r".*\.dist-info$",
r".*\.py[co]$",
r".*\.class$",
r".*\.sw[po]$",
r".*~$",
r".*\.(?:tmp|bak)$",
)
_IGNORED_PATTERNS = re.compile(
"|".join(f"(?:{part})" for part in _IGNORED_PATTERN_PARTS),
re.IGNORECASE,
)

def __init__(
self,
Expand All @@ -700,9 +633,12 @@ def __init__(
self._limit = limit
self._cache_time: float = 0.0
self._cached_paths: list[str] = []
self._cache_scope: str | None = None
self._top_cache_time: float = 0.0
self._top_cached_paths: list[str] = []
self._fragment_hint: str | None = None
self._is_git: bool | None = None # lazily detected
self._git_index_mtime: float | None = None

self._word_completer = WordCompleter(
self._get_paths,
Expand All @@ -716,21 +652,15 @@ def __init__(
pattern=r"^[^\s@]*",
)

@classmethod
def _is_ignored(cls, name: str) -> bool:
if not name:
return True
if name in cls._IGNORED_NAMES:
return True
return bool(cls._IGNORED_PATTERNS.fullmatch(name))

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()

def _get_top_level_paths(self) -> list[str]:
from kimi_cli.utils.file_filter import is_ignored

now = time.monotonic()
if now - self._top_cache_time <= self._refresh_interval:
return self._top_cached_paths
Expand All @@ -739,7 +669,7 @@ def _get_top_level_paths(self) -> list[str]:
try:
for entry in sorted(self._root.iterdir(), key=lambda p: p.name):
name = entry.name
if self._is_ignored(name):
if is_ignored(name):
continue
entries.append(f"{name}/" if entry.is_dir() else name)
if len(entries) >= self._limit:
Expand All @@ -752,45 +682,45 @@ def _get_top_level_paths(self) -> list[str]:
return self._top_cached_paths

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

paths: list[str] = []
try:
for current_root, dirs, files in os.walk(self._root):
relative_root = Path(current_root).relative_to(self._root)
from kimi_cli.utils.file_filter import (
detect_git,
git_index_mtime,
list_files_git,
list_files_walk,
)

# Prevent descending into ignored directories.
dirs[:] = sorted(d for d in dirs if not self._is_ignored(d))
fragment = self._fragment_hint or ""

if relative_root.parts and any(
self._is_ignored(part) for part in relative_root.parts
):
dirs[:] = []
continue
scope: str | None = None
if "/" in fragment:
scope = fragment.rsplit("/", 1)[0]

if relative_root.parts:
paths.append(relative_root.as_posix() + "/")
if len(paths) >= self._limit:
break
now = time.monotonic()
cache_valid = (
now - self._cache_time <= self._refresh_interval and self._cache_scope == scope
)

for file_name in sorted(files):
if self._is_ignored(file_name):
continue
relative = (relative_root / file_name).as_posix()
if not relative:
continue
paths.append(relative)
if len(paths) >= self._limit:
break
# Invalidate on .git/index mtime change (like Claude Code).
if cache_valid and self._is_git:
mtime = git_index_mtime(self._root)
if mtime != self._git_index_mtime:
cache_valid = False

if len(paths) >= self._limit:
break
except OSError:
if cache_valid:
return self._cached_paths

if self._is_git is None:
self._is_git = detect_git(self._root)

paths: list[str] | None = None
if self._is_git:
paths = list_files_git(self._root, scope)
self._git_index_mtime = git_index_mtime(self._root)
Comment on lines +716 to +718
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reapply completion cap for git-backed deep searches

When a repo is detected, deep @ completion now calls list_files_git without applying self._limit, so the candidate set becomes unbounded while the os.walk fallback still enforces the limit. In large repos this can feed tens of thousands of paths into FuzzyCompleter on each completion request, causing noticeable UI latency and negating the guardrail that the limit constructor argument previously provided.

Useful? React with 👍 / 👎.

if paths is None:
paths = list_files_walk(self._root, scope, limit=self._limit)

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

Expand Down
Loading
Loading