import { memo, useCallback, useMemo, useRef } from 'react'; import { MessageList } from './MessageList'; import { ChatInput } from './ChatInput'; import { StatusBar } from './StatusBar'; import { PermissionDialog } from './PermissionDialog'; import { useSessionManager } from '../contexts/SessionContext'; import { Bot } from 'lucide-react'; // Wrapper to compute placeholder inside and prevent parent re-renders from affecting input const MemoizedChatInput = memo(function MemoizedChatInput({ onSend, onStop, disabled, isProcessing, sessionId, connected, active }) { const placeholder = !connected ? 'Connecting...' : !active ? 'Start session to begin' : 'Type your message...'; return ( ); }, (prevProps, nextProps) => { // Custom comparison - only re-render if these specific props change return ( prevProps.disabled === nextProps.disabled && prevProps.isProcessing === nextProps.isProcessing && prevProps.sessionId === nextProps.sessionId && prevProps.connected === nextProps.connected && prevProps.active === nextProps.active && prevProps.onSend === nextProps.onSend && prevProps.onStop === nextProps.onStop ); }); // Welcome screen when no messages const WelcomeScreen = memo(function WelcomeScreen({ session, onStart }) { const hostName = session.host.charAt(0).toUpperCase() + session.host.slice(1); const projectName = session.project.split('/').pop() || session.project; return (

{hostName}: {projectName}

{session.active ? 'Session is active. Start chatting with Claude.' : 'Click "Start Session" in the sidebar or press the button below to begin.'}

{!session.active && ( )}
); }); // Error banner const ErrorBanner = memo(function ErrorBanner({ error, onClear }) { if (!error) return null; return (
{error}
); }); // Use a separate hook that memoizes everything to prevent unnecessary re-renders function useMemoizedSession(sessionId) { const { sessions, sessionMessages, startClaudeSession, stopClaudeSession, sendMessage, stopGeneration, clearMessages, changePermissionMode, respondToPermission, } = useSessionManager(); const session = sessions[sessionId]; const messages = sessionMessages[sessionId] || []; // Memoize the combined session object const sessionWithMessages = useMemo(() => { return session ? { ...session, messages } : null; }, [session, messages]); // Memoize all action functions - use individual functions as deps, not the whole manager const actions = useMemo(() => ({ start: () => startClaudeSession(sessionId), stop: () => stopClaudeSession(sessionId), send: (msg, attachments) => sendMessage(sessionId, msg, attachments), stopGeneration: () => stopGeneration(sessionId), clearMessages: () => clearMessages(sessionId), changePermissionMode: (mode) => changePermissionMode(sessionId, mode), respondToPermission: (reqId, allow) => respondToPermission(sessionId, reqId, allow), }), [sessionId, startClaudeSession, stopClaudeSession, sendMessage, stopGeneration, clearMessages, changePermissionMode, respondToPermission]); return { session: sessionWithMessages, ...actions }; } export const ChatPanel = memo(function ChatPanel({ sessionId }) { const { session, start, stop, send, stopGeneration, clearMessages, changePermissionMode, respondToPermission, } = useMemoizedSession(sessionId); const handleClearError = useCallback(() => { // We'd need to add this to the session context // For now, errors auto-clear on next action }, []); // Use refs for callbacks to keep them stable across re-renders const sendRef = useRef(send); sendRef.current = send; const stopGenerationRef = useRef(stopGeneration); stopGenerationRef.current = stopGeneration; // These callbacks never change identity, preventing ChatInput re-renders const handleSendMessage = useCallback((message, attachments = []) => { if (message.trim() || attachments.length > 0) { sendRef.current(message, attachments); } }, []); const handleStopGeneration = useCallback(() => { stopGenerationRef.current(); }, []); if (!session) { return (
Session not found
); } const hasMessages = session.messages && session.messages.length > 0; return (
{/* Error Banner */} {/* Messages or Welcome */} {hasMessages || session.active ? ( ) : ( )} {/* Status Bar */} changePermissionMode(mode)} /> {/* Input - memoized props to prevent re-renders during streaming */} {/* Permission Dialog */} respondToPermission(requestId, true)} onDeny={(requestId) => respondToPermission(requestId, false)} />
); });