diff --git a/frontend/src/components/thread/content/ThreadContent.tsx b/frontend/src/components/thread/content/ThreadContent.tsx index 0b7a422d..6854c853 100644 --- a/frontend/src/components/thread/content/ThreadContent.tsx +++ b/frontend/src/components/thread/content/ThreadContent.tsx @@ -53,26 +53,52 @@ export function renderAttachments(attachments: string[], fileViewerHandler?: (fi // Render Markdown content while preserving XML tags that should be displayed as tool calls function preprocessTextOnlyTools(content: string): string { console.log('🔍 preprocessTextOnlyTools called with:', typeof content, content); - + if (!content || typeof content !== 'string') { console.warn('❌ preprocessTextOnlyTools: Invalid content type:', typeof content, content); return content || ''; } - - // Handle new function calls format for text-only tools - extract text parameter content - // Complete XML format - content = content.replace(/\s*\s*([\s\S]*?)<\/parameter>[\s\S]*?<\/invoke>\s*<\/function_calls>/gi, '$1'); - content = content.replace(/\s*\s*([\s\S]*?)<\/parameter>[\s\S]*?<\/invoke>\s*<\/function_calls>/gi, '$1'); + + // For ask/complete tools, we need to preserve them if they have attachments + // Only strip them if they don't have attachments parameter + + // Handle new function calls format - only strip if no attachments + content = content.replace(/\s*\s*([\s\S]*?)<\/parameter>\s*<\/invoke>\s*<\/function_calls>/gi, (match) => { + if (match.includes('\s*\s*([\s\S]*?)<\/parameter>\s*<\/invoke>\s*<\/function_calls>/gi, '$1'); + }); + + content = content.replace(/\s*\s*([\s\S]*?)<\/parameter>\s*<\/invoke>\s*<\/function_calls>/gi, (match) => { + if (match.includes('\s*\s*([\s\S]*?)<\/parameter>\s*<\/invoke>\s*<\/function_calls>/gi, '$1'); + }); + content = content.replace(/\s*[\s\S]*?([\s\S]*?)<\/parameter>[\s\S]*?<\/invoke>\s*<\/function_calls>/gi, '$1'); - - // Handle streaming/partial XML for message tools - extract text parameter content even if incomplete - content = content.replace(/\s*\s*([\s\S]*?)$/gi, '$1'); - content = content.replace(/\s*\s*([\s\S]*?)$/gi, '$1'); + + // Handle streaming/partial XML for message tools - only strip if no attachments visible yet + content = content.replace(/\s*\s*([\s\S]*?)$/gi, (match) => { + if (match.includes('\s*\s*([\s\S]*?)$/gi, '$1'); + }); + + content = content.replace(/\s*\s*([\s\S]*?)$/gi, (match) => { + if (match.includes('\s*\s*([\s\S]*?)$/gi, '$1'); + }); + content = content.replace(/\s*[\s\S]*?([\s\S]*?)$/gi, '$1'); - - // Also handle old format for backward compatibility - content = content.replace(/]*>([\s\S]*?)<\/ask>/gi, '$1'); - content = content.replace(/]*>([\s\S]*?)<\/complete>/gi, '$1'); + + // Also handle old format - only strip if no attachments attribute + content = content.replace(/]*>([\s\S]*?)<\/ask>/gi, (match) => { + if (match.match(/]*attachments=/i)) return match; + return match.replace(/]*>([\s\S]*?)<\/ask>/gi, '$1'); + }); + + content = content.replace(/]*>([\s\S]*?)<\/complete>/gi, (match) => { + if (match.match(/]*attachments=/i)) return match; + return match.replace(/]*>([\s\S]*?)<\/complete>/gi, '$1'); + }); + content = content.replace(/]*>([\s\S]*?)<\/present_presentation>/gi, '$1'); return content; } @@ -88,7 +114,7 @@ export function renderMarkdownContent( ) { // Preprocess content to convert text-only tools to natural text content = preprocessTextOnlyTools(content); - + // If in debug mode, just display raw content in a pre tag if (debugMode) { return ( @@ -139,7 +165,7 @@ export function renderMarkdownContent( {renderAttachments(attachmentArray, fileViewerHandler, sandboxId, project)} ); - + // Also render standalone attachments outside the message const standaloneAttachments = renderStandaloneAttachments(attachmentArray, fileViewerHandler, sandboxId, project); if (standaloneAttachments) { @@ -165,7 +191,7 @@ export function renderMarkdownContent( {renderAttachments(attachmentArray, fileViewerHandler, sandboxId, project)} ); - + // Also render standalone attachments outside the message const standaloneAttachments = renderStandaloneAttachments(attachmentArray, fileViewerHandler, sandboxId, project); if (standaloneAttachments) { @@ -268,7 +294,7 @@ export function renderMarkdownContent( {renderAttachments(attachments, fileViewerHandler, sandboxId, project)} ); - + // Also render standalone attachments outside the message const standaloneAttachments = renderStandaloneAttachments(attachments, fileViewerHandler, sandboxId, project); if (standaloneAttachments) { @@ -296,7 +322,7 @@ export function renderMarkdownContent( {renderAttachments(attachments, fileViewerHandler, sandboxId, project)} ); - + // Also render standalone attachments outside the message const standaloneAttachments = renderStandaloneAttachments(attachments, fileViewerHandler, sandboxId, project); if (standaloneAttachments) { @@ -671,13 +697,13 @@ export const ThreadContent: React.FC = ({ // Use merged groups instead of original grouped messages const finalGroupedMessages = mergedGroups; - + // Helper function to add streaming content to groups const appendStreamingContent = (content: string, isPlayback: boolean = false) => { const messageId = isPlayback ? 'playbackStreamingText' : 'streamingTextContent'; const metadata = isPlayback ? 'playbackStreamingText' : 'streamingTextContent'; const keySuffix = isPlayback ? 'playback-streaming' : 'streaming'; - + const lastGroup = finalGroupedMessages.at(-1); if (!lastGroup || lastGroup.type === 'user') { // Create new assistant group for streaming content @@ -770,7 +796,7 @@ export const ThreadContent: React.FC = ({
{/* All file attachments rendered outside message bubble */} {renderStandaloneAttachments(attachments as string[], handleOpenFileViewer, sandboxId, project, true)} - +
@@ -789,7 +815,7 @@ export const ThreadContent: React.FC = ({ // Get agent_id from the first assistant message in this group const firstAssistantMsg = group.messages.find(m => m.type === 'assistant'); const groupAgentId = firstAssistantMsg?.agent_id; - + return (
@@ -903,7 +929,7 @@ export const ThreadContent: React.FC = ({ // Preprocess content first to remove text-only tool tags const textToRender = preprocessTextOnlyTools(streamingTextContent || ''); - + let detectedTag: string | null = null; let tagStartIndex = -1; if (textToRender) { @@ -927,11 +953,11 @@ export const ThreadContent: React.FC = ({ } const textBeforeTag = detectedTag ? textToRender.substring(0, tagStartIndex) : textToRender; const showCursor = - (streamHookStatus === - 'streaming' || - streamHookStatus === - 'connecting') && - !detectedTag; + (streamHookStatus === + 'streaming' || + streamHookStatus === + 'connecting') && + !detectedTag; // Show minimal processing indicator when agent is active but no streaming text after preprocessing if (!textToRender && (streamHookStatus === 'streaming' || streamHookStatus === 'connecting')) { @@ -946,8 +972,8 @@ export const ThreadContent: React.FC = ({ return ( <> - @@ -972,7 +998,7 @@ export const ThreadContent: React.FC = ({ {(() => { // Preprocess content first to remove text-only tool tags const textToRender = preprocessTextOnlyTools(streamingText || ''); - + let detectedTag: string | null = null; let tagStartIndex = -1; if (textToRender) { @@ -1009,8 +1035,8 @@ export const ThreadContent: React.FC = ({ ) : ( <> -