/** * Content sanitization utilities for display. * * SHARED MODULE: Used by both main and renderer processes. * - Main process: Used in jsonl.ts for initial parsing * - Renderer process: Used in groupTransformer.ts for display formatting * * This module handles conversion of raw JSONL content (with XML tags) into * human-readable format for the UI. * * NOTE: This file was previously duplicated in both main/utils and renderer/utils. * Consolidated to src/shared/utils to maintain DRY principle while serving both processes. */ /** * Patterns for noise tags that should be completely removed. * These are system-generated metadata that provide no value in display. */ const NOISE_TAG_PATTERNS = [ /[\s\S]*?<\/local-command-caveat>/gi, /[\s\S]*?<\/system-reminder>/gi, /[\s\S]*?<\/task-notification>/gi, /[\s\S]*?<\/opencode_runtime_identity>/gi, /[\s\S]*?<\/opencode_app_message_delivery>/gi, /[\s\S]*?<\/opencode_delivery_context>/gi, /[\s\S]*?<\/opencode_delivery_retry>/gi, ]; /** * Pattern to match the trailing output-file instruction emitted after * task notifications. */ const TASK_OUTPUT_INSTRUCTION_PATTERN = / ?Read the output file to retrieve the result: [^\s]+/g; const OPENCODE_INBOUND_APP_MESSAGE_PATTERN = /\s*([\s\S]*?)\s*<\/opencode_inbound_app_message>/gi; export interface CommandOutputInfo { stream: 'stdout' | 'stderr'; output: string; } /** * Extract content from tags. * Returns the command output without the wrapper tags. */ export function extractCommandOutputInfo(content: string): CommandOutputInfo | null { const match = /([\s\S]*?)<\/local-command-stdout>/i.exec(content); const matchStderr = /([\s\S]*?)<\/local-command-stderr>/i.exec(content); if (match) { return { stream: 'stdout', output: match[1].trim(), }; } if (matchStderr) { return { stream: 'stderr', output: matchStderr[1].trim(), }; } return null; } /** * Extract command info from command XML tags. * Returns the slash command in readable format (e.g., "/model sonnet") */ function extractCommandDisplay(content: string): string | null { const commandNameMatch = /\/([^<]+)<\/command-name>/.exec(content); const commandArgsMatch = /([^<]*)<\/command-args>/.exec(content); if (commandNameMatch) { const commandName = `/${commandNameMatch[1].trim()}`; const args = commandArgsMatch?.[1]?.trim(); return args ? `${commandName} ${args}` : commandName; } return null; } /** * Check if content is primarily a command message. * Handles both orderings: * - Built-in commands: comes first * - Skill commands: comes first, followed by */ export function isCommandContent(content: string): boolean { return content.startsWith('') || content.startsWith(''); } /** * Check if content is a command output message. */ export function isCommandOutputContent(content: string): boolean { return ( content.startsWith('') || content.startsWith('') ); } /** * Sanitize content for display. * * - Command messages: Converted to readable format (e.g., "/model sonnet") * - Command output: Extracted from tags * - Noise tags: Completely removed * - Regular content: Returned as-is */ export function sanitizeDisplayContent(content: string): string { // If it's a command output message, extract the output content if (isCommandOutputContent(content)) { const commandOutput = extractCommandOutputInfo(content); if (commandOutput) { return commandOutput.output; } } // If it's a command message, extract the command for display if (isCommandContent(content)) { const commandDisplay = extractCommandDisplay(content); if (commandDisplay) { return commandDisplay; } } // Remove noise tags let sanitized = content; for (const pattern of NOISE_TAG_PATTERNS) { sanitized = sanitized.replace(pattern, ''); } sanitized = sanitized.replace( OPENCODE_INBOUND_APP_MESSAGE_PATTERN, (_match, innerContent: string | undefined) => innerContent?.trim() ?? '' ); // Also remove any remaining command tags (in case of mixed content) sanitized = sanitized .replace(/[\s\S]*?<\/command-name>/gi, '') .replace(/[\s\S]*?<\/command-message>/gi, '') .replace(/[\s\S]*?<\/command-args>/gi, ''); // Remove follow-up instructions that only make sense in raw XML form. sanitized = sanitized.replace(TASK_OUTPUT_INSTRUCTION_PATTERN, ''); return sanitized.replace(/\n{3,}/g, '\n\n').trim(); } /** * Slash info extracted from command XML tags. * All slash commands have the same format: * /xxx * xxx * optional */ export interface SlashInfo { /** Slash name without the leading slash (e.g., "model", "isolate-context") */ name: string; /** Message content from */ message?: string; /** Optional arguments from */ args?: string; } /** * Extract slash information from command XML tags. * Works for all slash types: skills, built-in commands, plugins, MCP, user commands. * Returns null if not a slash command format. */ export function extractSlashInfo(content: string): SlashInfo | null { const nameMatch = /\/([^<]+)<\/command-name>/.exec(content); if (!nameMatch) return null; const name = nameMatch[1].trim(); const messageMatch = /([^<]*)<\/command-message>/.exec(content); const argsMatch = /([^<]*)<\/command-args>/.exec(content); return { name, message: messageMatch?.[1]?.trim() ?? undefined, args: argsMatch?.[1]?.trim() ?? undefined, }; } // ============================================================================= // Task Notification Parsing // ============================================================================= /** * Parsed background task notification embedded in a user message. */ export interface TaskNotification { taskId: string; status: string; summary: string; outputFile: string; } /** * Extract task notifications from raw Claude Code XML blocks. */ export function parseTaskNotifications(content: string): TaskNotification[] { const notifications: TaskNotification[] = []; const pattern = /([\s\S]*?)<\/task-notification>/gi; let match: RegExpExecArray | null; while ((match = pattern.exec(content)) !== null) { const block = match[1]; notifications.push({ taskId: /([^<]*)<\/task-id>/.exec(block)?.[1] ?? '', status: /([^<]*)<\/status>/.exec(block)?.[1] ?? '', summary: /([\s\S]*?)<\/summary>/.exec(block)?.[1]?.trim() ?? '', outputFile: /([^<]*)<\/output-file>/.exec(block)?.[1] ?? '', }); } return notifications; }