import { useEffect, useRef, useState, useMemo } from 'react'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Loader2, List, FileText, GitBranch, ClipboardList } from 'lucide-react'; import { getElectronAPI } from '@/lib/electron'; import { LogViewer } from '@/components/ui/log-viewer'; import { GitDiffPanel } from '@/components/ui/git-diff-panel'; import { TaskProgressPanel } from '@/components/ui/task-progress-panel'; import { Markdown } from '@/components/ui/markdown'; import { useAppStore } from '@/store/app-store'; import { extractSummary } from '@/lib/log-parser'; import type { AutoModeEvent } from '@/types/electron'; interface AgentOutputModalProps { open: boolean; onClose: () => void; featureDescription: string; featureId: string; /** The status of the feature - used to determine if spinner should be shown */ featureStatus?: string; /** Called when a number key (0-9) is pressed while the modal is open */ onNumberKeyPress?: (key: string) => void; /** Project path - if not provided, falls back to window.__currentProject for backward compatibility */ projectPath?: string; } type ViewMode = 'summary' | 'parsed' | 'raw' | 'changes'; export function AgentOutputModal({ open, onClose, featureDescription, featureId, featureStatus, onNumberKeyPress, projectPath: projectPathProp, }: AgentOutputModalProps) { const isBacklogPlan = featureId.startsWith('backlog-plan:'); const [output, setOutput] = useState(''); const [isLoading, setIsLoading] = useState(true); const [viewMode, setViewMode] = useState(null); const [projectPath, setProjectPath] = useState(''); // Extract summary from output const summary = useMemo(() => extractSummary(output), [output]); // Determine the effective view mode - default to summary if available, otherwise parsed const effectiveViewMode = viewMode ?? (summary ? 'summary' : 'parsed'); const scrollRef = useRef(null); const autoScrollRef = useRef(true); const projectPathRef = useRef(''); const useWorktrees = useAppStore((state) => state.useWorktrees); // Auto-scroll to bottom when output changes useEffect(() => { if (autoScrollRef.current && scrollRef.current) { scrollRef.current.scrollTop = scrollRef.current.scrollHeight; } }, [output]); // Load existing output from file useEffect(() => { if (!open) return; const loadOutput = async () => { const api = getElectronAPI(); if (!api) return; setIsLoading(true); try { // Use projectPath prop if provided, otherwise fall back to window.__currentProject for backward compatibility const resolvedProjectPath = projectPathProp || (window as any).__currentProject?.path; if (!resolvedProjectPath) { setIsLoading(false); return; } projectPathRef.current = resolvedProjectPath; setProjectPath(resolvedProjectPath); if (isBacklogPlan) { setOutput(''); return; } // Use features API to get agent output if (api.features) { const result = await api.features.getAgentOutput(resolvedProjectPath, featureId); if (result.success) { setOutput(result.content || ''); } else { setOutput(''); } } else { setOutput(''); } } catch (error) { console.error('Failed to load output:', error); setOutput(''); } finally { setIsLoading(false); } }; loadOutput(); }, [open, featureId, projectPathProp, isBacklogPlan]); // Listen to auto mode events and update output useEffect(() => { if (!open) return; const api = getElectronAPI(); if (!api?.autoMode || isBacklogPlan) return; console.log('[AgentOutputModal] Subscribing to events for featureId:', featureId); const unsubscribe = api.autoMode.onEvent((event) => { console.log( '[AgentOutputModal] Received event:', event.type, 'featureId:', 'featureId' in event ? event.featureId : 'none', 'modalFeatureId:', featureId ); // Filter events for this specific feature only (skip events without featureId) if ('featureId' in event && event.featureId !== featureId) { console.log('[AgentOutputModal] Skipping event - featureId mismatch'); return; } let newContent = ''; switch (event.type) { case 'auto_mode_progress': newContent = event.content || ''; break; case 'auto_mode_tool': { const toolName = event.tool || 'Unknown Tool'; const toolInput = event.input ? JSON.stringify(event.input, null, 2) : ''; newContent = `\n🔧 Tool: ${toolName}\n${toolInput ? `Input: ${toolInput}\n` : ''}`; break; } case 'auto_mode_phase': { const phaseEmoji = event.phase === 'planning' ? '📋' : event.phase === 'action' ? '⚡' : '✅'; newContent = `\n${phaseEmoji} ${event.message}\n`; break; } case 'auto_mode_error': newContent = `\n❌ Error: ${event.error}\n`; break; case 'auto_mode_ultrathink_preparation': { // Format thinking level preparation information let prepContent = `\n🧠 Ultrathink Preparation\n`; if (event.warnings && event.warnings.length > 0) { prepContent += `\n⚠️ Warnings:\n`; event.warnings.forEach((warning: string) => { prepContent += ` • ${warning}\n`; }); } if (event.recommendations && event.recommendations.length > 0) { prepContent += `\n💡 Recommendations:\n`; event.recommendations.forEach((rec: string) => { prepContent += ` • ${rec}\n`; }); } if (event.estimatedCost !== undefined) { prepContent += `\n💰 Estimated Cost: ~$${event.estimatedCost.toFixed( 2 )} per execution\n`; } if (event.estimatedTime) { prepContent += `\n⏱️ Estimated Time: ${event.estimatedTime}\n`; } newContent = prepContent; break; } case 'planning_started': { // Show when planning mode begins if ('mode' in event && 'message' in event) { const modeLabel = event.mode === 'lite' ? 'Lite' : event.mode === 'spec' ? 'Spec' : 'Full'; newContent = `\n📋 Planning Mode: ${modeLabel}\n${event.message}\n`; } break; } case 'plan_approval_required': // Show when plan requires approval if ('planningMode' in event) { newContent = `\n⏸️ Plan generated - waiting for your approval...\n`; } break; case 'plan_approved': // Show when plan is manually approved if ('hasEdits' in event) { newContent = event.hasEdits ? `\n✅ Plan approved (with edits) - continuing to implementation...\n` : `\n✅ Plan approved - continuing to implementation...\n`; } break; case 'plan_auto_approved': // Show when plan is auto-approved newContent = `\n✅ Plan auto-approved - continuing to implementation...\n`; break; case 'plan_revision_requested': { // Show when user requests plan revision if ('planVersion' in event) { const revisionEvent = event as Extract< AutoModeEvent, { type: 'plan_revision_requested' } >; newContent = `\n🔄 Revising plan based on your feedback (v${revisionEvent.planVersion})...\n`; } break; } case 'auto_mode_task_started': { // Show when a task starts if ('taskId' in event && 'taskDescription' in event) { const taskEvent = event as Extract; newContent = `\n▶ Starting ${taskEvent.taskId}: ${taskEvent.taskDescription}\n`; } break; } case 'auto_mode_task_complete': { // Show task completion progress if ('taskId' in event && 'tasksCompleted' in event && 'tasksTotal' in event) { const taskEvent = event as Extract; newContent = `\n✓ ${taskEvent.taskId} completed (${taskEvent.tasksCompleted}/${taskEvent.tasksTotal})\n`; } break; } case 'auto_mode_phase_complete': { // Show phase completion for full mode if ('phaseNumber' in event) { const phaseEvent = event as Extract< AutoModeEvent, { type: 'auto_mode_phase_complete' } >; newContent = `\n🏁 Phase ${phaseEvent.phaseNumber} complete\n`; } break; } case 'auto_mode_feature_complete': { const emoji = event.passes ? '✅' : '⚠️'; newContent = `\n${emoji} Task completed: ${event.message}\n`; // Close the modal when the feature is verified (passes = true) if (event.passes) { // Small delay to show the completion message before closing setTimeout(() => { onClose(); }, 1500); } break; } } if (newContent) { // Only update local state - server is the single source of truth for file writes setOutput((prev) => prev + newContent); } }); return () => { unsubscribe(); }; }, [open, featureId, isBacklogPlan]); // Listen to backlog plan events and update output useEffect(() => { if (!open || !isBacklogPlan) return; const api = getElectronAPI(); if (!api?.backlogPlan) return; const unsubscribe = api.backlogPlan.onEvent((event: any) => { if (!event?.type) return; let newContent = ''; switch (event.type) { case 'backlog_plan_progress': newContent = `\n🧭 ${event.content || 'Backlog plan progress update'}\n`; break; case 'backlog_plan_error': newContent = `\n❌ Backlog plan error: ${event.error || 'Unknown error'}\n`; break; case 'backlog_plan_complete': newContent = `\n✅ Backlog plan completed\n`; break; default: newContent = `\nℹ️ ${event.type}\n`; break; } if (newContent) { setOutput((prev) => `${prev}${newContent}`); } }); return () => { unsubscribe(); }; }, [open, isBacklogPlan]); // Handle scroll to detect if user scrolled up const handleScroll = () => { if (!scrollRef.current) return; const { scrollTop, scrollHeight, clientHeight } = scrollRef.current; const isAtBottom = scrollHeight - scrollTop - clientHeight < 50; autoScrollRef.current = isAtBottom; }; // Handle number key presses while modal is open useEffect(() => { if (!open || !onNumberKeyPress) return; const handleKeyDown = (event: KeyboardEvent) => { // Check if a number key (0-9) was pressed without modifiers if (!event.ctrlKey && !event.altKey && !event.metaKey && /^[0-9]$/.test(event.key)) { event.preventDefault(); onNumberKeyPress(event.key); } }; window.addEventListener('keydown', handleKeyDown); return () => { window.removeEventListener('keydown', handleKeyDown); }; }, [open, onNumberKeyPress]); return (
{featureStatus !== 'verified' && featureStatus !== 'waiting_approval' && ( )} Agent Output
{summary && ( )}
{featureDescription}
{/* Task Progress Panel - shows when tasks are being executed */} {!isBacklogPlan && ( )} {effectiveViewMode === 'changes' ? (
{projectPath ? ( ) : (
Loading...
)}
) : effectiveViewMode === 'summary' && summary ? (
{summary}
) : ( <>
{isLoading && !output ? (
Loading output...
) : !output ? (
No output yet. The agent will stream output here as it works.
) : effectiveViewMode === 'parsed' ? ( ) : (
{output}
)}
{autoScrollRef.current ? 'Auto-scrolling enabled' : 'Scroll to bottom to enable auto-scroll'}
)}
); }