| 14 | * @returns Parsed command or null if not a command |
| 15 | */ |
| 16 | export function parseCommand(input: string): ParsedCommand { |
| 17 | const trimmed = input.trim(); |
| 18 | if (!trimmed.startsWith("/")) { |
| 19 | return null; |
| 20 | } |
| 21 | |
| 22 | // Remove leading slash and split by spaces (respecting quotes) |
| 23 | // Parse tokens from the full input so newlines can act as whitespace between args. |
| 24 | const parts = (trimmed.substring(1).match(/(?:[^\s"]+|"[^"]*")+/g) ?? []) as string[]; |
| 25 | if (parts.length === 0) { |
| 26 | return null; |
| 27 | } |
| 28 | |
| 29 | const [commandKey, ...restTokens] = parts; |
| 30 | const definition = SLASH_COMMAND_DEFINITION_MAP.get(commandKey); |
| 31 | |
| 32 | if (!definition) { |
| 33 | // Parse oneshot syntax: /model, /model+thinking, /+thinking |
| 34 | // Examples: /haiku, /opus+2, /+0, /haiku+medium, /sonnet+high |
| 35 | const oneshotResult = commandKey ? parseOneshotCommandKey(commandKey) : null; |
| 36 | if (oneshotResult) { |
| 37 | // Extract the message: everything after the command key |
| 38 | const commandKeyWithSlash = `/${commandKey}`; |
| 39 | let message = trimmed.substring(commandKeyWithSlash.length); |
| 40 | // Only trim spaces at the start, not newlines (preserves multiline messages) |
| 41 | while (message.startsWith(" ")) { |
| 42 | message = message.substring(1); |
| 43 | } |
| 44 | |
| 45 | // If no message provided, show model help instead |
| 46 | if (!message.trim()) { |
| 47 | return { type: "model-help" }; |
| 48 | } |
| 49 | |
| 50 | return { |
| 51 | type: "model-oneshot", |
| 52 | ...oneshotResult, |
| 53 | message, |
| 54 | }; |
| 55 | } |
| 56 | |
| 57 | return { |
| 58 | type: "unknown-command", |
| 59 | command: commandKey ?? "", |
| 60 | subcommand: restTokens[0], |
| 61 | }; |
| 62 | } |
| 63 | |
| 64 | const path: SlashCommandDefinition[] = [definition]; |
| 65 | let remainingTokens = restTokens; |
| 66 | |
| 67 | while (remainingTokens.length > 0) { |
| 68 | const currentDefinition = path[path.length - 1]; |
| 69 | const children = currentDefinition.children ?? []; |
| 70 | const nextToken = remainingTokens[0]; |
| 71 | const nextDefinition = children.find((child) => child.key === nextToken); |
| 72 | |
| 73 | if (!nextDefinition) { |