import { html, useState, useEffect, useRef } from '../lib/preact.js'; import { getStatusMeta, getUserMessageBg } from '../utils/status.js'; import { formatDuration, formatTime } from '../utils/formatting.js'; import { MessageBubble, filterDisplayMessages } from './MessageBubble.js'; export function Modal({ session, conversations, conversationLoading, onClose, onSendMessage, onRefreshConversation }) { const [inputValue, setInputValue] = useState(''); const [sending, setSending] = useState(false); const [inputFocused, setInputFocused] = useState(false); const inputRef = useRef(null); if (!session) return null; const conversation = conversations[session.session_id] || []; const hasPendingQuestions = session.pending_questions && session.pending_questions.length > 0; const optionCount = hasPendingQuestions ? session.pending_questions[0]?.options?.length || 0 : 0; const status = getStatusMeta(session.status); const agent = session.agent === 'codex' ? 'codex' : 'claude'; const agentHeaderClass = agent === 'codex' ? 'agent-header-codex' : 'agent-header-claude'; // Track if user has scrolled away from bottom const wasAtBottomRef = useRef(true); const prevConversationLenRef = useRef(0); const chatContainerRef = useRef(null); // Initialize scroll position to bottom on mount (no animation) useEffect(() => { if (chatContainerRef.current) { chatContainerRef.current.scrollTop = chatContainerRef.current.scrollHeight; } }, []); // Track scroll position useEffect(() => { const container = chatContainerRef.current; if (!container) return; const handleScroll = () => { const threshold = 50; wasAtBottomRef.current = container.scrollHeight - container.scrollTop - container.clientHeight < threshold; }; container.addEventListener('scroll', handleScroll); return () => container.removeEventListener('scroll', handleScroll); }, []); // Only scroll to bottom on NEW messages, and only if user was already at bottom useEffect(() => { const container = chatContainerRef.current; if (!container || !conversation) return; const hasNewMessages = conversation.length > prevConversationLenRef.current; prevConversationLenRef.current = conversation.length; if (hasNewMessages && wasAtBottomRef.current) { container.scrollTop = container.scrollHeight; } }, [conversation]); // Focus input when modal opens useEffect(() => { if (inputRef.current) { inputRef.current.focus(); } }, [session]); // Lock body scroll when modal is open useEffect(() => { document.body.style.overflow = 'hidden'; return () => { document.body.style.overflow = ''; }; }, []); // Handle keyboard events useEffect(() => { const handleKeyDown = (e) => { // Escape closes modal if (e.key === 'Escape') { onClose(); } }; document.addEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown); }, [onClose]); // Handle input key events const handleInputKeyDown = (e) => { // Enter sends message (unless Shift+Enter for newline) if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handleSend(); } }; // Send message const handleSend = async () => { const text = inputValue.trim(); if (!text || sending) return; setSending(true); try { if (onSendMessage) { await onSendMessage(session.session_id, text, true, optionCount); } setInputValue(''); // Refresh conversation after sending if (onRefreshConversation) { await onRefreshConversation(session.session_id, session.project_dir, agent); } } catch (err) { console.error('Failed to send message:', err); } finally { setSending(false); } }; return html`
No conversation messages
`}