'use client'; import { useMemo, useState } from 'react'; import { ChevronDown, ChevronRight, Wrench } from 'lucide-react'; import { Markdown } from '@/components/ui/markdown'; import { cn } from '@/lib/utils'; interface ToolCall { tool: string; input: string; } interface ParsedPlanContent { toolCalls: ToolCall[]; planMarkdown: string; } /** * Parses plan content to separate tool calls from the actual plan/specification markdown. * Tool calls appear at the beginning (exploration phase), followed by the plan markdown. */ function parsePlanContent(content: string): ParsedPlanContent { const lines = content.split('\n'); const toolCalls: ToolCall[] = []; let planStartIndex = -1; let currentTool: string | null = null; let currentInput: string[] = []; let inJsonBlock = false; let braceDepth = 0; for (let i = 0; i < lines.length; i++) { const line = lines[i]; const trimmed = line.trim(); // Check if this line starts the actual plan/spec (markdown heading) // Plans typically start with # or ## headings if ( !inJsonBlock && (trimmed.match(/^#{1,3}\s+\S/) || // Markdown headings (including emoji like ## ✅ Plan) trimmed.startsWith('---') || // Horizontal rule often used as separator trimmed.match(/^\*\*\S/)) // Bold text starting a section ) { // Flush any active tool call before starting the plan if (currentTool && currentInput.length > 0) { toolCalls.push({ tool: currentTool, input: currentInput.join('\n').trim(), }); currentTool = null; currentInput = []; } planStartIndex = i; break; } // Detect tool call start (supports tool names with dots/hyphens like web.run, file-read) const toolMatch = trimmed.match(/^(?:🔧\s*)?Tool:\s*([^\s]+)/i); if (toolMatch && !inJsonBlock) { // Save previous tool call if exists if (currentTool && currentInput.length > 0) { toolCalls.push({ tool: currentTool, input: currentInput.join('\n').trim(), }); } currentTool = toolMatch[1]; currentInput = []; continue; } // Detect Input: line if (trimmed.startsWith('Input:') && currentTool) { const inputContent = trimmed.replace(/^Input:\s*/, ''); if (inputContent) { currentInput.push(inputContent); // Check if JSON starts if (inputContent.includes('{')) { braceDepth = (inputContent.match(/\{/g) || []).length - (inputContent.match(/\}/g) || []).length; inJsonBlock = braceDepth > 0; } } continue; } // If we're collecting input for a tool if (currentTool) { if (inJsonBlock) { currentInput.push(line); braceDepth += (trimmed.match(/\{/g) || []).length - (trimmed.match(/\}/g) || []).length; if (braceDepth <= 0) { inJsonBlock = false; // Save tool call toolCalls.push({ tool: currentTool, input: currentInput.join('\n').trim(), }); currentTool = null; currentInput = []; } } else if (trimmed.startsWith('{')) { // JSON block starting currentInput.push(line); braceDepth = (trimmed.match(/\{/g) || []).length - (trimmed.match(/\}/g) || []).length; inJsonBlock = braceDepth > 0; if (!inJsonBlock) { // Single-line JSON toolCalls.push({ tool: currentTool, input: currentInput.join('\n').trim(), }); currentTool = null; currentInput = []; } } else if (trimmed === '') { // Empty line might end the tool call section if (currentInput.length > 0) { toolCalls.push({ tool: currentTool, input: currentInput.join('\n').trim(), }); currentTool = null; currentInput = []; } } } } // Save any remaining tool call if (currentTool && currentInput.length > 0) { toolCalls.push({ tool: currentTool, input: currentInput.join('\n').trim(), }); } // Extract plan markdown let planMarkdown = ''; if (planStartIndex >= 0) { planMarkdown = lines.slice(planStartIndex).join('\n').trim(); } else if (toolCalls.length === 0) { // No tool calls found, treat entire content as markdown planMarkdown = content.trim(); } return { toolCalls, planMarkdown }; } interface PlanContentViewerProps { content: string; className?: string; } export function PlanContentViewer({ content, className }: PlanContentViewerProps) { const [showToolCalls, setShowToolCalls] = useState(false); const { toolCalls, planMarkdown } = useMemo(() => parsePlanContent(content), [content]); if (!content || !content.trim()) { return (
{tc.input}
No specification content found.
The plan appears to only contain exploration tool calls.