feat(kap-server): add workspace fs:suggest file completion endpoint - #3019
Conversation
|
commit: |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e9b2de5a71
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| const listed = await this.list({ | ||
| path: '.', | ||
| depth: 1, | ||
| limit: req.limit, | ||
| show_hidden: req.show_hidden, |
There was a problem hiding this comment.
Apply filters before limiting the root listing
When an empty query uses include_globs, or show_hidden: true exposes a VCS directory, list applies req.limit before the subsequent filters. For example, with limit: 1, a directory sorted before a matching root-level TypeScript file can consume the only slot and then be removed by include_globs, yielding no usable candidate even though one exists and there is no pagination mechanism to retrieve it. Filter candidates before enforcing the suggestion limit.
Useful? React with 👍 / 👎.
| if (controller.signal.aborted) throw err; | ||
| this.telemetry.track2('fs_suggest_node_fallback', { reason: 'rg_error' }); | ||
| return await this.suggestWithNode(query, cap); |
There was a problem hiding this comment.
Enforce the timeout in the Node fallback
When ripgrep is unavailable or fails, this fallback receives neither the abort signal nor any other work budget. On a large or slow remote workspace, the recursive walk therefore continues past SUGGEST_TIMEOUT_MS, and the timer is only cleared after it eventually finishes, allowing one completion request to remain pending indefinitely. Pass the signal into the fallback traversal and stop with a truncated result when it fires.
Useful? React with 👍 / 👎.
| let slash = line.lastIndexOf('/'); | ||
| while (slash > 0) { | ||
| const dir = line.slice(0, slash); | ||
| if (!seenDirs.has(dir)) { | ||
| seenDirs.add(dir); |
There was a problem hiding this comment.
Surface directories that contain no listed files
When ripgrep is available, directories are synthesized exclusively from paths emitted by rg --files. An empty directory—or one containing only ignored files—emits no file path, so a query matching that directory can never return it even though the endpoint advertises directory completions and the Node fallback does return such directories. Enumerate directories separately or supplement the ripgrep output so results do not depend on directory contents.
Useful? React with 👍 / 👎.
| try { | ||
| await Promise.all([drainStdout(), readStream(proc.stderr), proc.wait().catch(() => -1)]); |
There was a problem hiding this comment.
Check ripgrep's exit status before accepting results
When rg --files starts successfully but exits with an error status, such as code 2 for an I/O or permission error, proc.wait() resolves with that code and this expression discards it. The method then returns an empty or partial response as a successful, complete suggestion instead of taking the documented rg_error fallback; only spawn and stream exceptions currently reach that path. Preserve the exit code and treat error statuses as failures while retaining ripgrep's valid empty-result status.
Useful? React with 👍 / 👎.
| readonly token?: string | undefined; | ||
| readonly workspace: string; | ||
| readonly query: string; | ||
| readonly limit?: number | undefined; | ||
| readonly followGitignore?: boolean | undefined; |
There was a problem hiding this comment.
Remove explicit undefined from optional properties
These optional properties redundantly add | undefined, and the same pattern continues through the remaining optional fields in this interface. Declare them as token?: string, limit?: number, and so on to comply with the repository's required optional-property convention.
AGENTS.md reference: AGENTS.md:L55-L57
Useful? React with 👍 / 👎.
e9b2de5 to
0858eab
Compare
0858eab to
a3c00e2
Compare
Related Issue
No linked issue — the problem is explained below.
Problem
Editor integrations need a file/folder completion backend for
@mentions: as the user types a partial name or path, the server should return a small set of highly relevant path candidates. The existingfs:searchonly scores basenames, has no path-pattern mode, and offers no hidden-file control, so it cannot drive@completion.What changed
POST /api/v1/workspace/fs:suggest(session-less, same envelope/conventions asfs:search): empty query lists the workspace root; a query containing/matches path segments in order (subsequence within a segment, segment skipping allowed); a plain query fuzzy-matches basenames. Ranking prefers exact/prefix hits, then shorter names and shallower paths;limit,truncated,follow_gitignore,show_hidden, and include/exclude globs are supported, and missing-path matches return an empty list rather than an error.agent-core-v2):IWorkspaceFsService.suggestenumerates candidates withrg --files(streamed line-by-line into a bounded top-N heap, so memory stays flat on large trees), which gives git-consistent nested.gitignorehandling for local and remote runtimes; when rg is unavailable it falls back to the existing Node walk and emits a telemetry event. Accepted trade-offs: the rg path yields no symlink candidates, and the walk fallback does not honor nested gitignore.packages/protocol(src/fs.ts,src/rest/fs.ts).Checklist
gen-changesetsskill, or this PR needs no changeset. (No changeset: kap-server REST addition currently consumed only by dev tooling / external editor clients, not perceivable from the CLI.)gen-docsskill, or this PR needs no doc update. (No user-facing surface changed.)