Spaces:
Running
Running
| import type { CommandMode, ParsedCommand, WorkPackage } from "./work-package-types"; | |
| export type CommandParseResult = { | |
| parsed: ParsedCommand; | |
| matchedWorkPackageId?: string; | |
| matchedWorkPackageTitle?: string; | |
| suggestions?: { id: string; title: string; shortName: string }[]; | |
| error?: string; | |
| }; | |
| const MODES: CommandMode[] = ["ask", "plan", "change", "execute"]; | |
| function normalize(s: string): string { | |
| return s.trim().replace(/\s+/g, " ").toLowerCase(); | |
| } | |
| function matchWorkPackageByName( | |
| workPackages: WorkPackage[], | |
| name: string, | |
| ): { id: string; title: string; shortName: string } | undefined { | |
| const n = normalize(name); | |
| if (!n) return; | |
| return workPackages.find((wp) => { | |
| return normalize(wp.title) === n || normalize(wp.shortName) === n; | |
| }); | |
| } | |
| function getSuggestions(workPackages: WorkPackage[], name: string) { | |
| const n = normalize(name); | |
| if (!n) return []; | |
| // Cheap “contains” suggestion without pulling in a fuzzy-match dep. | |
| return workPackages | |
| .map((wp) => ({ | |
| id: wp.id, | |
| title: wp.title, | |
| shortName: wp.shortName, | |
| score: | |
| Number(normalize(wp.title).includes(n)) + | |
| Number(normalize(wp.shortName).includes(n)), | |
| })) | |
| .filter((x) => x.score > 0) | |
| .sort((a, b) => b.score - a.score) | |
| .slice(0, 5) | |
| .map(({ id, title, shortName }) => ({ id, title, shortName })); | |
| } | |
| export function parseCommand( | |
| text: string, | |
| workPackages: WorkPackage[], | |
| ): CommandParseResult { | |
| const raw = text.trim(); | |
| if (!raw.startsWith("@")) { | |
| return { parsed: { instruction: raw } }; | |
| } | |
| const afterAt = raw.slice(1).trim(); | |
| if (!afterAt) { | |
| return { parsed: { instruction: "" }, error: "Missing work package name." }; | |
| } | |
| const tokens = afterAt.split(/\s+/g); | |
| // Find explicit mode token (if present). | |
| let modeIdx = -1; | |
| for (let i = 0; i < tokens.length; i++) { | |
| if (MODES.includes(tokens[i].toLowerCase() as CommandMode)) { | |
| modeIdx = i; | |
| break; | |
| } | |
| } | |
| // If mode is missing, infer the longest package-name prefix that matches. | |
| if (modeIdx === -1) { | |
| let bestMatchLen = 0; | |
| let bestMatch: | |
| | { id: string; title: string; shortName: string; matchedName: string } | |
| | undefined; | |
| for (let len = Math.min(tokens.length, 10); len >= 1; len--) { | |
| const candidate = tokens.slice(0, len).join(" "); | |
| const m = matchWorkPackageByName(workPackages, candidate); | |
| if (m) { | |
| bestMatchLen = len; | |
| bestMatch = { ...m, matchedName: candidate }; | |
| break; | |
| } | |
| } | |
| if (!bestMatch) { | |
| const guessName = tokens[0]; | |
| return { | |
| parsed: { referencedPackageName: guessName, mode: "ask", instruction: tokens.slice(1).join(" ") }, | |
| suggestions: getSuggestions(workPackages, guessName), | |
| error: `Work package not found: "${guessName}".`, | |
| }; | |
| } | |
| const instruction = tokens.slice(bestMatchLen).join(" ").trim(); | |
| return { | |
| parsed: { | |
| referencedPackageName: bestMatch.matchedName, | |
| mode: "ask", | |
| instruction, | |
| }, | |
| matchedWorkPackageId: bestMatch.id, | |
| matchedWorkPackageTitle: bestMatch.title, | |
| }; | |
| } | |
| const packageName = tokens.slice(0, modeIdx).join(" ").trim(); | |
| const mode = tokens[modeIdx].toLowerCase() as CommandMode; | |
| const instruction = tokens.slice(modeIdx + 1).join(" ").trim(); | |
| const m = matchWorkPackageByName(workPackages, packageName); | |
| if (!m) { | |
| return { | |
| parsed: { referencedPackageName: packageName, mode, instruction }, | |
| suggestions: getSuggestions(workPackages, packageName), | |
| error: `Work package not found: "${packageName}".`, | |
| }; | |
| } | |
| return { | |
| parsed: { referencedPackageName: m.title, mode, instruction }, | |
| matchedWorkPackageId: m.id, | |
| matchedWorkPackageTitle: m.title, | |
| }; | |
| } | |