import { insertRankedSearchResult, normalizeSearchQuery, scoreQueryMatch,} from "@t3tools/shared/searchRanking"; import type { ComposerCommandItem } from "./ComposerCommandMenu";import { scoreProviderSkill } from "../../providerSkillSearch"; type SlashSearchItem = Extract< ComposerCommandItem, { type: "slash-command" | "provider-slash-command" | "skill" }>; /** * A provider expands a slash command only when it opens the whole message; * anywhere else it reaches the agent as literal text, so it is not offered * there. Built-ins apply locally on selection and skills insert a `$` mention * the server dispatches from any position, so both stay available. */export function slashCommandItemsForPromptPosition( items: ReadonlyArray<SlashSearchItem>, isAtPromptStart: boolean,): SlashSearchItem[] { if (isAtPromptStart) { return [...items]; } return items.filter((item) => item.type !== "provider-slash-command");} function scoreSlashCommandItem(item: SlashSearchItem, query: string): number | null { if (item.type === "skill") { if (query === "skill") { return 0; } const skillQuery = query.startsWith("skill:") ? query.slice("skill:".length) : query; const skillScore = skillQuery ? scoreProviderSkill(item.skill, skillQuery) : 0; if (skillScore !== null) { return skillScore; } return "skill".startsWith(query) ? Number.MAX_SAFE_INTEGER : null; } const primaryValue = item.type === "slash-command" ? item.command.toLowerCase() : item.command.name.toLowerCase(); const description = item.description.toLowerCase(); const scores = [ scoreQueryMatch({ value: primaryValue, query, exactBase: 0, prefixBase: 2, boundaryBase: 4, includesBase: 6, fuzzyBase: 100, boundaryMarkers: ["-", "_", "/"], }), scoreQueryMatch({ value: description, query, exactBase: 20, prefixBase: 22, boundaryBase: 24, includesBase: 26, }), ].filter((score): score is number => score !== null); if (scores.length === 0) { return null; } return Math.min(...scores);} export function searchSlashCommandItems( items: ReadonlyArray<SlashSearchItem>, query: string,): SlashSearchItem[] { const normalizedQuery = normalizeSearchQuery(query, { trimLeadingPattern: /^\/+/ }); if (!normalizedQuery) { return [...items]; } const ranked: Array<{ item: SlashSearchItem; score: number; tieBreaker: string; }> = []; for (const item of items) { const score = scoreSlashCommandItem(item, normalizedQuery); if (score === null) { continue; } insertRankedSearchResult( ranked, { item, score, tieBreaker: item.type === "slash-command" ? `0\u0000${item.command}` : item.type === "provider-slash-command" ? `1\u0000${item.command.name}\u0000${item.provider}` : `2\u0000${item.skill.name}\u0000${item.provider}`, }, Number.POSITIVE_INFINITY, ); } return ranked.map((entry) => entry.item);}
import {
insertRankedSearchResult,
normalizeSearchQuery,
scoreQueryMatch,
} from "@t3tools/shared/searchRanking";
import type { ComposerCommandItem } from "./ComposerCommandMenu";
import { scoreProviderSkill } from "../../providerSkillSearch";
type SlashSearchItem = Extract<
ComposerCommandItem,
{ type: "slash-command" | "provider-slash-command" | "skill" }
>;
/**
* A provider expands a slash command only when it opens the whole message;
* anywhere else it reaches the agent as literal text, so it is not offered
* there. Built-ins apply locally on selection and skills insert a `$` mention
* the server dispatches from any position, so both stay available.
*/
export function slashCommandItemsForPromptPosition(
items: ReadonlyArray<SlashSearchItem>,
isAtPromptStart: boolean,
): SlashSearchItem[] {
if (isAtPromptStart) {
return [...items];
}
return items.filter((item) => item.type !== "provider-slash-command");
}
function scoreSlashCommandItem(item: SlashSearchItem, query: string): number | null {
if (item.type === "skill") {
if (query === "skill") {
return 0;
}
const skillQuery = query.startsWith("skill:") ? query.slice("skill:".length) : query;
const skillScore = skillQuery ? scoreProviderSkill(item.skill, skillQuery) : 0;
if (skillScore !== null) {
return skillScore;
}
return "skill".startsWith(query) ? Number.MAX_SAFE_INTEGER : null;
}
const primaryValue =
item.type === "slash-command" ? item.command.toLowerCase() : item.command.name.toLowerCase();
const description = item.description.toLowerCase();
const scores = [
scoreQueryMatch({
value: primaryValue,
query,
exactBase: 0,
prefixBase: 2,
boundaryBase: 4,
includesBase: 6,
fuzzyBase: 100,
boundaryMarkers: ["-", "_", "/"],
}),
scoreQueryMatch({
value: description,
query,
exactBase: 20,
prefixBase: 22,
boundaryBase: 24,
includesBase: 26,
}),
].filter((score): score is number => score !== null);
if (scores.length === 0) {
return null;
}
return Math.min(...scores);
}
export function searchSlashCommandItems(
items: ReadonlyArray<SlashSearchItem>,
query: string,
): SlashSearchItem[] {
const normalizedQuery = normalizeSearchQuery(query, { trimLeadingPattern: /^\/+/ });
if (!normalizedQuery) {
return [...items];
}
const ranked: Array<{
item: SlashSearchItem;
score: number;
tieBreaker: string;
}> = [];
for (const item of items) {
const score = scoreSlashCommandItem(item, normalizedQuery);
if (score === null) {
continue;
}
insertRankedSearchResult(
ranked,
{
item,
score,
tieBreaker:
item.type === "slash-command"
? `0\u0000${item.command}`
: item.type === "provider-slash-command"
? `1\u0000${item.command.name}\u0000${item.provider}`
: `2\u0000${item.skill.name}\u0000${item.provider}`,
},
Number.POSITIVE_INFINITY,
);
}
return ranked.map((entry) => entry.item);
}