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
|
// 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 ToolUseCard = memo(function ToolUseCard({ tool, input, result, onSendMessage }) {
|
||||||
const [showResult, setShowResult] = useState(false);
|
const [showResult, setShowResult] = useState(false);
|
||||||
const [selectedOptions, setSelectedOptions] = useState({});
|
const [selectedOptions, setSelectedOptions] = useState({});
|
||||||
const [customInput, setCustomInput] = useState('');
|
|
||||||
|
|
||||||
const config = TOOL_CONFIG[tool] || {
|
const config = TOOL_CONFIG[tool] || {
|
||||||
icon: Terminal,
|
icon: Terminal,
|
||||||
@@ -972,17 +1033,21 @@ const ToolUseCard = memo(function ToolUseCard({ tool, input, result, onSendMessa
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Submit selected answers or custom input
|
const isOptionSelected = (qIdx, optIdx) => {
|
||||||
const handleSubmit = () => {
|
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 (!onSendMessage || hasResult) return;
|
||||||
|
|
||||||
// If custom input is provided, use that
|
if (customText) {
|
||||||
if (customInput.trim()) {
|
// Custom text provided
|
||||||
onSendMessage(customInput.trim());
|
onSendMessage(customText);
|
||||||
return;
|
} else {
|
||||||
}
|
// Use selected options
|
||||||
|
|
||||||
// Otherwise use selected options
|
|
||||||
const answers = questions.map((q, qIdx) => {
|
const answers = questions.map((q, qIdx) => {
|
||||||
const selected = selectedOptions[`q${qIdx}`] || [];
|
const selected = selectedOptions[`q${qIdx}`] || [];
|
||||||
const labels = selected.map(idx => q.options[idx]?.label).filter(Boolean);
|
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) {
|
if (answers.length > 0) {
|
||||||
onSendMessage(answers.join('\n'));
|
onSendMessage(answers.join('\n'));
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
}, [onSendMessage, hasResult, questions, selectedOptions]);
|
||||||
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();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
@@ -1066,38 +1125,9 @@ const ToolUseCard = memo(function ToolUseCard({ tool, input, result, onSendMessa
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{/* Custom input + Submit section */}
|
{/* Custom input + Submit section - isolated to prevent re-renders */}
|
||||||
{!hasResult && onSendMessage && (
|
{!hasResult && onSendMessage && (
|
||||||
<div className="space-y-3 pt-3 border-t border-dark-700">
|
<CustomInputSection onSubmit={handleCustomSubmit} hasSelection={hasSelection} />
|
||||||
{/* 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>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{hasResult && (
|
{hasResult && (
|
||||||
|
|||||||
Reference in New Issue
Block a user