159 lines · 4.6 KB · TypeScript
RawDownload
/** * SearchTextExtractor - Lightweight text extraction for search. * * Mirrors ChunkBuilder's classification loop (classifyMessages → buffer flush) * but only extracts searchable text + metadata, skipping all expensive operations: * - No tool execution building * - No semantic step extraction * - No subagent linking * - No timeline gap filling * - No metrics calculation */ import { classifyMessages } from '@main/services/parsing/MessageClassifier';import { sanitizeDisplayContent } from '@shared/utils/contentSanitizer'; import type { ParsedMessage } from '@main/types'; /** * A lightweight entry containing only the data needed for search matching. */export interface SearchableEntry {  text: string;  groupId: string;  messageType: 'user' | 'assistant';  itemType: 'user' | 'ai';  timestamp: number;  messageUuid: string;} /** * Result of extracting searchable text from a session's messages. */export interface SearchTextResult {  entries: SearchableEntry[];  sessionTitle: string | undefined;} /** * Extract searchable text entries from parsed messages. * * Algorithm mirrors ChunkBuilder.buildChunks() lines 78-151: * - Filter to main thread (!m.isSidechain) * - classifyMessages() — cheap type guard checks * - Walk classified messages with an aiBuffer: *   - hardNoise → skip *   - compact / system / user → flush AI buffer, then handle *   - ai → push to buffer * - Flush remaining buffer at end */export function extractSearchableEntries(messages: ParsedMessage[]): SearchTextResult {  const entries: SearchableEntry[] = [];  let sessionTitle: string | undefined;   // Filter to main thread messages (non-sidechain) — same as ChunkBuilder line 82  const mainMessages = messages.filter((m) => !m.isSidechain);  const classified = classifyMessages(mainMessages);   let aiBuffer: ParsedMessage[] = [];   for (const { message, category } of classified) {    switch (category) {      case 'hardNoise':        // Skip — filtered out        break;       case 'compact':      case 'system':        // Flush AI buffer, but compact/system messages have no searchable text        if (aiBuffer.length > 0) {          const aiEntry = extractAIEntry(aiBuffer);          if (aiEntry) entries.push(aiEntry);          aiBuffer = [];        }        break;       case 'user': {        // Flush AI buffer        if (aiBuffer.length > 0) {          const aiEntry = extractAIEntry(aiBuffer);          if (aiEntry) entries.push(aiEntry);          aiBuffer = [];        }        // Extract user text        const userText = extractUserText(message);        if (userText) {          if (!sessionTitle) {            sessionTitle = userText.slice(0, 100);          }          entries.push({            text: userText,            groupId: `user-${message.uuid}`,            messageType: 'user',            itemType: 'user',            timestamp: message.timestamp.getTime(),            messageUuid: message.uuid,          });        }        break;      }       case 'ai':        aiBuffer.push(message);        break;    }  }   // Flush remaining AI buffer  if (aiBuffer.length > 0) {    const aiEntry = extractAIEntry(aiBuffer);    if (aiEntry) entries.push(aiEntry);  }   return { entries, sessionTitle };} /** * Extract the last text output from an AI message buffer. * Scans backward for the last assistant message with a text content block. */function extractAIEntry(buffer: ParsedMessage[]): SearchableEntry | null {  // Scan backward for last assistant message with text content  for (let i = buffer.length - 1; i >= 0; i--) {    const msg = buffer[i];    if (msg.role !== 'assistant' || !Array.isArray(msg.content)) continue;     // Find the last text block in this message    for (let j = msg.content.length - 1; j >= 0; j--) {      const block = msg.content[j];      if (block.type === 'text' && block.text) {        return {          text: block.text,          groupId: `ai-${buffer[0].uuid}`,          messageType: 'assistant',          itemType: 'ai',          timestamp: msg.timestamp.getTime(),          messageUuid: msg.uuid,        };      }    }  }  return null;} /** * Extract searchable text from a user message. * Shared logic previously in SessionSearcher.extractUserSearchableText(). */export function extractUserText(message: ParsedMessage): string {  let rawText = '';  if (typeof message.content === 'string') {    rawText = message.content;  } else if (Array.isArray(message.content)) {    rawText = message.content      .filter((block) => block.type === 'text')      .map((block) => block.text)      .join('');  }  return sanitizeDisplayContent(rawText);}