import React, { useCallback, useEffect, useRef } from 'react'; import QuickSettingsPanel from './QuickSettingsPanel'; import { useTasksSettings } from '../contexts/TasksSettingsContext'; import { useTranslation } from 'react-i18next'; import ChatMessagesPane from './chat/view/ChatMessagesPane'; import ChatComposer from './chat/view/ChatComposer'; import type { ChatInterfaceProps } from './chat/types'; import { useChatProviderState } from '../hooks/chat/useChatProviderState'; import { useChatSessionState } from '../hooks/chat/useChatSessionState'; import { useChatRealtimeHandlers } from '../hooks/chat/useChatRealtimeHandlers'; import { useChatComposerState } from '../hooks/chat/useChatComposerState'; import type { Provider } from './chat/types'; type PendingViewSession = { sessionId: string | null; startedAt: number; }; function ChatInterface({ selectedProject, selectedSession, ws, sendMessage, latestMessage, onFileOpen, onInputFocusChange, onSessionActive, onSessionInactive, onSessionProcessing, onSessionNotProcessing, processingSessions, onReplaceTemporarySession, onNavigateToSession, onShowSettings, autoExpandTools, showRawParameters, showThinking, autoScrollToBottom, sendByCtrlEnter, externalMessageUpdate, onShowAllTasks, }: ChatInterfaceProps) { const { tasksEnabled, isTaskMasterInstalled } = useTasksSettings(); const { t } = useTranslation('chat'); const streamBufferRef = useRef(''); const streamTimerRef = useRef(null); const pendingViewSessionRef = useRef(null); const resetStreamingState = useCallback(() => { if (streamTimerRef.current) { clearTimeout(streamTimerRef.current); streamTimerRef.current = null; } streamBufferRef.current = ''; }, []); const { provider, setProvider, cursorModel, setCursorModel, claudeModel, setClaudeModel, codexModel, setCodexModel, permissionMode, pendingPermissionRequests, setPendingPermissionRequests, cyclePermissionMode, } = useChatProviderState({ selectedSession, }); const { chatMessages, setChatMessages, isLoading, setIsLoading, currentSessionId, setCurrentSessionId, sessionMessages, setSessionMessages, isLoadingSessionMessages, isLoadingMoreMessages, hasMoreMessages, totalMessages, isSystemSessionChange, setIsSystemSessionChange, canAbortSession, setCanAbortSession, isUserScrolledUp, setIsUserScrolledUp, tokenBudget, setTokenBudget, visibleMessageCount, visibleMessages, loadEarlierMessages, claudeStatus, setClaudeStatus, createDiff, scrollContainerRef, scrollToBottom, handleScroll, } = useChatSessionState({ selectedProject, selectedSession, ws, sendMessage, autoScrollToBottom, externalMessageUpdate, processingSessions, resetStreamingState, pendingViewSessionRef, }); const { input, setInput, textareaRef, inputHighlightRef, isTextareaExpanded, thinkingMode, setThinkingMode, slashCommandsCount, filteredCommands, frequentCommands, commandQuery, showCommandMenu, selectedCommandIndex, resetCommandMenuState, handleCommandSelect, handleToggleCommandMenu, showFileDropdown, filteredFiles, selectedFileIndex, renderInputWithMentions, selectFile, attachedImages, setAttachedImages, uploadingImages, imageErrors, getRootProps, getInputProps, isDragActive, openImagePicker, handleSubmit, handleInputChange, handleKeyDown, handlePaste, handleTextareaClick, handleTextareaInput, syncInputOverlayScroll, handleClearInput, handleAbortSession, handleTranscript, handlePermissionDecision, handleGrantToolPermission, handleInputFocusChange, } = useChatComposerState({ selectedProject, selectedSession, currentSessionId, provider, permissionMode, cyclePermissionMode, cursorModel, claudeModel, codexModel, isLoading, canAbortSession, tokenBudget, sendMessage, sendByCtrlEnter, onSessionActive, onInputFocusChange, onFileOpen, onShowSettings, pendingViewSessionRef, scrollToBottom, setChatMessages, setSessionMessages, setIsLoading, setCanAbortSession, setClaudeStatus, setIsUserScrolledUp, setPendingPermissionRequests, }); useChatRealtimeHandlers({ latestMessage, provider, selectedProject, selectedSession, currentSessionId, setCurrentSessionId, setChatMessages, setIsLoading, setCanAbortSession, setClaudeStatus, setTokenBudget, setIsSystemSessionChange, setPendingPermissionRequests, pendingViewSessionRef, streamBufferRef, streamTimerRef, onSessionInactive, onSessionProcessing, onSessionNotProcessing, onReplaceTemporarySession, onNavigateToSession, }); useEffect(() => { if (!isLoading || !canAbortSession) { return; } const handleGlobalEscape = (event: KeyboardEvent) => { if (event.key !== 'Escape' || event.repeat || event.defaultPrevented) { return; } event.preventDefault(); handleAbortSession(); }; document.addEventListener('keydown', handleGlobalEscape, { capture: true }); return () => { document.removeEventListener('keydown', handleGlobalEscape, { capture: true }); }; }, [canAbortSession, handleAbortSession, isLoading]); useEffect(() => { if (currentSessionId && isLoading && onSessionProcessing) { onSessionProcessing(currentSessionId); } }, [currentSessionId, isLoading, onSessionProcessing]); useEffect(() => { return () => { resetStreamingState(); }; }, [resetStreamingState]); if (!selectedProject) { return (

Select a project to start chatting with Claude

); } return ( <>
setProvider(nextProvider as Provider)} textareaRef={textareaRef} claudeModel={claudeModel} setClaudeModel={setClaudeModel} cursorModel={cursorModel} setCursorModel={setCursorModel} codexModel={codexModel} setCodexModel={setCodexModel} tasksEnabled={tasksEnabled} isTaskMasterInstalled={isTaskMasterInstalled} onShowAllTasks={onShowAllTasks} setInput={setInput} isLoadingMoreMessages={isLoadingMoreMessages} hasMoreMessages={hasMoreMessages} totalMessages={totalMessages} sessionMessagesCount={sessionMessages.length} visibleMessageCount={visibleMessageCount} visibleMessages={visibleMessages} loadEarlierMessages={loadEarlierMessages} createDiff={createDiff} onFileOpen={onFileOpen} onShowSettings={onShowSettings} onGrantToolPermission={handleGrantToolPermission} autoExpandTools={autoExpandTools} showRawParameters={showRawParameters} showThinking={showThinking} selectedProject={selectedProject} isLoading={isLoading} /> 0} onScrollToBottom={scrollToBottom} onSubmit={handleSubmit} isDragActive={isDragActive} attachedImages={attachedImages} onRemoveImage={(index) => setAttachedImages((previous) => previous.filter((_, currentIndex) => currentIndex !== index), ) } uploadingImages={uploadingImages} imageErrors={imageErrors} showFileDropdown={showFileDropdown} filteredFiles={filteredFiles} selectedFileIndex={selectedFileIndex} onSelectFile={selectFile} filteredCommands={filteredCommands} selectedCommandIndex={selectedCommandIndex} onCommandSelect={handleCommandSelect} onCloseCommandMenu={resetCommandMenuState} isCommandMenuOpen={showCommandMenu} frequentCommands={commandQuery ? [] : frequentCommands} getRootProps={getRootProps as (...args: unknown[]) => Record} getInputProps={getInputProps as (...args: unknown[]) => Record} openImagePicker={openImagePicker} inputHighlightRef={inputHighlightRef} renderInputWithMentions={renderInputWithMentions} textareaRef={textareaRef} input={input} onInputChange={handleInputChange} onTextareaClick={handleTextareaClick} onTextareaKeyDown={handleKeyDown} onTextareaPaste={handlePaste} onTextareaScrollSync={syncInputOverlayScroll} onTextareaInput={handleTextareaInput} onInputFocusChange={handleInputFocusChange} placeholder={t('input.placeholder', { provider: provider === 'cursor' ? t('messageTypes.cursor') : provider === 'codex' ? t('messageTypes.codex') : t('messageTypes.claude'), })} isTextareaExpanded={isTextareaExpanded} sendByCtrlEnter={sendByCtrlEnter} onTranscript={handleTranscript} />
); } export default React.memo(ChatInterface);