diff --git a/.changeset/slash-command-sorting.md b/.changeset/slash-command-sorting.md new file mode 100644 index 00000000000..bda54cfc00f --- /dev/null +++ b/.changeset/slash-command-sorting.md @@ -0,0 +1,4 @@ +--- +"kilo-code": patch +--- +Sort slash-command dropdown results by relevance. Exact matches now appear first, followed by prefix matches, then substring matches. diff --git a/packages/kilo-vscode/tests/unit/use-slash-command-sorting.test.ts b/packages/kilo-vscode/tests/unit/use-slash-command-sorting.test.ts new file mode 100644 index 00000000000..9082a2a421b --- /dev/null +++ b/packages/kilo-vscode/tests/unit/use-slash-command-sorting.test.ts @@ -0,0 +1,50 @@ +import { describe, it, expect } from "bun:test" +import { sortByScore } from "../../webview-ui/src/hooks/useSlashCommand" + +describe("sortByScore", () => { + it("prefers exact matches over prefix matches", () => { + const commands = [ + { name: "commit-all", description: "", hints: [] }, + { name: "commit", description: "", hints: [] }, + ] + const result = sortByScore(commands, "commit") + expect(result.map((c) => c.name)).toEqual(["commit", "commit-all"]) + }) + + it("prefers prefix matches over substring matches", () => { + const commands = [ + { name: "compact", description: "", hints: [] }, + { name: "commit", description: "", hints: [] }, + { name: "telecompact", description: "", hints: [] }, + { name: "mycommittool", description: "", hints: [] }, + ] + const result = sortByScore(commands, "co") + const names = result.map((c) => c.name) + expect(names.indexOf("compact")).toBeLessThan(names.indexOf("telecompact")) + expect(names.indexOf("commit")).toBeLessThan(names.indexOf("mycommittool")) + }) + + it("matches descriptions", () => { + const commands = [ + { name: "other", description: "", hints: [] }, + { name: "help", description: "Open documentation", hints: [] }, + ] + const result = sortByScore(commands, "documentation") + expect(result[0]?.name).toBe("help") + }) + + it("matches hints", () => { + const commands = [ + { name: "other", description: "", hints: [] }, + { name: "compact", description: "", hints: ["smol"] }, + ] + const result = sortByScore(commands, "smol") + expect(result[0]?.name).toBe("compact") + }) + + it("is case insensitive", () => { + const commands = [{ name: "compact", description: "", hints: [] }] + const result = sortByScore(commands, "COMPACT") + expect(result[0]?.name).toBe("compact") + }) +}) diff --git a/packages/kilo-vscode/webview-ui/src/hooks/useSlashCommand.ts b/packages/kilo-vscode/webview-ui/src/hooks/useSlashCommand.ts index 31bfef0766f..7cdaaf150c3 100644 --- a/packages/kilo-vscode/webview-ui/src/hooks/useSlashCommand.ts +++ b/packages/kilo-vscode/webview-ui/src/hooks/useSlashCommand.ts @@ -4,6 +4,21 @@ import type { SlashCommandInfo, WebviewMessage, ExtensionMessage } from "../type export const SLASH_PATTERN = /^\/(\S*)$/ +function getMatchScore(cmd: SlashCommandEntry, lower: string): number { + const name = cmd.name.toLowerCase() + if (name === lower) return 3 + if (name.startsWith(lower)) return 2 + if (name.includes(lower)) return 1 + if (cmd.description?.toLowerCase().includes(lower)) return 1 + if (cmd.hints.some((h) => h.toLowerCase().includes(lower))) return 1 + return 0 +} + +export function sortByScore(matches: SlashCommandEntry[], query: string): SlashCommandEntry[] { + const lower = query.toLowerCase() + return [...matches].sort((a, b) => getMatchScore(b, lower) - getMatchScore(a, lower)) +} + interface VSCodeContext { postMessage: (message: WebviewMessage) => void onMessage: (handler: (message: ExtensionMessage) => void) => () => void @@ -158,12 +173,13 @@ export function useSlashCommand(vscode: VSCodeContext, exclude?: Set | A const all = commands() if (!q) return all const lower = q.toLowerCase() - return all.filter( + const matches = all.filter( (cmd) => cmd.name.toLowerCase().includes(lower) || cmd.description?.toLowerCase().includes(lower) || cmd.hints.some((h) => h.toLowerCase().includes(lower)), ) + return sortByScore(matches, lower) } const unsubscribe = vscode.onMessage((message) => {