/** * Slash Command Extractor - Handle slash command extraction from AI Group responses * * Extracts and processes slash command invocations and their follow-up instructions. */ import { extractSlashInfo, isCommandContent } from '@shared/utils/contentSanitizer'; import { estimateTokens, toDate } from './aiGroupHelpers'; import type { ParsedMessage } from '@renderer/types/data'; import type { SlashItem } from '@renderer/types/groups'; /** * Info about the preceding user message's slash invocation. * This is passed from the UserGroup to help link slash outputs to slash names. */ export interface PrecedingSlashInfo { /** Slash name (e.g., "claude-hud:setup", "isolate-context", "model") */ name: string; /** Message content from */ message?: string; /** Arguments from */ args?: string; /** UUID of the slash command message */ commandMessageUuid: string; /** Timestamp of the slash command message */ timestamp: Date; } /** * Extract slash items from AI group responses. * * All slash invocations follow the same format: * /xxx * xxx * optional * * Strategy: * 1. Build a map of follow-up messages (isMeta:true with parentUuid) by their parentUuid * 2. If precedingSlash is provided, create a SlashItem with its follow-up instructions * 3. Also check for any slash invocations in responses (fallback) * * @param responses - All response messages in the AI group * @param precedingSlash - Optional slash info from the preceding UserGroup * @returns Array of SlashItem objects ready for display */ export function extractSlashes( responses: ParsedMessage[], precedingSlash?: PrecedingSlashInfo ): SlashItem[] { const slashes: SlashItem[] = []; // Build a map of follow-up messages by their parentUuid // These are isMeta:true messages that contain slash instructions/output const followUpsByParentUuid = new Map< string, { text: string; timestamp: Date; } >(); // Also build a map of potential slash messages from responses (fallback) const slashMessagesById = new Map< string, { uuid: string; name: string; message?: string; args?: string; timestamp: Date; } >(); for (const msg of responses) { // Look for slash messages (user messages with string content containing ) // This is a fallback in case the slash invocation is somehow in responses if (msg.type === 'user' && typeof msg.content === 'string' && isCommandContent(msg.content)) { const slashInfo = extractSlashInfo(msg.content); if (slashInfo) { slashMessagesById.set(msg.uuid, { uuid: msg.uuid, name: slashInfo.name, message: slashInfo.message, args: slashInfo.args, timestamp: toDate(msg.timestamp), }); } } // Look for follow-up isMeta messages with parentUuid if ( msg.type === 'user' && msg.isMeta === true && msg.parentUuid && !msg.sourceToolUseID && // Exclude tool-call related messages Array.isArray(msg.content) ) { // Extract text from the message for (const block of msg.content) { if (block.type === 'text' && block.text) { const text = block.text; followUpsByParentUuid.set(msg.parentUuid, { text, timestamp: toDate(msg.timestamp), }); break; // Only need the first text block } } } } // Strategy 1: If we have precedingSlash info, create a SlashItem with its follow-up if (precedingSlash) { const followUp = followUpsByParentUuid.get(precedingSlash.commandMessageUuid); slashes.push({ id: `slash-${precedingSlash.commandMessageUuid}`, name: precedingSlash.name, message: precedingSlash.message, args: precedingSlash.args, commandMessageUuid: precedingSlash.commandMessageUuid, instructions: followUp?.text, instructionsTokenCount: followUp ? estimateTokens(followUp.text) : undefined, // Use follow-up timestamp if available (sorts with other AI items), // otherwise fall back to slash invocation timestamp timestamp: followUp?.timestamp ?? precedingSlash.timestamp, }); } // Strategy 2: Fallback - match slash messages found in responses to their follow-ups for (const [uuid, slashMsg] of slashMessagesById.entries()) { // Skip if we already added this slash via precedingSlash if (uuid === precedingSlash?.commandMessageUuid) { continue; } const followUp = followUpsByParentUuid.get(uuid); slashes.push({ id: `slash-${uuid}`, name: slashMsg.name, message: slashMsg.message, args: slashMsg.args, commandMessageUuid: uuid, instructions: followUp?.text, instructionsTokenCount: followUp ? estimateTokens(followUp.text) : undefined, timestamp: slashMsg.timestamp, }); } return slashes; }