fix: resolve input lag in AskUserQuestion custom input field

- Extract CustomInputSection as isolated memoized component
- Use uncontrolled input (defaultValue + ref) instead of controlled (value + onChange)
- Add cacheMeasurements for TextareaAutosize performance
- Only track hasText boolean for button state, not actual content
- Matches ChatInput pattern for consistent lag-free typing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-18 07:02:10 +01:00
parent eb45891d6f
commit ea7ea9c5f8

View File

@@ -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 (
<div className="space-y-3 pt-3 border-t border-dark-700">
{/* Custom input field - uncontrolled for performance */}
<div className="space-y-2">
<label className="text-xs text-dark-500 font-medium">Or type a custom response:</label>
<TextareaAutosize
ref={textareaRef}
defaultValue=""
onInput={handleInput}
placeholder="Enter your own answer..."
minRows={2}
maxRows={6}
cacheMeasurements
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-colors duration-200"
/>
</div>
{/* Submit button */}
<button
onClick={handleSubmit}
disabled={!canSubmit}
className={`w-full py-2.5 rounded-lg text-sm font-medium transition-all duration-200 ${
canSubmit
? 'bg-yellow-500 text-dark-900 hover:bg-yellow-400 shadow-lg shadow-yellow-500/20 hover:shadow-yellow-500/30'
: 'bg-dark-700 text-dark-500 cursor-not-allowed'
}`}
>
Submit Answer
</button>
</div>
);
});
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,17 +1033,21 @@ const ToolUseCard = memo(function ToolUseCard({ tool, input, result, onSendMessa
});
};
// Submit selected answers or custom input
const handleSubmit = () => {
const isOptionSelected = (qIdx, optIdx) => {
return (selectedOptions[`q${qIdx}`] || []).includes(optIdx);
};
const hasSelection = Object.values(selectedOptions).some(arr => arr.length > 0);
// Handle submit from CustomInputSection
const handleCustomSubmit = useCallback((customText) => {
if (!onSendMessage || hasResult) return;
// If custom input is provided, use that
if (customInput.trim()) {
onSendMessage(customInput.trim());
return;
}
// Otherwise use selected options
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);
@@ -992,14 +1057,8 @@ const ToolUseCard = memo(function ToolUseCard({ tool, input, result, onSendMessa
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();
}
}, [onSendMessage, hasResult, questions, selectedOptions]);
return (
<div className="space-y-5">
@@ -1066,38 +1125,9 @@ const ToolUseCard = memo(function ToolUseCard({ tool, input, result, onSendMessa
</div>
))}
{/* Custom input + Submit section */}
{/* Custom input + Submit section - isolated to prevent re-renders */}
{!hasResult && onSendMessage && (
<div className="space-y-3 pt-3 border-t border-dark-700">
{/* Custom input field */}
<div className="space-y-2">
<label className="text-xs text-dark-500 font-medium">Or type a custom response:</label>
<TextareaAutosize
value={customInput}
onChange={(e) => 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"
/>
</div>
{/* Submit button */}
<button
onClick={handleSubmit}
disabled={!canSubmit}
className={`w-full py-2.5 rounded-lg text-sm font-medium transition-all duration-200 ${
canSubmit
? 'bg-yellow-500 text-dark-900 hover:bg-yellow-400 shadow-lg shadow-yellow-500/20 hover:shadow-yellow-500/30'
: 'bg-dark-700 text-dark-500 cursor-not-allowed'
}`}
>
Submit Answer
</button>
</div>
<CustomInputSection onSubmit={handleCustomSubmit} hasSelection={hasSelection} />
)}
{hasResult && (