import type { CommandMode, WorkPackage } from "./work-package-types"; export type ComposerPackageSuggestion = { kind: "package"; id: string; title: string; shortName: string; }; export type ComposerCommandSuggestion = { kind: "command"; mode: CommandMode; label: string; description: string; }; export type ComposerSuggestion = | ComposerPackageSuggestion | ComposerCommandSuggestion; const AT_TRIGGER = /(^|\s)@([A-Za-z\s-]*)$/; const SLASH_TRIGGER = /(^|\s)\/([A-Za-z-]*)$/i; const COMMAND_SUGGESTIONS: ComposerCommandSuggestion[] = [ { kind: "command", mode: "ask", label: "/ask", description: "Ask about the current package context", }, { kind: "command", mode: "plan", label: "/plan", description: "Break a package into actionable work", }, { kind: "command", mode: "change", label: "/change", description: "Request updates to the current package", }, { kind: "command", mode: "execute", label: "/execute", description: "Run the current package flow", }, ]; function normalize(value: string) { return value.trim().replace(/\s+/g, " ").toLowerCase(); } function packageScore(workPackage: WorkPackage, query: string) { const title = normalize(workPackage.title); const shortName = normalize(workPackage.shortName); if (!query) { return 1; } return ( Number(shortName.startsWith(query)) * 4 + Number(title.startsWith(query)) * 3 + Number(shortName.includes(query)) * 2 + Number(title.includes(query)) ); } export function getComposerSuggestions( draft: string, workPackages: WorkPackage[], ): ComposerSuggestion[] { const atMatch = draft.match(AT_TRIGGER); if (atMatch) { const query = normalize(atMatch[2] ?? ""); return workPackages .map((workPackage) => ({ kind: "package" as const, id: workPackage.id, title: workPackage.title, shortName: workPackage.shortName, score: packageScore(workPackage, query), })) .filter((suggestion) => suggestion.score > 0) .sort((left, right) => right.score - left.score) .slice(0, 6) .map((suggestion) => ({ kind: suggestion.kind, id: suggestion.id, title: suggestion.title, shortName: suggestion.shortName, })); } const slashMatch = draft.match(SLASH_TRIGGER); if (slashMatch) { const query = normalize(slashMatch[2] ?? ""); return COMMAND_SUGGESTIONS.filter((suggestion) => { return !query || suggestion.mode.startsWith(query); }); } return []; } export function applyComposerSuggestion(args: { draft: string; suggestion: ComposerSuggestion; selectedWorkPackage?: Pick; }) { const { draft, suggestion, selectedWorkPackage } = args; if (suggestion.kind === "package") { return draft.replace( AT_TRIGGER, (_, prefix: string) => `${prefix}@${suggestion.shortName} `, ); } const packageRef = selectedWorkPackage?.shortName || selectedWorkPackage?.title || ""; const replacement = packageRef ? `@${packageRef} ${suggestion.mode} ` : `/${suggestion.mode} `; return draft.replace(SLASH_TRIGGER, (_, prefix: string) => `${prefix}${replacement}`); }