Skip to content
Source
Sign inSign up

composerSlashCommandSearch.ts

116 lines3.0 KB

1import {
2 insertRankedSearchResult,
3 normalizeSearchQuery,
4 scoreQueryMatch,
5} from "@t3tools/shared/searchRanking";
6​
7import type { ComposerCommandItem } from "./ComposerCommandMenu";
8import { scoreProviderSkill } from "../../providerSkillSearch";
9​
10type SlashSearchItem = Extract<
11 ComposerCommandItem,
12 { type: "slash-command" | "provider-slash-command" | "skill" }
13>;
14​
15/** Provider commands, `/side`, `/btw` and `/clear` work only at the message start. */
16export function slashCommandItemsForPromptPosition(
17 items: ReadonlyArray<SlashSearchItem>,
18 isAtPromptStart: boolean,
19): SlashSearchItem[] {
20 if (isAtPromptStart) {
21 return [...items];
22 }
23 return items.filter(
24 (item) =>
25 item.type === "skill" ||
26 (item.type === "slash-command" &&
27 item.command !== "side" &&
28 item.command !== "btw" &&
29 item.command !== "clear"),
30 );
32​
33function scoreSlashCommandItem(item: SlashSearchItem, query: string): number | null {
34 if (item.type === "skill") {
35 if (query === "skill") {
36 return 0;
37 }
38 const skillQuery = query.startsWith("skill:") ? query.slice("skill:".length) : query;
39 const skillScore = skillQuery ? scoreProviderSkill(item.skill, skillQuery) : 0;
40 if (skillScore !== null) {
41 return skillScore;
42 }
43 return "skill".startsWith(query) ? Number.MAX_SAFE_INTEGER : null;
44 }
45​
46 const primaryValue =
47 item.type === "slash-command" ? item.command.toLowerCase() : item.command.name.toLowerCase();
48 const description = item.description.toLowerCase();
49​
50 const scores = [
51 scoreQueryMatch({
52 value: primaryValue,
53 query,
54 exactBase: 0,
55 prefixBase: 2,
56 boundaryBase: 4,
57 includesBase: 6,
58 fuzzyBase: 100,
59 boundaryMarkers: ["-", "_", "/"],
60 }),
61 scoreQueryMatch({
62 value: description,
63 query,
64 exactBase: 20,
65 prefixBase: 22,
66 boundaryBase: 24,
67 includesBase: 26,
68 }),
69 ].filter((score): score is number => score !== null);
70​
71 if (scores.length === 0) {
72 return null;
73 }
74​
75 return Math.min(...scores);
77​
78export function searchSlashCommandItems(
79 items: ReadonlyArray<SlashSearchItem>,
80 query: string,
81): SlashSearchItem[] {
82 const normalizedQuery = normalizeSearchQuery(query, { trimLeadingPattern: /^\/+/ });
83 if (!normalizedQuery) {
84 return [...items];
85 }
86​
87 const ranked: Array<{
88 item: SlashSearchItem;
89 score: number;
90 tieBreaker: string;
91 }> = [];
92​
93 for (const item of items) {
94 const score = scoreSlashCommandItem(item, normalizedQuery);
95 if (score === null) {
96 continue;
97 }
98​
99 insertRankedSearchResult(
100 ranked,
101 {
102 item,
103 score,
104 tieBreaker:
105 item.type === "slash-command"
106 ? `0\u0000${item.command}`
107 : item.type === "provider-slash-command"
108 ? `1\u0000${item.command.name}\u0000${item.provider}`
109 : `2\u0000${item.skill.name}\u0000${item.provider}`,
110 },
111 Number.POSITIVE_INFINITY,
112 );
113 }
114​
115 return ranked.map((entry) => entry.item);