feat(palette): unify quick open into command palette with multi-source search

Merge file picker into command palette. Single Cmd+P entry searches
files, sessions, settings pages and commands; groups re-order by best
fuzzy score per source. Sessions show branch labels; git status is
lazily fetched for all session directories on open.

Drop QuickOpenDialog and Cmd+K shortcut.
This commit is contained in:
Bohdan Triapitsyn
2026-04-30 18:37:37 +03:00
parent 57eec76a7b
commit 24533dfe32
23 changed files with 550 additions and 882 deletions
+61
View File
@@ -107,6 +107,67 @@ export function filterByFuzzyQuery<T>(
return matching;
}
/**
* Score-sorted fuzzy ranking. Strict (low threshold), prioritizes substring
* matches (especially prefix matches), and returns the top N.
*
* Use this for command-palette-style result ranking where order matters more
* than recall.
*/
export function scoreByFuzzyQuery<T>(
items: T[],
query: string,
getText: (item: T) => string,
options?: { limit?: number; threshold?: number; noFuzzy?: boolean },
): { item: T; score: number }[] {
if (!query) return items.map((item) => ({ item, score: 0 }));
const limit = options?.limit ?? items.length;
const threshold = options?.threshold ?? 0.3;
const noFuzzy = options?.noFuzzy ?? false;
const queryLower = query.toLowerCase();
const scored: { item: T; score: number }[] = [];
const fuzzyCandidates: { item: T; text: string }[] = [];
for (const item of items) {
const text = getText(item);
if (!text) continue;
const lower = text.toLowerCase();
const idx = lower.indexOf(queryLower);
if (idx === 0) {
scored.push({ item, score: -1 });
continue;
}
if (idx > 0) {
scored.push({ item, score: idx / 1000 });
continue;
}
if (!noFuzzy) fuzzyCandidates.push({ item, text });
}
if (fuzzyCandidates.length > 0) {
const fuse = new Fuse(
fuzzyCandidates.map((c) => c.text),
{ threshold, ignoreLocation: true, distance: 100, includeScore: true, minMatchCharLength: 2 },
);
for (const result of fuse.search(query)) {
scored.push({ item: fuzzyCandidates[result.refIndex].item, score: result.score ?? 1 });
}
}
scored.sort((a, b) => a.score - b.score);
return scored.slice(0, limit);
}
export function rankByFuzzyQuery<T>(
items: T[],
query: string,
getText: (item: T) => string,
options?: { limit?: number; threshold?: number; noFuzzy?: boolean },
): T[] {
return scoreByFuzzyQuery(items, query, getText, options).map((x) => x.item);
}
export function partitionByFuzzyQuery<T>(
items: T[],
query: string,