"use client"; import { useState, useCallback, useRef, useEffect, useMemo } from "react"; import { useAppStore, type AgentModel } from "@/store/app-store"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { ImageDropZone } from "@/components/ui/image-drop-zone"; import { Bot, Send, User, Loader2, Sparkles, Wrench, Trash2, PanelLeftClose, PanelLeft, Paperclip, X, ImageIcon, ChevronDown, } from "lucide-react"; import { cn } from "@/lib/utils"; import { useElectronAgent } from "@/hooks/use-electron-agent"; import { SessionManager } from "@/components/session-manager"; import { Markdown } from "@/components/ui/markdown"; import type { ImageAttachment } from "@/store/app-store"; import { useKeyboardShortcuts, useKeyboardShortcutsConfig, KeyboardShortcut, } from "@/hooks/use-keyboard-shortcuts"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { CLAUDE_MODELS } from "@/components/views/board-view/shared/model-constants"; export function AgentView() { const { currentProject, setLastSelectedSession, getLastSelectedSession } = useAppStore(); const shortcuts = useKeyboardShortcutsConfig(); const [input, setInput] = useState(""); const [selectedImages, setSelectedImages] = useState([]); const [showImageDropZone, setShowImageDropZone] = useState(false); const [currentTool, setCurrentTool] = useState(null); const [currentSessionId, setCurrentSessionId] = useState(null); const [showSessionManager, setShowSessionManager] = useState(true); const [isDragOver, setIsDragOver] = useState(false); const [selectedModel, setSelectedModel] = useState("sonnet"); // Track if initial session has been loaded const initialSessionLoadedRef = useRef(false); // Scroll management for auto-scroll const messagesContainerRef = useRef(null); const [isUserAtBottom, setIsUserAtBottom] = useState(true); // Input ref for auto-focus const inputRef = useRef(null); // Ref for quick create session function from SessionManager const quickCreateSessionRef = useRef<(() => Promise) | null>(null); // Use the Electron agent hook (only if we have a session) const { messages, isProcessing, isConnected, sendMessage, clearHistory, error: agentError, } = useElectronAgent({ sessionId: currentSessionId || "", workingDirectory: currentProject?.path, model: selectedModel, onToolUse: (toolName) => { setCurrentTool(toolName); setTimeout(() => setCurrentTool(null), 2000); }, }); // Handle session selection with persistence const handleSelectSession = useCallback( (sessionId: string | null) => { setCurrentSessionId(sessionId); // Persist the selection for this project if (currentProject?.path) { setLastSelectedSession(currentProject.path, sessionId); } }, [currentProject?.path, setLastSelectedSession] ); // Restore last selected session when switching to Agent view or when project changes useEffect(() => { if (!currentProject?.path) { // No project, reset setCurrentSessionId(null); initialSessionLoadedRef.current = false; return; } // Only restore once per project if (initialSessionLoadedRef.current) return; initialSessionLoadedRef.current = true; const lastSessionId = getLastSelectedSession(currentProject.path); if (lastSessionId) { console.log( "[AgentView] Restoring last selected session:", lastSessionId ); setCurrentSessionId(lastSessionId); } }, [currentProject?.path, getLastSelectedSession]); // Reset initialSessionLoadedRef when project changes useEffect(() => { initialSessionLoadedRef.current = false; }, [currentProject?.path]); const handleSend = useCallback(async () => { if ((!input.trim() && selectedImages.length === 0) || isProcessing) return; const messageContent = input; const messageImages = selectedImages; setInput(""); setSelectedImages([]); setShowImageDropZone(false); await sendMessage(messageContent, messageImages); }, [input, selectedImages, isProcessing, sendMessage]); const handleImagesSelected = useCallback((images: ImageAttachment[]) => { setSelectedImages(images); }, []); const toggleImageDropZone = useCallback(() => { setShowImageDropZone(!showImageDropZone); }, [showImageDropZone]); // Helper function to convert file to base64 const fileToBase64 = useCallback((file: File): Promise => { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => { if (typeof reader.result === "string") { resolve(reader.result); } else { reject(new Error("Failed to read file as base64")); } }; reader.onerror = () => reject(new Error("Failed to read file")); reader.readAsDataURL(file); }); }, []); // Process dropped files const processDroppedFiles = useCallback( async (files: FileList) => { if (isProcessing) return; const ACCEPTED_IMAGE_TYPES = [ "image/jpeg", "image/jpg", "image/png", "image/gif", "image/webp", ]; const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10MB const MAX_FILES = 5; const newImages: ImageAttachment[] = []; const errors: string[] = []; for (const file of Array.from(files)) { // Validate file type if (!ACCEPTED_IMAGE_TYPES.includes(file.type)) { errors.push( `${file.name}: Unsupported file type. Please use JPG, PNG, GIF, or WebP.` ); continue; } // Validate file size if (file.size > MAX_FILE_SIZE) { const maxSizeMB = MAX_FILE_SIZE / (1024 * 1024); errors.push( `${file.name}: File too large. Maximum size is ${maxSizeMB}MB.` ); continue; } // Check if we've reached max files if (newImages.length + selectedImages.length >= MAX_FILES) { errors.push(`Maximum ${MAX_FILES} images allowed.`); break; } try { const base64 = await fileToBase64(file); const imageAttachment: ImageAttachment = { id: `img-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, data: base64, mimeType: file.type, filename: file.name, size: file.size, }; newImages.push(imageAttachment); } catch (error) { errors.push(`${file.name}: Failed to process image.`); } } if (errors.length > 0) { console.warn("Image upload errors:", errors); } if (newImages.length > 0) { setSelectedImages((prev) => [...prev, ...newImages]); } }, [isProcessing, selectedImages, fileToBase64] ); // Remove individual image const removeImage = useCallback((imageId: string) => { setSelectedImages((prev) => prev.filter((img) => img.id !== imageId)); }, []); // Drag and drop handlers for the input area const handleDragEnter = useCallback( (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); if (isProcessing || !isConnected) return; // Check if dragged items contain files if (e.dataTransfer.types.includes("Files")) { setIsDragOver(true); } }, [isProcessing, isConnected] ); const handleDragLeave = useCallback((e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); // Only set dragOver to false if we're leaving the input container const rect = e.currentTarget.getBoundingClientRect(); const x = e.clientX; const y = e.clientY; if (x < rect.left || x > rect.right || y < rect.top || y > rect.bottom) { setIsDragOver(false); } }, []); const handleDragOver = useCallback((e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); }, []); const handleDrop = useCallback( async (e: React.DragEvent) => { e.preventDefault(); e.stopPropagation(); setIsDragOver(false); if (isProcessing || !isConnected) return; // Check if we have files const files = e.dataTransfer.files; if (files && files.length > 0) { processDroppedFiles(files); return; } // Handle file paths (from screenshots or other sources) const items = e.dataTransfer.items; if (items && items.length > 0) { for (let i = 0; i < items.length; i++) { const item = items[i]; if (item.kind === "file") { const file = item.getAsFile(); if (file) { const dataTransfer = new DataTransfer(); dataTransfer.items.add(file); processDroppedFiles(dataTransfer.files); } } } } }, [isProcessing, isConnected, processDroppedFiles] ); const handlePaste = useCallback( async (e: React.ClipboardEvent) => { // Check if clipboard contains files const items = e.clipboardData?.items; if (items) { const files: File[] = []; for (let i = 0; i < items.length; i++) { const item = items[i]; if (item.kind === "file") { const file = item.getAsFile(); if (file && file.type.startsWith("image/")) { e.preventDefault(); // Prevent default paste of file path files.push(file); } } } if (files.length > 0) { const dataTransfer = new DataTransfer(); files.forEach((file) => dataTransfer.items.add(file)); await processDroppedFiles(dataTransfer.files); } } }, [processDroppedFiles] ); const handleKeyPress = (e: React.KeyboardEvent) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); handleSend(); } }; const handleClearChat = async () => { if (!confirm("Are you sure you want to clear this conversation?")) return; await clearHistory(); }; // Scroll position detection const checkIfUserIsAtBottom = useCallback(() => { const container = messagesContainerRef.current; if (!container) return; const threshold = 50; // 50px threshold for "near bottom" const isAtBottom = container.scrollHeight - container.scrollTop - container.clientHeight <= threshold; setIsUserAtBottom(isAtBottom); }, []); // Scroll to bottom function const scrollToBottom = useCallback((behavior: ScrollBehavior = "smooth") => { const container = messagesContainerRef.current; if (!container) return; container.scrollTo({ top: container.scrollHeight, behavior: behavior, }); }, []); // Handle scroll events const handleScroll = useCallback(() => { checkIfUserIsAtBottom(); }, [checkIfUserIsAtBottom]); // Auto-scroll effect when messages change useEffect(() => { // Only auto-scroll if user was already at bottom if (isUserAtBottom && messages.length > 0) { // Use a small delay to ensure DOM is updated setTimeout(() => { scrollToBottom("smooth"); }, 100); } }, [messages, isUserAtBottom, scrollToBottom]); // Initial scroll to bottom when session changes useEffect(() => { if (currentSessionId && messages.length > 0) { // Scroll immediately without animation when switching sessions setTimeout(() => { scrollToBottom("auto"); setIsUserAtBottom(true); }, 100); } }, [currentSessionId, scrollToBottom]); // Auto-focus input when session is selected/changed useEffect(() => { if (currentSessionId && inputRef.current) { // Small delay to ensure UI has updated setTimeout(() => { inputRef.current?.focus(); }, 200); } }, [currentSessionId]); // Keyboard shortcuts for agent view const agentShortcuts: KeyboardShortcut[] = useMemo(() => { const shortcutsList: KeyboardShortcut[] = []; // New session shortcut - only when in agent view with a project if (currentProject) { shortcutsList.push({ key: shortcuts.newSession, action: () => { if (quickCreateSessionRef.current) { quickCreateSessionRef.current(); } }, description: "Create new session", }); } return shortcutsList; }, [currentProject, shortcuts]); // Register keyboard shortcuts useKeyboardShortcuts(agentShortcuts); if (!currentProject) { return (

No Project Selected

Open or create a project to start working with the AI agent.

); } // Show welcome message if no messages yet const displayMessages = messages.length === 0 ? [ { id: "welcome", role: "assistant" as const, content: "Hello! I'm the Automaker Agent. I can help you build software autonomously. I can read and modify files in this project, run commands, and execute tests. What would you like to create today?", timestamp: new Date().toISOString(), }, ] : messages; return (
{/* Session Manager Sidebar */} {showSessionManager && currentProject && (
)} {/* Chat Area */}
{/* Header */}

AI Agent

{currentProject.name} {currentSessionId && !isConnected && " - Connecting..."}

{/* Status indicators & actions */}
{/* Model Selector */} {CLAUDE_MODELS.map((model) => ( setSelectedModel(model.id)} className={cn( "cursor-pointer", selectedModel === model.id && "bg-accent" )} data-testid={`model-option-${model.id}`} >
{model.label} {model.description}
))}
{currentTool && (
{currentTool}
)} {agentError && ( {agentError} )} {currentSessionId && messages.length > 0 && ( )}
{/* Messages */} {!currentSessionId ? (

No Session Selected

Create or select a session to start chatting with the AI agent

) : (
{displayMessages.map((message) => (
{/* Avatar */}
{message.role === "assistant" ? ( ) : ( )}
{/* Message Bubble */}
{message.role === "assistant" ? ( {message.content} ) : (

{message.content}

)} {/* Display attached images for user messages */} {message.role === "user" && message.images && message.images.length > 0 && (
{message.images.length} image {message.images.length > 1 ? "s" : ""} attached
{message.images.map((image, index) => { // Construct proper data URL from base64 data and mime type const dataUrl = image.data.startsWith("data:") ? image.data : `data:${image.mimeType || "image/png"};base64,${ image.data }`; return (
{
{image.filename || `Image ${index + 1}`}
); })}
)}

{new Date(message.timestamp).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit", })}

))} {/* Thinking Indicator */} {isProcessing && (
Thinking...
)}
)} {/* Input Area */} {currentSessionId && (
{/* Image Drop Zone (when visible) */} {showImageDropZone && ( )} {/* Selected Images Preview */} {selectedImages.length > 0 && (

{selectedImages.length} image {selectedImages.length > 1 ? "s" : ""} attached

{selectedImages.map((image) => (
{/* Image thumbnail */}
{image.filename}
{/* Image info */}

{image.filename}

{image.size !== undefined && (

{formatFileSize(image.size)}

)}
{/* Remove button */} {image.id && ( )}
))}
)} {/* Text Input and Controls */}
setInput(e.target.value)} onKeyPress={handleKeyPress} onPaste={handlePaste} disabled={isProcessing || !isConnected} data-testid="agent-input" className={cn( "h-11 bg-background border-border rounded-xl pl-4 pr-20 text-sm transition-all", "focus:ring-2 focus:ring-primary/20 focus:border-primary/50", selectedImages.length > 0 && "border-primary/30", isDragOver && "border-primary bg-primary/5" )} /> {selectedImages.length > 0 && !isDragOver && (
{selectedImages.length} image {selectedImages.length > 1 ? "s" : ""}
)} {isDragOver && (
Drop here
)}
{/* Image Attachment Button */} {/* Send Button */}
{/* Keyboard hint */}

Press{" "} Enter {" "} to send

)}
); } // Helper function to format file size function formatFileSize(bytes: number): string { if (bytes === 0) return "0 B"; const k = 1024; const sizes = ["B", "KB", "MB", "GB"]; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(1)) + " " + sizes[i]; }