Repository navigation
feat(cli): report what semantic_search actually searched #14335
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
marius-kilocode
merged 2 commits into
Kilo-Org:main
from
sylwester-liljegren:feat/semantic-search-scope-reporting
Sep 21, 2026
Merged
Changes from 1 commit
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@kilocode/cli": patch | ||
| --- | ||
|
|
||
| Report what `semantic_search` actually covered. It names the indexed root it searched, and an empty result now says whether the index was complete, still building, disabled, or failed, so a miss is no longer mistaken for code that does not exist. |
55 changes: 55 additions & 0 deletions
55
packages/opencode/src/kilocode/tool/semantic-search-output.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| import type { IndexingStatus } from "@kilocode/kilo-indexing/status" | ||
|
|
||
| /** | ||
| * Output text for `semantic_search`. | ||
| * | ||
| * Split from the tool so it can be exercised without booting the indexing | ||
| * worker: the wording is the whole point of the behavior, not an incidental | ||
| * detail of it. | ||
| */ | ||
|
|
||
| export function normalizePath(value: string): string { | ||
| return value.replaceAll("\\", "/") | ||
| } | ||
|
|
||
| /** Human-readable description of what was actually searched. */ | ||
| export function scope(root: string, prefix?: string): string { | ||
| return prefix ? `${root}/${normalizePath(prefix)}` : root | ||
| } | ||
|
|
||
| /** | ||
| * Explain an empty result set in terms of index state. | ||
| * | ||
| * `KiloIndexing.search` returns `[]` when the index is disabled, unbuilt, or | ||
| * broken, which is indistinguishable from a genuine miss. Left unexplained, a | ||
| * model reads "no results" as "this code does not exist" and acts on it. | ||
| */ | ||
| export function reason(status?: IndexingStatus): string { | ||
| if (!status) return "The index could not be queried, so this is not evidence that no matching code exists." | ||
| const detail = status.message.trim() | ||
| const suffix = detail ? ` ${detail}` : "" | ||
| if (status.state === "Disabled") { | ||
| return `Codebase indexing is disabled for this project, so nothing was searched.${suffix}` | ||
| } | ||
| if (status.state === "Error") return `Codebase indexing failed, so nothing was searched.${suffix}` | ||
| if (status.state === "In Progress") { | ||
| return `The index is still building (${status.percent}%, ${status.processedFiles}/${status.totalFiles} files), so results are incomplete.` | ||
| } | ||
| if (status.state === "Standby") return `The index is not active, so results are incomplete.${suffix}` | ||
| return "The index is up to date, so no semantically similar code exists in this scope." | ||
| } | ||
|
|
||
| /** | ||
| * Full output for a search that matched nothing. | ||
| * | ||
| * Always names the indexed root: only one root is indexed, but files from other | ||
| * editor workspace folders are mentionable, so the caller needs to know that a | ||
| * miss here does not cover them. | ||
| */ | ||
| export function empty(query: string, root: string, prefix?: string, status?: IndexingStatus): string { | ||
| return [ | ||
| `No results for "${query}" in ${scope(root, prefix)}.`, | ||
| reason(status), | ||
| `Only ${root} is indexed. Files in other workspace folders are not searchable here — use Read with an absolute path.`, | ||
| ].join("\n") | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
70 changes: 70 additions & 0 deletions
70
packages/opencode/test/kilocode/tool/semantic-search-output.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,70 @@ | ||
| import { describe, expect, test } from "bun:test" | ||
| import type { IndexingStatus } from "@kilocode/kilo-indexing/status" | ||
| import { empty, reason, scope } from "@/kilocode/tool/semantic-search-output" | ||
|
|
||
| function status(over: Partial<IndexingStatus>): IndexingStatus { | ||
| return { state: "Complete", message: "", processedFiles: 0, totalFiles: 0, percent: 0, ...over } | ||
| } | ||
|
|
||
| describe("scope", () => { | ||
| test("names the root when the whole index was searched", () => { | ||
| expect(scope("/repo")).toBe("/repo") | ||
| }) | ||
|
|
||
| test("names the subdirectory when the search was narrowed", () => { | ||
| expect(scope("/repo", "src/app")).toBe("/repo/src/app") | ||
| }) | ||
|
|
||
| test("reports a Windows prefix with forward slashes", () => { | ||
| expect(scope("C:/repo", "src\\app")).toBe("C:/repo/src/app") | ||
| }) | ||
| }) | ||
|
|
||
| describe("reason", () => { | ||
| test("distinguishes a complete index from an unavailable one", () => { | ||
| expect(reason(status({ state: "Complete" }))).toContain("up to date") | ||
| expect(reason(undefined)).toContain("not evidence") | ||
| }) | ||
|
|
||
| test("reports progress while the index is still building", () => { | ||
| const text = reason(status({ state: "In Progress", percent: 40, processedFiles: 120, totalFiles: 300 })) | ||
| expect(text).toContain("still building (40%, 120/300 files)") | ||
| expect(text).toContain("incomplete") | ||
| }) | ||
|
|
||
| test("says nothing was searched when indexing is disabled", () => { | ||
| const text = reason(status({ state: "Disabled", message: "Enable it in Kilo Settings." })) | ||
| expect(text).toContain("disabled for this project") | ||
| expect(text).toContain("nothing was searched") | ||
| expect(text).toContain("Enable it in Kilo Settings.") | ||
| }) | ||
|
|
||
| test("says nothing was searched when indexing failed", () => { | ||
| expect(reason(status({ state: "Error", message: "Failed to initialize: bad model" }))).toContain( | ||
| "Codebase indexing failed, so nothing was searched. Failed to initialize: bad model", | ||
| ) | ||
| }) | ||
|
|
||
| test("omits an empty status message rather than leaving trailing space", () => { | ||
| expect(reason(status({ state: "Disabled", message: " " }))).toBe( | ||
| "Codebase indexing is disabled for this project, so nothing was searched.", | ||
| ) | ||
| }) | ||
| }) | ||
|
|
||
| describe("empty", () => { | ||
| test("states the query, the searched scope, the index state, and the multi-root caveat", () => { | ||
| const text = empty("auth flow", "/repo", undefined, status({ state: "Complete" })) | ||
| expect(text.split("\n")).toEqual([ | ||
| 'No results for "auth flow" in /repo.', | ||
| "The index is up to date, so no semantically similar code exists in this scope.", | ||
| "Only /repo is indexed. Files in other workspace folders are not searchable here — use Read with an absolute path.", | ||
| ]) | ||
| }) | ||
|
|
||
| test("does not claim the code is absent when the index never ran", () => { | ||
| const text = empty("auth flow", "/repo", undefined, status({ state: "Disabled" })) | ||
| expect(text).not.toContain("no semantically similar code exists") | ||
| expect(text).toContain("nothing was searched") | ||
| }) | ||
| }) |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
WARNING: This states that
GrepandGlob"cannot reach outside" the indexed root, but that appears inaccurate. Both tools accept absolute paths and delegate toassertExternalDirectoryEffect(packages/opencode/src/tool/grep.ts:60,packages/opencode/src/tool/glob.ts:65), which allows paths outside the root after anexternal_directoryprompt — and the default action when no rule matches isask(packages/opencode/src/permission/index.ts:107).glob.tseven has explicit Kilo support for absolute patterns such as~/.config/kilo/command/*.md. Bounded-by-default is the useful point; "cannot reach outside" is stronger than the implementation. Consider rewording to something like "they search the indexed root by default and needexternal_directoryapproval to go outside it" so the model does not avoid a working approach.Reply with
@kilocode-bot fix itto have Kilo Code address this issue.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good catch — you're right, and I've fixed it in 8b7b8a5.
I had verified this earlier against an older revision where both tools resolved a base and then hard-failed with
Path escapes the active Location. That is no longer how they work:glob.ts:65andgrep.ts:60now callassertExternalDirectoryEffect, which returnsfalsewhen the target is inside the instance directory or worktree and otherwise raises anexternal_directoryprompt viactx.ask(external-directory.ts:36-54). It permits the path once approved rather than refusing it, andglob.tscarries an explicit Kilo change for absolute patterns.So "cannot reach outside" was wrong, and wrong in the direction that matters — it would steer a model away from an approach that works.
Reworded to keep the useful distinction rather than drop it, since
semantic_searchgenuinely cannot go outside its indexed root:I applied the same correction to the constraint line and to the runtime empty-result message, which carried the same "use Read with an absolute path" advice. Tests updated for the new wording and passing.