feat(web): add fuzzy matching for slash commands (#63)

Co-authored-by: Liheng <liheng@example.com>
This commit is contained in:
Lihengwannafly
2026-01-13 11:06:28 +08:00
committed by GitHub
co-authored by Liheng
parent 8ed94193d8
commit f6cf2ee49c
+43 -3
View File
@@ -5,6 +5,22 @@ import type { SlashCommand } from '@/types/api'
import type { Suggestion } from '@/hooks/useActiveSuggestions'
import { queryKeys } from '@/lib/query-keys'
function levenshteinDistance(a: string, b: string): number {
if (a.length === 0) return b.length
if (b.length === 0) return a.length
const matrix: number[][] = []
for (let i = 0; i <= b.length; i++) matrix[i] = [i]
for (let j = 0; j <= a.length; j++) matrix[0][j] = j
for (let i = 1; i <= b.length; i++) {
for (let j = 1; j <= a.length; j++) {
matrix[i][j] = b[i - 1] === a[j - 1]
? matrix[i - 1][j - 1]
: Math.min(matrix[i - 1][j - 1] + 1, matrix[i][j - 1] + 1, matrix[i - 1][j] + 1)
}
}
return matrix[b.length][a.length]
}
/**
* Built-in slash commands per agent type.
* These are shown immediately without waiting for RPC.
@@ -78,14 +94,38 @@ export function useSlashCommands(
}, [agentType, query.data])
const getSuggestions = useCallback(async (queryText: string): Promise<Suggestion[]> => {
// queryText will be like "/clea" - strip the leading slash
const searchTerm = queryText.startsWith('/')
? queryText.slice(1).toLowerCase()
: queryText.toLowerCase()
if (!searchTerm) {
return commands.map(cmd => ({
key: `/${cmd.name}`,
text: `/${cmd.name}`,
label: `/${cmd.name}`,
description: cmd.description ?? (cmd.source === 'user' ? 'Custom command' : undefined),
content: cmd.content,
source: cmd.source
}))
}
const maxDistance = Math.max(2, Math.floor(searchTerm.length / 2))
return commands
.filter(cmd => cmd.name.toLowerCase().startsWith(searchTerm))
.map(cmd => ({
.map(cmd => {
const name = cmd.name.toLowerCase()
let score: number
if (name === searchTerm) score = 0
else if (name.startsWith(searchTerm)) score = 1
else if (name.includes(searchTerm)) score = 2
else {
const dist = levenshteinDistance(searchTerm, name)
score = dist <= maxDistance ? 3 + dist : Infinity
}
return { cmd, score }
})
.filter(item => item.score < Infinity)
.sort((a, b) => a.score - b.score)
.map(({ cmd }) => ({
key: `/${cmd.name}`,
text: `/${cmd.name}`,
label: `/${cmd.name}`,