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:
@@ -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,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 (
|
||||
<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 && (
|
||||
|
||||
Reference in New Issue
Block a user