Skip to content
4 changes: 4 additions & 0 deletions .changeset/slash-command-sorting.md
Original file line number Diff line number Diff line change
@@ -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.
50 changes: 50 additions & 0 deletions packages/kilo-vscode/tests/unit/use-slash-command-sorting.test.ts
Original file line number Diff line number Diff line change
@@ -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")
})
})
18 changes: 17 additions & 1 deletion packages/kilo-vscode/webview-ui/src/hooks/useSlashCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -158,12 +173,13 @@ export function useSlashCommand(vscode: VSCodeContext, exclude?: Set<string> | 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) => {
Expand Down
Loading