diff --git a/frontend/src/components/MessageList.jsx b/frontend/src/components/MessageList.jsx index bf1db66..1c3bddf 100644 --- a/frontend/src/components/MessageList.jsx +++ b/frontend/src/components/MessageList.jsx @@ -604,10 +604,71 @@ const COLOR_CLASSES = { // Note: PlanApprovalCard removed - ExitPlanMode approval now handled via PermissionDialog modal +// Separate component for custom input - uses uncontrolled input for lag-free typing +const CustomInputSection = memo(function CustomInputSection({ onSubmit, hasSelection }) { + const textareaRef = useRef(null); + const [hasText, setHasText] = useState(false); + + const canSubmit = hasSelection || hasText; + + const handleSubmit = useCallback(() => { + const value = textareaRef.current?.value?.trim() || ''; + if (value) { + onSubmit(value); + if (textareaRef.current) { + textareaRef.current.value = ''; + setHasText(false); + } + } else { + onSubmit(null); // Signal to use selected options + } + }, [onSubmit]); + + // Only track whether there's text, not the actual content (for button state) + const handleInput = useCallback(() => { + const value = textareaRef.current?.value?.trim() || ''; + setHasText(value.length > 0); + }, []); + + return ( +
+ {/* Custom input field - uncontrolled for performance */} +
+ + +
+ + {/* Submit button */} + +
+ ); +}); + const ToolUseCard = memo(function ToolUseCard({ tool, input, result, onSendMessage }) { const [showResult, setShowResult] = useState(false); const [selectedOptions, setSelectedOptions] = useState({}); - const [customInput, setCustomInput] = useState(''); const config = TOOL_CONFIG[tool] || { icon: Terminal, @@ -972,34 +1033,32 @@ const ToolUseCard = memo(function ToolUseCard({ tool, input, result, onSendMessa }); }; - // Submit selected answers or custom input - const handleSubmit = () => { - if (!onSendMessage || hasResult) return; - - // If custom input is provided, use that - if (customInput.trim()) { - onSendMessage(customInput.trim()); - return; - } - - // Otherwise use selected options - const answers = questions.map((q, qIdx) => { - const selected = selectedOptions[`q${qIdx}`] || []; - const labels = selected.map(idx => q.options[idx]?.label).filter(Boolean); - return labels.join(', ') || ''; - }).filter(Boolean); - - if (answers.length > 0) { - onSendMessage(answers.join('\n')); - } - }; - const isOptionSelected = (qIdx, optIdx) => { return (selectedOptions[`q${qIdx}`] || []).includes(optIdx); }; const hasSelection = Object.values(selectedOptions).some(arr => arr.length > 0); - const canSubmit = hasSelection || customInput.trim(); + + // Handle submit from CustomInputSection + const handleCustomSubmit = useCallback((customText) => { + if (!onSendMessage || hasResult) return; + + if (customText) { + // Custom text provided + onSendMessage(customText); + } else { + // Use selected options + const answers = questions.map((q, qIdx) => { + const selected = selectedOptions[`q${qIdx}`] || []; + const labels = selected.map(idx => q.options[idx]?.label).filter(Boolean); + return labels.join(', ') || ''; + }).filter(Boolean); + + if (answers.length > 0) { + onSendMessage(answers.join('\n')); + } + } + }, [onSendMessage, hasResult, questions, selectedOptions]); return (
@@ -1066,38 +1125,9 @@ const ToolUseCard = memo(function ToolUseCard({ tool, input, result, onSendMessa
))} - {/* Custom input + Submit section */} + {/* Custom input + Submit section - isolated to prevent re-renders */} {!hasResult && onSendMessage && ( -
- {/* Custom input field */} -
- - setCustomInput(e.target.value)} - placeholder="Enter your own answer..." - minRows={2} - maxRows={6} - className="w-full px-3 py-2 bg-dark-800 border border-dark-700 rounded-lg text-sm text-dark-200 - placeholder-dark-500 resize-none - focus:outline-none focus:border-yellow-500/50 focus:ring-1 focus:ring-yellow-500/20 - transition-all duration-200" - /> -
- - {/* Submit button */} - -
+ )} {hasResult && (