1export interface CommandPaletteItem {
2 readonly key: string;
3 readonly kind: "action" | "project" | "thread";
4 readonly title: string;
5 readonly detail?: string;
6 readonly searchTerms: ReadonlyArray<string>;
7 readonly run: () => void;
8}
9
10/** `>` narrows to actions, matching the desktop palette. Stable ties retain recent-thread order. */
11export function filterCommandPaletteItems(
12 items: ReadonlyArray<CommandPaletteItem>,
13 query: string,
14 matchedThreadKeys: ReadonlySet<string>,
15) {
16 const actionsOnly = query.startsWith(">");
17 const normalized = (actionsOnly ? query.slice(1) : query).trim().toLocaleLowerCase();
18 const tokens = normalized.split(/\s+/);
19 return items
20 .flatMap((item, index) => {
21 if (actionsOnly && item.kind !== "action") return [];
22 if (!normalized) return item.kind === "project" ? [] : [{ item, rank: 0, index }];
23 const title = item.title.toLocaleLowerCase();
24 const haystack = [title, ...item.searchTerms].join(" ").toLocaleLowerCase();
25 if (
26 !tokens.every((token) => haystack.includes(token)) &&
27 !(item.kind === "thread" && matchedThreadKeys.has(item.key))
28 )
29 return [];
30 const rank =
31 title === normalized
32 ? 3
33 : title.startsWith(normalized)
34 ? 2
35 : title.includes(normalized)
36 ? 1
37 : 0;
38 return [{ item, rank, index }];
39 })
40 .sort((left, right) => right.rank - left.rank || left.index - right.index)
41 .map(({ item }) => item);
42}
43
44export function nextPaletteIndex(index: number, direction: -1 | 1, count: number) {
45 return count === 0 ? 0 : (index + direction + count) % count;
46}