/* * ChatInterface.jsx - Chat Component with Session Protection Integration * * SESSION PROTECTION INTEGRATION: * =============================== * * This component integrates with the Session Protection System to prevent project updates * from interrupting active conversations: * * Key Integration Points: * 1. handleSubmit() - Marks session as active when user sends message (including temp ID for new sessions) * 2. session-created handler - Replaces temporary session ID with real WebSocket session ID * 3. claude-complete handler - Marks session as inactive when conversation finishes * 4. session-aborted handler - Marks session as inactive when conversation is aborted * * This ensures uninterrupted chat experience by coordinating with App.jsx to pause sidebar updates. */ import React, { useState, useEffect, useRef, useMemo, useCallback, memo } from 'react'; import ReactMarkdown from 'react-markdown'; import { useDropzone } from 'react-dropzone'; import TodoList from './TodoList'; import ClaudeLogo from './ClaudeLogo.jsx'; import ClaudeStatus from './ClaudeStatus'; import { MicButton } from './MicButton.jsx'; import { api } from '../utils/api'; // Safe localStorage utility to handle quota exceeded errors const safeLocalStorage = { setItem: (key, value) => { try { // For chat messages, implement compression and size limits if (key.startsWith('chat_messages_') && typeof value === 'string') { try { const parsed = JSON.parse(value); // Limit to last 50 messages to prevent storage bloat if (Array.isArray(parsed) && parsed.length > 50) { console.warn(`Truncating chat history for ${key} from ${parsed.length} to 50 messages`); const truncated = parsed.slice(-50); value = JSON.stringify(truncated); } } catch (parseError) { console.warn('Could not parse chat messages for truncation:', parseError); } } localStorage.setItem(key, value); } catch (error) { if (error.name === 'QuotaExceededError') { console.warn('localStorage quota exceeded, clearing old data'); // Clear old chat messages to free up space const keys = Object.keys(localStorage); const chatKeys = keys.filter(k => k.startsWith('chat_messages_')).sort(); // Remove oldest chat data first, keeping only the 3 most recent projects if (chatKeys.length > 3) { chatKeys.slice(0, chatKeys.length - 3).forEach(k => { localStorage.removeItem(k); console.log(`Removed old chat data: ${k}`); }); } // If still failing, clear draft inputs too const draftKeys = keys.filter(k => k.startsWith('draft_input_')); draftKeys.forEach(k => { localStorage.removeItem(k); }); // Try again with reduced data try { localStorage.setItem(key, value); } catch (retryError) { console.error('Failed to save to localStorage even after cleanup:', retryError); // Last resort: Try to save just the last 10 messages if (key.startsWith('chat_messages_') && typeof value === 'string') { try { const parsed = JSON.parse(value); if (Array.isArray(parsed) && parsed.length > 10) { const minimal = parsed.slice(-10); localStorage.setItem(key, JSON.stringify(minimal)); console.warn('Saved only last 10 messages due to quota constraints'); } } catch (finalError) { console.error('Final save attempt failed:', finalError); } } } } else { console.error('localStorage error:', error); } } }, getItem: (key) => { try { return localStorage.getItem(key); } catch (error) { console.error('localStorage getItem error:', error); return null; } }, removeItem: (key) => { try { localStorage.removeItem(key); } catch (error) { console.error('localStorage removeItem error:', error); } } }; // Memoized message component to prevent unnecessary re-renders const MessageComponent = memo(({ message, index, prevMessage, createDiff, onFileOpen, onShowSettings, autoExpandTools, showRawParameters }) => { const isGrouped = prevMessage && prevMessage.type === message.type && prevMessage.type === 'assistant' && !prevMessage.isToolUse && !message.isToolUse; const messageRef = React.useRef(null); const [isExpanded, setIsExpanded] = React.useState(false); React.useEffect(() => { if (!autoExpandTools || !messageRef.current || !message.isToolUse) return; const observer = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (entry.isIntersecting && !isExpanded) { setIsExpanded(true); // Find all details elements and open them const details = messageRef.current.querySelectorAll('details'); details.forEach(detail => { detail.open = true; }); } }); }, { threshold: 0.1 } ); observer.observe(messageRef.current); return () => { if (messageRef.current) { observer.unobserve(messageRef.current); } }; }, [autoExpandTools, isExpanded, message.isToolUse]); return (
{message.toolInput}
{message.toolInput}
{message.toolInput}
{message.toolInput}
{message.toolInput}
{message.toolInput}
{beforePrompt}
{questionLine}
{/* Option buttons */}✓ Claude selected option {selectedOption}
In the CLI, you would select this option interactively using arrow keys or by typing the number.
The file content is displayed in the diff view above
{questionLine}
{/* Option buttons */}⏳ Waiting for your response in the CLI
Please select an option in your terminal where Claude is running.
{children}
{children}), a: ({href, children}) => ( {children} ), p: ({children}) => (
Select a project to start chatting with Claude
Loading session messages...
Start a conversation with Claude
Ask questions about your code, request changes, or get help with development tasks