154 lines · 4.9 KB · TypeScript
RawDownload
/** * 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 <command-message> */  message?: string;  /** Arguments from <command-args> */  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: *   <command-name>/xxx</command-name> *   <command-message>xxx</command-message> *   <command-args>optional</command-args> * * 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 <command-name>)    // 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;}