fix: preserve fuzzy ranking in model picker (#1198) (thanks @vignesh07)
This commit is contained in:
@@ -7,6 +7,7 @@ Docs: https://docs.clawd.bot
|
|||||||
### Changes
|
### Changes
|
||||||
- Dependencies: update core + plugin deps (grammy, vitest, openai, Microsoft agents hosting, etc.).
|
- Dependencies: update core + plugin deps (grammy, vitest, openai, Microsoft agents hosting, etc.).
|
||||||
- Onboarding: add allowlist prompts and username-to-id resolution across core and extension channels.
|
- Onboarding: add allowlist prompts and username-to-id resolution across core and extension channels.
|
||||||
|
- TUI: add searchable model picker for quicker model selection. (#1198) — thanks @vignesh07.
|
||||||
- Docs: clarify allowlist input types and onboarding behavior for messaging channels.
|
- Docs: clarify allowlist input types and onboarding behavior for messaging channels.
|
||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
|
|||||||
@@ -92,6 +92,21 @@ describe("SearchableSelectList", () => {
|
|||||||
expect(selected?.value).toContain("gpt");
|
expect(selected?.value).toContain("gpt");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("preserves fuzzy ranking when only fuzzy matches exist", () => {
|
||||||
|
const items = [
|
||||||
|
{ value: "xg---4", label: "xg---4", description: "Worse fuzzy match" },
|
||||||
|
{ value: "gpt-4", label: "gpt-4", description: "Better fuzzy match" },
|
||||||
|
];
|
||||||
|
const list = new SearchableSelectList(items, 5, mockTheme);
|
||||||
|
|
||||||
|
for (const ch of "g4") {
|
||||||
|
list.handleInput(ch);
|
||||||
|
}
|
||||||
|
|
||||||
|
const selected = list.getSelectedItem();
|
||||||
|
expect(selected?.value).toBe("gpt-4");
|
||||||
|
});
|
||||||
|
|
||||||
it("shows no match message when filter yields no results", () => {
|
it("shows no match message when filter yields no results", () => {
|
||||||
const list = new SearchableSelectList(testItems, 5, mockTheme);
|
const list = new SearchableSelectList(testItems, 5, mockTheme);
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ export class SearchableSelectList implements Component {
|
|||||||
private maxVisible: number;
|
private maxVisible: number;
|
||||||
private theme: SearchableSelectListTheme;
|
private theme: SearchableSelectListTheme;
|
||||||
private searchInput: Input;
|
private searchInput: Input;
|
||||||
private searchQuery = "";
|
|
||||||
|
|
||||||
onSelect?: (item: SelectItem) => void;
|
onSelect?: (item: SelectItem) => void;
|
||||||
onCancel?: () => void;
|
onCancel?: () => void;
|
||||||
@@ -41,7 +40,6 @@ export class SearchableSelectList implements Component {
|
|||||||
|
|
||||||
private updateFilter() {
|
private updateFilter() {
|
||||||
const query = this.searchInput.getValue().trim();
|
const query = this.searchInput.getValue().trim();
|
||||||
this.searchQuery = query;
|
|
||||||
|
|
||||||
if (!query) {
|
if (!query) {
|
||||||
this.filteredItems = this.items;
|
this.filteredItems = this.items;
|
||||||
@@ -63,45 +61,45 @@ export class SearchableSelectList implements Component {
|
|||||||
*/
|
*/
|
||||||
private smartFilter(query: string): SelectItem[] {
|
private smartFilter(query: string): SelectItem[] {
|
||||||
const q = query.toLowerCase();
|
const q = query.toLowerCase();
|
||||||
|
|
||||||
type ScoredItem = { item: SelectItem; score: number };
|
type ScoredItem = { item: SelectItem; score: number };
|
||||||
const scored: ScoredItem[] = [];
|
const exactLabel: ScoredItem[] = [];
|
||||||
|
const wordBoundary: SelectItem[] = [];
|
||||||
|
const descriptionMatches: SelectItem[] = [];
|
||||||
|
const fuzzyCandidates: SelectItem[] = [];
|
||||||
|
|
||||||
for (const item of this.items) {
|
for (const item of this.items) {
|
||||||
const label = item.label.toLowerCase();
|
const label = item.label.toLowerCase();
|
||||||
const desc = (item.description ?? "").toLowerCase();
|
const desc = (item.description ?? "").toLowerCase();
|
||||||
let score = Infinity;
|
|
||||||
|
|
||||||
// Tier 1: Exact substring in label (score 0-99)
|
// Tier 1: Exact substring in label (score 0-99)
|
||||||
const labelIndex = label.indexOf(q);
|
const labelIndex = label.indexOf(q);
|
||||||
if (labelIndex !== -1) {
|
if (labelIndex !== -1) {
|
||||||
// Earlier match = better score
|
// Earlier match = better score
|
||||||
score = labelIndex;
|
exactLabel.push({ item, score: labelIndex });
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
// Tier 2: Word-boundary prefix in label (score 100-199)
|
// Tier 2: Word-boundary prefix in label (score 100-199)
|
||||||
else if (this.matchesWordBoundary(label, q)) {
|
if (this.matchesWordBoundary(label, q)) {
|
||||||
score = 100;
|
wordBoundary.push(item);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
// Tier 3: Exact substring in description (score 200-299)
|
// Tier 3: Exact substring in description (score 200-299)
|
||||||
else if (desc.indexOf(q) !== -1) {
|
if (desc.indexOf(q) !== -1) {
|
||||||
score = 200;
|
descriptionMatches.push(item);
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
// Tier 4: Fuzzy match (score 300+)
|
// Tier 4: Fuzzy match (score 300+)
|
||||||
else {
|
fuzzyCandidates.push(item);
|
||||||
const fuzzyResult = fuzzyFilter([item], query, (i) => `${i.label} ${i.description ?? ""}`);
|
|
||||||
if (fuzzyResult.length > 0) {
|
|
||||||
score = 300;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (score !== Infinity) {
|
|
||||||
scored.push({ item, score });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort by score (lower = better)
|
exactLabel.sort((a, b) => a.score - b.score);
|
||||||
scored.sort((a, b) => a.score - b.score);
|
const fuzzyMatches = fuzzyFilter(fuzzyCandidates, query, (i) => `${i.label} ${i.description ?? ""}`);
|
||||||
return scored.map((s) => s.item);
|
return [
|
||||||
|
...exactLabel.map((s) => s.item),
|
||||||
|
...wordBoundary,
|
||||||
|
...descriptionMatches,
|
||||||
|
...fuzzyMatches,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -130,7 +128,8 @@ export class SearchableSelectList implements Component {
|
|||||||
|
|
||||||
// Search input line
|
// Search input line
|
||||||
const prompt = this.theme.searchPrompt("search: ");
|
const prompt = this.theme.searchPrompt("search: ");
|
||||||
const inputLines = this.searchInput.render(width - 8);
|
const inputWidth = Math.max(1, width - 8);
|
||||||
|
const inputLines = this.searchInput.render(inputWidth);
|
||||||
const inputText = inputLines[0] ?? "";
|
const inputText = inputLines[0] ?? "";
|
||||||
lines.push(`${prompt}${this.theme.searchInput(inputText)}`);
|
lines.push(`${prompt}${this.theme.searchInput(inputText)}`);
|
||||||
lines.push(""); // Spacer
|
lines.push(""); // Spacer
|
||||||
|
|||||||
Reference in New Issue
Block a user