mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-16 04:01:56 +00:00
- Skills registered as /skill:name commands for quick access - Toggle via /settings or skills.enableSkillCommands in settings.json - Fuzzy matching for all slash command autocomplete (type /skbra for /skill:brave-search) - Moved fuzzy module from coding-agent to tui package for reuse Closes #630 by @Dwsy (reimplemented with fixes)
107 lines
2.3 KiB
TypeScript
107 lines
2.3 KiB
TypeScript
/**
|
|
* Fuzzy matching utilities.
|
|
* Matches if all query characters appear in order (not necessarily consecutive).
|
|
* Lower score = better match.
|
|
*/
|
|
|
|
export interface FuzzyMatch {
|
|
matches: boolean;
|
|
score: number;
|
|
}
|
|
|
|
export function fuzzyMatch(query: string, text: string): FuzzyMatch {
|
|
const queryLower = query.toLowerCase();
|
|
const textLower = text.toLowerCase();
|
|
|
|
if (queryLower.length === 0) {
|
|
return { matches: true, score: 0 };
|
|
}
|
|
|
|
if (queryLower.length > textLower.length) {
|
|
return { matches: false, score: 0 };
|
|
}
|
|
|
|
let queryIndex = 0;
|
|
let score = 0;
|
|
let lastMatchIndex = -1;
|
|
let consecutiveMatches = 0;
|
|
|
|
for (let i = 0; i < textLower.length && queryIndex < queryLower.length; i++) {
|
|
if (textLower[i] === queryLower[queryIndex]) {
|
|
const isWordBoundary = i === 0 || /[\s\-_./:]/.test(textLower[i - 1]!);
|
|
|
|
// Reward consecutive matches
|
|
if (lastMatchIndex === i - 1) {
|
|
consecutiveMatches++;
|
|
score -= consecutiveMatches * 5;
|
|
} else {
|
|
consecutiveMatches = 0;
|
|
// Penalize gaps
|
|
if (lastMatchIndex >= 0) {
|
|
score += (i - lastMatchIndex - 1) * 2;
|
|
}
|
|
}
|
|
|
|
// Reward word boundary matches
|
|
if (isWordBoundary) {
|
|
score -= 10;
|
|
}
|
|
|
|
// Slight penalty for later matches
|
|
score += i * 0.1;
|
|
|
|
lastMatchIndex = i;
|
|
queryIndex++;
|
|
}
|
|
}
|
|
|
|
if (queryIndex < queryLower.length) {
|
|
return { matches: false, score: 0 };
|
|
}
|
|
|
|
return { matches: true, score };
|
|
}
|
|
|
|
/**
|
|
* Filter and sort items by fuzzy match quality (best matches first).
|
|
* Supports space-separated tokens: all tokens must match.
|
|
*/
|
|
export function fuzzyFilter<T>(items: T[], query: string, getText: (item: T) => string): T[] {
|
|
if (!query.trim()) {
|
|
return items;
|
|
}
|
|
|
|
const tokens = query
|
|
.trim()
|
|
.split(/\s+/)
|
|
.filter((t) => t.length > 0);
|
|
|
|
if (tokens.length === 0) {
|
|
return items;
|
|
}
|
|
|
|
const results: { item: T; totalScore: number }[] = [];
|
|
|
|
for (const item of items) {
|
|
const text = getText(item);
|
|
let totalScore = 0;
|
|
let allMatch = true;
|
|
|
|
for (const token of tokens) {
|
|
const match = fuzzyMatch(token, text);
|
|
if (match.matches) {
|
|
totalScore += match.score;
|
|
} else {
|
|
allMatch = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (allMatch) {
|
|
results.push({ item, totalScore });
|
|
}
|
|
}
|
|
|
|
results.sort((a, b) => a.totalScore - b.totalScore);
|
|
return results.map((r) => r.item);
|
|
}
|