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 && (