diff --git a/src/browser/components/RightSidebar/CodeReview/ImmersiveReviewView.tsx b/src/browser/components/RightSidebar/CodeReview/ImmersiveReviewView.tsx index a3ae7fd026..3ab956201d 100644 --- a/src/browser/components/RightSidebar/CodeReview/ImmersiveReviewView.tsx +++ b/src/browser/components/RightSidebar/CodeReview/ImmersiveReviewView.tsx @@ -1111,12 +1111,20 @@ export const ImmersiveReviewView: React.FC = (props) = const anchorIndex = shiftKey ? (selectedLineRangeRef.current?.startIndex ?? activeLineIndexRef.current ?? lineIndex) : lineIndex; - setActiveLineIndex(lineIndex); + setActiveLineIndex((previousLineIndex) => + previousLineIndex === lineIndex ? previousLineIndex : lineIndex + ); if (shiftKey) { - setSelectedLineRange({ startIndex: anchorIndex, endIndex: lineIndex }); + setSelectedLineRange((previousRange) => { + if (previousRange?.startIndex === anchorIndex && previousRange?.endIndex === lineIndex) { + return previousRange; + } + + return { startIndex: anchorIndex, endIndex: lineIndex }; + }); } else { - setSelectedLineRange(null); + setSelectedLineRange((previousRange) => (previousRange === null ? previousRange : null)); } if (isTouchExperience && !shiftKey && resolvedHunk) { diff --git a/src/browser/components/shared/DiffRenderer.tsx b/src/browser/components/shared/DiffRenderer.tsx index 9ff73a83d7..80fd2c7ad8 100644 --- a/src/browser/components/shared/DiffRenderer.tsx +++ b/src/browser/components/shared/DiffRenderer.tsx @@ -122,6 +122,14 @@ const getIndicatorChar = (type: DiffLineType): string => { const REVIEW_RANGE_TINT = "hsl(from var(--color-review-accent) h s l / 0.08)"; +const REVIEW_NOTE_MIN_ROWS = 2; +const REVIEW_NOTE_MAX_ROWS = 8; + +const getTextareaRowsFromNoteText = (text: string): number => { + const explicitLineCount = text.split("\n").length; + return Math.min(REVIEW_NOTE_MAX_ROWS, Math.max(REVIEW_NOTE_MIN_ROWS, explicitLineCount)); +}; + const applyReviewRangeOverlay = (base: string, isActive: boolean): string => { if (!isActive) return base; return `linear-gradient(${REVIEW_RANGE_TINT}, ${REVIEW_RANGE_TINT}), ${base}`; @@ -706,29 +714,43 @@ const ReviewNoteInput: React.FC = React.memo( initialNoteText, }) => { const { showOld, showNew } = getLineNumberModeFlags(lineNumberMode); - const [noteText, setNoteText] = React.useState(initialNoteText ?? ""); const textareaRef = React.useRef(null); - // Auto-focus on mount - React.useEffect(() => { - textareaRef.current?.focus(); + // Avoid scrollHeight reads during typing: in large immersive diffs those force full-grid + // layout on each keypress. Row count based on explicit newlines keeps input latency stable. + const syncTextareaRows = React.useCallback((textarea: HTMLTextAreaElement) => { + const nextRows = getTextareaRowsFromNoteText(textarea.value); + if (textarea.rows === nextRows) { + return; + } + textarea.rows = nextRows; }, []); + // Keep the composer uncontrolled so typing does not trigger per-key React re-renders + // through immersive diff overlays. Parent-initiated prefill changes are synced here. React.useEffect(() => { - setNoteText(initialNoteText ?? ""); - }, [initialNoteText]); + const textarea = textareaRef.current; + if (!textarea) { + return; + } + + textarea.value = initialNoteText ?? ""; + syncTextareaRows(textarea); + }, [initialNoteText, syncTextareaRows]); - // Auto-expand textarea as user types + // Auto-focus on mount. React.useEffect(() => { const textarea = textareaRef.current; - if (!textarea) return; + if (!textarea) { + return; + } - textarea.style.height = "auto"; - textarea.style.height = `${textarea.scrollHeight}px`; - }, [noteText]); + textarea.focus(); + syncTextareaRows(textarea); + }, [syncTextareaRows]); const handleSubmit = () => { - const text = textareaRef.current?.value ?? noteText; + const text = textareaRef.current?.value ?? ""; if (!text.trim()) return; const [start, end] = [selection.startIndex, selection.endIndex].sort((a, b) => a - b); @@ -844,13 +866,13 @@ const ReviewNoteInput: React.FC = React.memo( />