import React, { useRef, useState, useCallback } from 'react';
import { ArrowDown, CircleDashed, CheckCircle, AlertTriangle } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Markdown } from '@/components/ui/markdown';
import { UnifiedMessage, ParsedContent, ParsedMetadata } from '@/components/thread/types';
import { FileAttachmentGrid } from '@/components/thread/file-attachment';
import { useFilePreloader, FileCache } from '@/hooks/react-query/files';
import { useAuth } from '@/components/AuthProvider';
import { Project } from '@/lib/api';
import {
extractPrimaryParam,
getToolIcon,
getUserFriendlyToolName,
safeJsonParse,
} from '@/components/thread/utils';
import { KortixLogo } from '@/components/sidebar/kortix-logo';
import { AgentLoader } from './loader';
import { parseXmlToolCalls, isNewXmlFormat, extractToolNameFromStream } from '@/components/thread/tool-views/xml-parser';
import { parseToolResult } from '@/components/thread/tool-views/tool-result-parser';
// Define the set of tags whose raw XML should be hidden during streaming
const HIDE_STREAMING_XML_TAGS = new Set([
'execute-command',
'create-file',
'delete-file',
'full-file-rewrite',
'str-replace',
'browser-click-element',
'browser-close-tab',
'browser-drag-drop',
'browser-get-dropdown-options',
'browser-go-back',
'browser-input-text',
'browser-navigate-to',
'browser-scroll-down',
'browser-scroll-to-text',
'browser-scroll-up',
'browser-select-dropdown-option',
'browser-send-keys',
'browser-switch-tab',
'browser-wait',
'deploy',
'ask',
'complete',
'crawl-webpage',
'web-search',
'see-image',
'call-mcp-tool',
'execute_data_provider_call',
'execute_data_provider_endpoint',
'execute-data-provider-call',
'execute-data-provider-endpoint',
]);
// Helper function to render attachments (keeping original implementation for now)
export function renderAttachments(attachments: string[], fileViewerHandler?: (filePath?: string, filePathList?: string[]) => void, sandboxId?: string, project?: Project) {
if (!attachments || attachments.length === 0) return null;
// Note: Preloading is now handled by React Query in the main ThreadContent component
// to avoid duplicate requests with different content types
return ;
}
// Render Markdown content while preserving XML tags that should be displayed as tool calls
export function renderMarkdownContent(
content: string,
handleToolClick: (assistantMessageId: string | null, toolName: string) => void,
messageId: string | null,
fileViewerHandler?: (filePath?: string, filePathList?: string[]) => void,
sandboxId?: string,
project?: Project,
debugMode?: boolean
) {
// If in debug mode, just display raw content in a pre tag
if (debugMode) {
return (
{content}
);
}
// Check if content contains the new Cursor-style format
if (isNewXmlFormat(content)) {
const contentParts: React.ReactNode[] = [];
let lastIndex = 0;
// Find all function_calls blocks
const functionCallsRegex = /([\s\S]*?)<\/function_calls>/gi;
let match;
while ((match = functionCallsRegex.exec(content)) !== null) {
// Add text before the function_calls block
if (match.index > lastIndex) {
const textBeforeBlock = content.substring(lastIndex, match.index);
if (textBeforeBlock.trim()) {
contentParts.push(
{textBeforeBlock}
);
}
}
// Parse the tool calls in this block
const toolCalls = parseXmlToolCalls(match[0]);
toolCalls.forEach((toolCall, index) => {
const toolName = toolCall.functionName.replace(/_/g, '-');
const IconComponent = getToolIcon(toolName);
// Extract primary parameter for display
let paramDisplay = '';
if (toolCall.parameters.file_path) {
paramDisplay = toolCall.parameters.file_path;
} else if (toolCall.parameters.command) {
paramDisplay = toolCall.parameters.command;
} else if (toolCall.parameters.query) {
paramDisplay = toolCall.parameters.query;
} else if (toolCall.parameters.url) {
paramDisplay = toolCall.parameters.url;
}
contentParts.push(
);
});
lastIndex = match.index + match[0].length;
}
// Add any remaining text after the last function_calls block
if (lastIndex < content.length) {
const remainingText = content.substring(lastIndex);
if (remainingText.trim()) {
contentParts.push(
{remainingText}
);
}
}
return contentParts.length > 0 ? contentParts : {content};
}
// Fall back to old XML format handling
const xmlRegex = /<(?!inform\b)([a-zA-Z\-_]+)(?:\s+[^>]*)?>(?:[\s\S]*?)<\/\1>|<(?!inform\b)([a-zA-Z\-_]+)(?:\s+[^>]*)?\/>/g;
let lastIndex = 0;
const contentParts: React.ReactNode[] = [];
let match;
// If no XML tags found, just return the full content as markdown
if (!content.match(xmlRegex)) {
return {content};
}
while ((match = xmlRegex.exec(content)) !== null) {
// Add text before the tag as markdown
if (match.index > lastIndex) {
const textBeforeTag = content.substring(lastIndex, match.index);
contentParts.push(
{textBeforeTag}
);
}
const rawXml = match[0];
const toolName = match[1] || match[2];
const toolCallKey = `tool-${match.index}`;
if (toolName === 'ask') {
// Extract attachments from the XML attributes
const attachmentsMatch = rawXml.match(/attachments=["']([^"']*)["']/i);
const attachments = attachmentsMatch
? attachmentsMatch[1].split(',').map(a => a.trim())
: [];
// Extract content from the ask tag
const contentMatch = rawXml.match(/]*>([\s\S]*?)<\/ask>/i);
const askContent = contentMatch ? contentMatch[1] : '';
// Render tag content with attachment UI (using the helper)
contentParts.push(
{cleanContent && (
{cleanContent}
)}
{/* Use the helper function to render user attachments */}
{renderAttachments(attachments as string[], handleOpenFileViewer, sandboxId, project)}
);
} else if (group.type === 'assistant_group') {
return (
{/* Logo positioned above the message content */}
{agentAvatar}
{agentName ? agentName : 'Suna'}
{/* Message content */}
{(() => {
// In debug mode, just show raw messages content
if (debugMode) {
return group.messages.map((message, msgIndex) => {
const msgKey = message.message_id || `raw-msg-${msgIndex}`;
return (