import CommandMenu from './CommandMenu'; import ClaudeStatus from './ClaudeStatus'; import { MicButton } from '../../../MicButton.jsx'; import ImageAttachment from './ImageAttachment'; import PermissionRequestsBanner from './PermissionRequestsBanner'; import ChatInputControls from './ChatInputControls'; import { useTranslation } from 'react-i18next'; import type { ChangeEvent, ClipboardEvent, Dispatch, FormEvent, KeyboardEvent, MouseEvent, ReactNode, RefObject, SetStateAction, TouchEvent, } from 'react'; import type { PendingPermissionRequest, PermissionMode, Provider } from '../../types/types'; interface MentionableFile { name: string; path: string; } interface SlashCommand { name: string; description?: string; namespace?: string; path?: string; type?: string; metadata?: Record; [key: string]: unknown; } interface ChatComposerProps { pendingPermissionRequests: PendingPermissionRequest[]; handlePermissionDecision: ( requestIds: string | string[], decision: { allow?: boolean; message?: string; rememberEntry?: string | null; updatedInput?: unknown }, ) => void; handleGrantToolPermission: (suggestion: { entry: string; toolName: string }) => { success: boolean }; claudeStatus: { text: string; tokens: number; can_interrupt: boolean } | null; isLoading: boolean; onAbortSession: () => void; provider: Provider | string; permissionMode: PermissionMode | string; onModeSwitch: () => void; thinkingMode: string; setThinkingMode: Dispatch>; tokenBudget: { used?: number; total?: number } | null; slashCommandsCount: number; onToggleCommandMenu: () => void; hasInput: boolean; onClearInput: () => void; isUserScrolledUp: boolean; hasMessages: boolean; onScrollToBottom: () => void; onSubmit: (event: FormEvent | MouseEvent | TouchEvent) => void; isDragActive: boolean; attachedImages: File[]; onRemoveImage: (index: number) => void; uploadingImages: Map; imageErrors: Map; showFileDropdown: boolean; filteredFiles: MentionableFile[]; selectedFileIndex: number; onSelectFile: (file: MentionableFile) => void; filteredCommands: SlashCommand[]; selectedCommandIndex: number; onCommandSelect: (command: SlashCommand, index: number, isHover: boolean) => void; onCloseCommandMenu: () => void; isCommandMenuOpen: boolean; frequentCommands: SlashCommand[]; getRootProps: (...args: unknown[]) => Record; getInputProps: (...args: unknown[]) => Record; openImagePicker: () => void; inputHighlightRef: RefObject; renderInputWithMentions: (text: string) => ReactNode; textareaRef: RefObject; input: string; onInputChange: (event: ChangeEvent) => void; onTextareaClick: (event: MouseEvent) => void; onTextareaKeyDown: (event: KeyboardEvent) => void; onTextareaPaste: (event: ClipboardEvent) => void; onTextareaScrollSync: (target: HTMLTextAreaElement) => void; onTextareaInput: (event: FormEvent) => void; onInputFocusChange?: (focused: boolean) => void; isInputFocused?: boolean; placeholder: string; isTextareaExpanded: boolean; sendByCtrlEnter?: boolean; onTranscript: (text: string) => void; } export default function ChatComposer({ pendingPermissionRequests, handlePermissionDecision, handleGrantToolPermission, claudeStatus, isLoading, onAbortSession, provider, permissionMode, onModeSwitch, thinkingMode, setThinkingMode, tokenBudget, slashCommandsCount, onToggleCommandMenu, hasInput, onClearInput, isUserScrolledUp, hasMessages, onScrollToBottom, onSubmit, isDragActive, attachedImages, onRemoveImage, uploadingImages, imageErrors, showFileDropdown, filteredFiles, selectedFileIndex, onSelectFile, filteredCommands, selectedCommandIndex, onCommandSelect, onCloseCommandMenu, isCommandMenuOpen, frequentCommands, getRootProps, getInputProps, openImagePicker, inputHighlightRef, renderInputWithMentions, textareaRef, input, onInputChange, onTextareaClick, onTextareaKeyDown, onTextareaPaste, onTextareaScrollSync, onTextareaInput, onInputFocusChange, isInputFocused, placeholder, isTextareaExpanded, sendByCtrlEnter, onTranscript, }: ChatComposerProps) { const { t } = useTranslation('chat'); const textareaRect = textareaRef.current?.getBoundingClientRect(); const commandMenuPosition = { top: textareaRect ? Math.max(16, textareaRect.top - 316) : 0, left: textareaRect ? textareaRect.left : 16, bottom: textareaRect ? window.innerHeight - textareaRect.top + 8 : 90, }; // Detect if the AskUserQuestion interactive panel is active const hasQuestionPanel = pendingPermissionRequests.some( (r) => r.toolName === 'AskUserQuestion' ); // On mobile, when input is focused, float the input box at the bottom const mobileFloatingClass = isInputFocused ? 'max-sm:fixed max-sm:bottom-0 max-sm:left-0 max-sm:right-0 max-sm:z-50 max-sm:bg-background max-sm:shadow-[0_-4px_20px_rgba(0,0,0,0.15)]' : ''; return (
{!hasQuestionPanel && (
)}
{!hasQuestionPanel && }
{!hasQuestionPanel &&
) => void} className="relative max-w-4xl mx-auto"> {isDragActive && (

Drop images here

)} {attachedImages.length > 0 && (
{attachedImages.map((file, index) => ( onRemoveImage(index)} uploadProgress={uploadingImages.get(file.name)} error={imageErrors.get(file.name)} /> ))}
)} {showFileDropdown && filteredFiles.length > 0 && (
{filteredFiles.map((file, index) => (
{ event.preventDefault(); event.stopPropagation(); }} onClick={(event) => { event.preventDefault(); event.stopPropagation(); onSelectFile(file); }} >
{file.name}
{file.path}
))}
)}