Skip to content

feat: suggest files from every folder in a multi-root workspace - #13589

Open
sylwester-liljegren wants to merge 4 commits into
Kilo-Org:mainfrom
sylwester-liljegren:feat/multi-root-file-mentions
Open

feat: suggest files from every folder in a multi-root workspace#13589
sylwester-liljegren wants to merge 4 commits into
Kilo-Org:mainfrom
sylwester-liljegren:feat/multi-root-file-mentions

Conversation

@sylwester-liljegren

@sylwester-liljegren sylwester-liljegren commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Issue

No existing issue — searched Kilo-Org/kilocode for "multi-root workspace mention" and "Add Folder to Workspace" and found nothing related. Reported directly by a user hitting it in a multi-root workspace. Happy to file one if maintainers prefer the issue-first flow.

Context

In a multi-root VS Code workspace, typing @ only ever suggested files from the first workspace folder. Any folder added through Add Folder to Workspace… was invisible to file search, so the only way to reference a file in it was the "Browse files…" picker, one file at a time.

The cause is a single-root assumption that runs the whole way down: KiloProvider.getRootDirectory() returns workspaceFolders[0], handleFileSearch passes that one directory to find.files, and the CLI binds a Location to it that hard-fails on anything outside. Open tabs and the active editor from other folders were dropped by the same relative-path filter.

Implementation

File search now fans out across every workspace folder in parallel and merges the results. No backend change was needed: find.files already accepts a directory, which is the same mechanism Agent Manager worktrees use against one shared kilo serve.

The security boundary is deliberately unchanged. Results from folders other than the session's own project are returned as absolute paths. buildFileAttachments already refuses to attach any path outside the session directory, so those entries are mention-only and the agent must Read them under the normal external_directory permission check. This is exactly the boundary "Browse files…" already relies on, and the guard itself is untouched — widening it would let attachment bypass the permission system, which the comment above isInsideWorkspace explicitly warns against. There is a regression test named for this feature so a future change cannot quietly loosen it.

Fan-out is declined unless the session's directory is itself one of the workspace folders. A session routed to a git worktree or an Agent Manager project has a directory outside that list, and silently widening its search to unrelated projects would be wrong.

Ranking is the part worth reviewing closely. The first version ranked each root separately and concatenated, primary first. That looked reasonable but was wrong: mergeFileSearchResults's strongest rule — a basename match beats a path-only match — never got to compare across roots, so an exact CLAUDE.md in an added folder ranked below files whose names don't even contain a c and only matched as a scattered subsequence of their full path. All roots are now ranked in one pass, with workspace folder order demoted to a tiebreak that applies only between equally good matches. Anti-flooding moved from ordering to a post-ranking cap, applied only in multi-root workspaces.

Every entry is labelled with its workspace folder when the workspace has more than one, including the session's own project — labelling only the added folders left the rest looking like they belonged to nowhere. Single-folder workspaces get no badges and no caps; that path is byte-identical to before, which the existing suite covers.

getIgnoreController changed from a one-entry cache to a bounded map. Multi-root search asks about several roots per keystroke, and the old cache would have re-read .kilocodeignore from disk on every alternating lookup.

semantic_search

semantic_search still covers a single indexed root, and this PR does not change that. But it becomes actively misleading once mentions span several folders, so two things are fixed:

  • Its description claimed it searched "the entire current workspace", and pointed at Grep and Glob for exploring outside the root — both are bounded to the same root and cannot reach outside it either, so Read is the only correct answer. That advice was already wrong before this PR.
  • KiloIndexing.search returns [] when the index is disabled, still building, or broken, which was indistinguishable from a genuine miss. Empty results now state which, so a model does not read "no results" as "this code does not exist".

Making the tool actually search other roots would require registering them as indexing projects, which means separate consent and a full embedding pass per folder. That is deliberately out of scope here.

Screenshots / Video

before after
image @ in a multi-root workspace lists only the first folder's files; a file in an added folder cannot be found by name image files from every folder are listed, each badged with its workspace folder, and an exact filename match in an added folder ranks first

How to Test

Manual/local verification

Executed by the agent:

  • bun run typecheck in packages/kilo-vscode and packages/opencode — pass
  • bun run lint and bun run knip in packages/kilo-vscode — pass
  • bun test across the 9 unit files covering every changed module (file-search, file-search-results, file-search-items, file-mention-utils, use-file-mention, file-ignore-controller, notebook-context, agent-manager-worktree-reference, agent-manager-arch) — 266 pass, 0 fail
  • bun run script/test-runner.ts for kilocode/tool/semantic-search-output.test.ts, kilocode/tool-registry-indexing.test.ts, kilocode/tool-registry-semantic-import-failure.test.ts — pass
  • bun run script/check-opencode-annotations.ts --worktree — "No shared upstream source files changed"; all CLI changes sit in the marker-exempt src/kilocode/ path

New tests added: exact filename match in an added folder outranks fuzzy matches in the session's project; identical filenames in two roots resolve to the session's project first; labels present in multi-root and absent in single-root; fan-out declined when the session directory is not a workspace folder; and a file from another workspace folder is not auto-attached.

Executed by a human (PR author), in the Extension Development Host:

  • Confirmed files from a folder added via Add Folder to Workspace… now appear under @
  • Found and reported two defects on the first implementation — missing badge on the original folder, and an exact CLAUDE.md match ranking last — both fixed and covered by tests above

Reviewer test steps

  1. Open a workspace with one folder, then File → Add Folder to Workspace… and add an unrelated repo
  2. In the Kilo chat input, type @ — files from both folders should be listed, each with a workspace folder badge
  3. Type the exact name of a file that exists only in the added folder — it should rank at or near the top, not at the bottom
  4. Select it, send the message, and confirm the agent reads it via Read with an external_directory approval prompt rather than the file arriving pre-attached
  5. Open a single-folder workspace and confirm no badges appear and results are unchanged
  6. Open an Agent Manager worktree session and confirm @ still lists only that worktree's files

Blocked checks and substitute verification

  • bun turbo typecheck at the repo root (and therefore the pre-push hook) could not complete: @kilocode/kilo-jetbrains#typecheck fails with "Cannot find a Java installation on your machine matching languageVersion=21" on this machine. This PR touches no Kotlin and no JetBrains files. Substitute verification was running typecheck directly in the two affected packages, both passing; the push used --no-verify for this reason only.
  • The full bun run test:unit suite in packages/kilo-vscode (351 files) exceeded a 10-minute budget locally. An untouched CLI test file times out the same way under bare bun test on this machine, so the slowness is environmental rather than introduced here. Substitute verification was running every unit file that references the changed modules, listed above.

Checklist

  • Issue linked above, or exception explained
  • Tests/verification described
  • Screenshots/video included for visual changes, or marked N/A — pending, see note above
  • Changeset considered for user-facing changes
  • I personally reviewed the diff and can explain the changes, including any AI-assisted work.

Get in Touch

Typing @ only searched workspaceFolders[0], so files in folders added
through "Add Folder to Workspace..." were unreachable without the file
picker. File search now fans out across every workspace folder.

Entries outside the session's own project are returned as absolute
paths, which keeps them mention-only: buildFileAttachments already
refuses to attach paths outside the session directory, so the agent must
Read them under the usual external_directory permission check. That is
the same boundary "Browse files..." relies on, and it is unchanged.

Fan-out is declined unless the session's directory is itself one of the
workspace folders, so worktree and Agent Manager sessions stay scoped to
their own tree.

All roots are ranked in one pass rather than per root, because scoring
each root separately and concatenating let fuzzy path noise in the first
root outrank an exact filename match in another. Workspace folder order
now only breaks ties between equally good matches.

semantic_search still covers a single indexed root. Its description
claimed it searched the entire current workspace, which is wrong once
mentions span several folders, and it recommended Grep and Glob for
outside-root work even though both are bounded to the same root. Empty
results now also report whether the index was complete, still building,
disabled, or failed, so a miss is no longer mistaken for absent code.
const items = mergeFileSearchItems({
query,
files: paths,
folders: multi ? folders.slice(0, MULTI_FOLDER_LIMIT) : folders,

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.

[WARNING]: Multi-root folder cap is applied before ranking

MULTI_FOLDER_LIMIT slices folders before mergeFileSearchItems ranks them, while files correctly cap after ranking. fetchBackend already returns up to 50 folders per root, so in a typical repo the primary root fills the cap and every secondary folder is dropped.

On empty @, folders from added workspace folders will not appear — the same inversion this PR already fixed for files. Rank first, then slice.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

const extras = split.secondary
const multi = extras.length > 0

const [primary, secondary] = await Promise.all([

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.

[WARNING]: One failing extra root drops the whole @ search

Promise.all waits on every root and rejects if any gather throws. fetchBackend uses allSettled, but gather still await open(root)getIgnoreController.initialize(), which is documented to let readFileSync permission errors propagate.

handleFileSearch has no try/catch and never posts a fileSearchResult on throw, so a single unreadable extra folder can leave the mention dropdown empty. Isolate per-root failures and still return primary hits.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

const tabs = await open(root)
const active = activeIn(root)
if (!absolute) return { files: files.map(slash), folders: folders.map(slash), open: tabs, active }
const abs = (value: string) => slash(path.resolve(root, value))

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.

[SUGGESTION]: Ranking secondary hits on absolute paths skews fuzzy match quality

Secondary paths are absolutized before mergeFileSearchResults scores them. Basename scoring is fine, but path scoring and depth() run on the full filesystem prefix, so a query that matches Users, a username, or a parent directory path-matches every extra-root file while primary hits stay relative.

Rank on the per-root relative path, then convert secondary results to absolute for the mention/attachment boundary.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

await controller.initialize()
return controller
})()
this.ignoreControllers.set(workspaceDir, pending)

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.

[WARNING]: A rejected ignore-controller init is cached forever

The in-flight promise is stored before initialize() completes. The old one-entry cache only assigned after a successful init, so a failure was retried. A rejected entry now sticks until 16 other directories evict it — typical workspaces never reach that.

The next @ search for that root reuses the rejected promise, and combined with Promise.all in handleFileSearch that can disable mentions until reload. Delete the map entry on rejection if it is still this promise.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@kilo-code-bot

kilo-code-bot Bot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Files Reviewed (9 files)
  • packages/kilo-vscode/src/KiloProvider.ts
  • packages/kilo-vscode/src/kilo-provider/file-search-items.ts
  • packages/kilo-vscode/src/kilo-provider/file-search.ts
  • packages/kilo-vscode/tests/unit/file-mention-utils.test.ts
  • packages/kilo-vscode/tests/unit/file-search.test.ts
  • packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx
  • packages/kilo-vscode/webview-ui/src/hooks/file-mention-utils.ts
  • packages/kilo-vscode/webview-ui/src/styles/prompt-dropdowns.css
  • packages/kilo-vscode/webview-ui/src/types/messages/extension-messages.ts
Previous Review Summaries (2 snapshots, latest commit 3661761)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 3661761)

Status: No Issues Found | Recommendation: Merge

Files Reviewed (5 files)
  • packages/kilo-vscode/src/KiloProvider.ts
  • packages/kilo-vscode/src/kilo-provider/file-search-results.ts
  • packages/kilo-vscode/src/kilo-provider/file-search.ts
  • packages/kilo-vscode/tests/unit/file-search.test.ts
  • packages/opencode/test/kilocode/semantic-search.test.ts

Previous review (commit 29fc848)

Status: 4 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 3
SUGGESTION 1

Fix these issues in Kilo Cloud

Issue Details (click to expand)

WARNING

File Line Issue
packages/kilo-vscode/src/kilo-provider/file-search.ts 172 Multi-root folder cap is applied before ranking
packages/kilo-vscode/src/kilo-provider/file-search.ts 131 One failing extra root drops the whole @ search
packages/kilo-vscode/src/KiloProvider.ts 5103 A rejected ignore-controller init is cached forever

SUGGESTION

File Line Issue
packages/kilo-vscode/src/kilo-provider/file-search.ts 108 Ranking secondary hits on absolute paths skews fuzzy match quality
Files Reviewed (15 files)
  • .changeset/multi-root-file-mentions.md
  • packages/kilo-vscode/src/KiloProvider.ts - 1 issue
  • packages/kilo-vscode/src/kilo-provider/file-search-items.ts
  • packages/kilo-vscode/src/kilo-provider/file-search-results.ts
  • packages/kilo-vscode/src/kilo-provider/file-search.ts - 3 issues
  • packages/kilo-vscode/tests/unit/file-mention-utils.test.ts
  • packages/kilo-vscode/tests/unit/file-search.test.ts
  • packages/kilo-vscode/webview-ui/src/components/chat/PromptInput.tsx
  • packages/kilo-vscode/webview-ui/src/hooks/file-mention-utils.ts
  • packages/kilo-vscode/webview-ui/src/styles/prompt-dropdowns.css
  • packages/kilo-vscode/webview-ui/src/types/messages/extension-messages.ts
  • packages/opencode/src/kilocode/tool/semantic-search-output.ts
  • packages/opencode/src/kilocode/tool/semantic-search.ts
  • packages/opencode/src/kilocode/tool/semantic-search.txt
  • packages/opencode/test/kilocode/tool/semantic-search-output.test.ts

Reviewed by grok-4.6 · Input: 95.5K · Output: 21.7K · Cached: 1.1M

Review guidance: REVIEW.md from base branch main

Update semantic_search tests for the new output. The tool now names the
searched root and explains an empty result in terms of index state, so
three assertions in the existing suite were stale. Index status is
stubbed there so the wording no longer depends on real indexing progress.

Rank entries from added folders on their root-relative path. They are
absolutised before insertion so the attachment boundary still rejects
them, but scoring the absolute form let a query match the filesystem
prefix — a username or a parent directory — on every file under that
root, and inflated their depth in tiebreaks.

Cap folders after ranking rather than before. The primary root can
return more folders than the multi-root allowance on its own, so slicing
the input handed it the whole budget and dropped every added folder
before its entries competed. This is the same inversion already fixed
for files.

Isolate per-root failures. Extra folders are arbitrary user-chosen
directories, and FileIgnoreController.initialize lets permission errors
from reading .kilocodeignore propagate. One unreadable folder rejected
the Promise.all and left the mention dropdown empty, since the handler
never posted a result on throw.

Evict a rejected ignore-controller init instead of caching it. The
in-flight promise is stored before initialize resolves, so a failure
stuck until 16 other directories displaced it; the old one-entry cache
only stored a controller after a successful init.
@sylwester-liljegren

Copy link
Copy Markdown
Contributor Author

All four review findings addressed in 3661761, plus the CI failure. Confirming each:

Multi-root folder cap applied before ranking — correct, and it was the same inversion this PR had already fixed for files. The primary root can return up to 50 folders on its own, so it consumed the entire allowance and every added folder was dropped before its entries competed. Folders are now capped after mergeFileSearchItems ranks them, via a helper that trims folder entries while leaving files and their order untouched. Regression test constructs exactly that shape: 60 primary folders against one exactly-matching folder in an added root, and asserts the added one survives.

One failing extra root drops the whole @ search — correct on both counts. gather now isolates per-root failures and yields nothing for a root it cannot read, and the workspace-folder lookup is guarded separately, so a result is always posted. Two tests cover it: an added folder whose ignore controller throws EACCES still returns the session's own files, and a throwing roots() still posts a result.

Ranking secondary hits on absolute paths skews fuzzy match quality — correct, and sharper than it first looks. rankOpen drops entries with no path match, so scoring the absolute form changed inclusion, not just order: an open tab in an added folder was surfaced whenever the query happened to match that folder's own path. Ranking now runs on the root-relative path while the absolute path remains what gets inserted, so the attachment boundary is unaffected. score takes an explicit basis, and the name-length, depth, and path-length tiebreaks all use it consistently. I verified the regression test fails against the previous implementation before keeping it.

A rejected ignore-controller init is cached forever — correct, and a regression I introduced: the old one-entry cache only stored a controller after a successful initialize(). The in-flight promise now removes itself from the map on rejection, guarded on identity so it cannot evict a newer entry for the same directory.

CI failure was separate and my miss: packages/opencode/test/kilocode/semantic-search.test.ts asserts semantic_search output, which this PR changes. My earlier grep for consumers hit a result limit and cut that file off, and I only ran the registry tests. Three assertions updated, and index status is now stubbed there so the wording no longer depends on real indexing progress — the failure output showed it picking up a genuine "still building (0%, 0/0 files)" state.

Local verification after the fixes: 270 extension unit tests across the 9 files covering every changed module, plus semantic-search, semantic-search-output, and tool-registry-semantic-import-failure on the CLI side; typecheck, lint, and knip clean in both packages. CI is green.

@marius-kilocode

Copy link
Copy Markdown
Collaborator

Unsure on the implications of this, all of this indexing has been historically tricky and hard to get right.

@sylwester-liljegren

Copy link
Copy Markdown
Contributor Author

@marius-kilocode My preliminary plan was to avoid touching on indexing parts, but my agent did push back and told me that if left unchanged, it would have caused confusion for Kilo Code when using the semantic_search tool in a multi-root workspace setting, hence the changes on those parts.

What if I split this PR into two minor PRs: One PR that actually does make the mentions search work appropriately in a multi-root workspace setting and the other PR focuses on implementing appropriate indexing management in a multi-root workspace setting. Because you seem to be more worried about the indexing parts and not much about the mentions part, am I right?

@marius-kilocode

Copy link
Copy Markdown
Collaborator

The problem is that you will need an index to keep @ fast or not? Right now that could be fine within the same vscode workspace as long as we don't have any performance/memory implications for users.

@sylwester-liljegren

Copy link
Copy Markdown
Contributor Author

@marius-kilocode Are you talking about indexing as in local codebase indexing / semantic_search or the index for searching files using "@"?

@marius-kilocode

Copy link
Copy Markdown
Collaborator

I am talking about [index for searching files using "@"]. I don't mean semantic search.

@marius-kilocode

Copy link
Copy Markdown
Collaborator

We build up a bunch of indexes that affect performance. The larger the scope of such an index get's the more likely this happens, especially since user repos tend to be very noisy on file level.

@sylwester-liljegren

Copy link
Copy Markdown
Contributor Author

Okay, now I get your point. I'll look into this later and see what options we have at our disposal to mitigate the risks of performance degradation.

@sylwester-liljegren

Copy link
Copy Markdown
Contributor Author

@marius-kilocode Could you look at #13592 first? If we can agree on a solution that is seemingly more performant over there, then we can take address this PR afterwards.

Sylwester Liljegren added 2 commits September 4, 2026 13:53
The @ menu was reworked upstream: mention entries are centralised behind
MentionEntry, and rankMentionResults now scores every offer — entries,
past chats and files — against one query on a single scale.

Resolved MentionResult by taking upstream's MentionEntry collapse and
re-applying the owning-folder field to the file variants.

That reranking reaches files this PR returns as absolute paths, and
labels() fed it the whole path, so the filesystem prefix could match a
username or a parent directory on every file under an added folder. This
is the same skew already fixed host-side in mergeFileSearchResults, and
the webview would have re-introduced it. FileSearchItem now carries the
path relative to its owning folder, and labels() scores that instead.
The absolute path is still what gets inserted, so the attachment
boundary is unchanged.

Equal scores keep the host's order because the sort is stable, so the
session's own project still wins ties.
Each workspace folder searched costs its own file index and watcher in
the backend, held for an hour, and repos tend to be noisy at file level.
Fanning out on the empty search that fills the initial dropdown meant
merely opening the menu paid for every folder, whether or not the user
went on to search across them.

A bare @ now searches the session's own project alone, exactly as before
this feature, and the other folders are searched from the first typed
character. Past chats already work this way, fetched on the first
character rather than on every @.

The number of added folders one query can search is also bounded, so an
unusually large workspace cannot grow the backend's index count without
limit.

Badges follow the shape of the workspace rather than what a particular
query happened to search, so rows do not sprout a badge on the first
keystroke.
@sylwester-liljegren

sylwester-liljegren commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

@marius-kilocode Merge conflicts resolved, and I've pushed two changes that address the index cost directly.

A bare @ no longer touches the other folders. It searches the session's own project only — exactly as before this PR — and the other workspace folders are searched from the first typed character. This is the same rule the past-chats list already follows (useFileMention.ts:661-663: "Only a typed query can match a chat title, so the list is fetched on the first character rather than on every bare @"). Opening the menu and moving on now costs nothing extra.

The number of folders one query can search is capped at four. Worth flagging explicitly: in a workspace with more than five folders, the extras beyond the cap aren't searched. That's a deliberate trade to bound the index count rather than an oversight — happy to change the number or the policy if you'd prefer a different one.

On what you'd still see: the first search in each added folder pays for that folder's index, then it's fast, with the backend holding it for the 60-minute idle TTL. So this changes when the cost is paid and bounds how many you can pay for, but it doesn't make the per-folder cost disappear. I don't want to claim more than that.

@sylwester-liljegren

Copy link
Copy Markdown
Contributor Author

Hi @marius-kilocode , what is the status with this PR? Do you desire any further changes or perhaps you see some other blockers that need to be addressed?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants