Skip to content

Conversation

@Avni2000
Copy link
Owner

@Avni2000 Avni2000 commented Feb 11, 2026

Summary by CodeRabbit

Release Notes

  • Chores

    • Added virtualization support to improve performance when handling large conflict lists.
  • Bug Fixes

    • Image outputs now display as text placeholders to prevent rendering flicker and optimize performance.
  • Style

    • Adjusted vertical spacing between conflict rows for improved layout consistency.

Copilot AI and others added 15 commits February 10, 2026 18:54
- Remove opacity-based virtualization from CellOutputs
- Replace <img> tags with text placeholders: ![Image: image/png]
- Add CSS styling for image-placeholder class
- Remove unused isVisible prop from CellOutputs component

Co-authored-by: Avni2000 <77120766+Avni2000@users.noreply.github.com>
- Add 'Courier New' as fallback font for better cross-platform support
- Remove unrelated .error-output style to keep changes focused

Co-authored-by: Avni2000 <77120766+Avni2000@users.noreply.github.com>
- Extract ImagePlaceholder component with aria-label for screen readers
- Add role="img" attribute for semantic HTML
- Use helper function for consistent placeholder formatting

Co-authored-by: Avni2000 <77120766+Avni2000@users.noreply.github.com>
- Use <div> instead of <pre> for image placeholder to avoid semantic conflict with role="img"
- Add white-space: pre-wrap and font-size to CSS for proper formatting
- Use double quotes for font-family names containing spaces (CSS best practice)

Co-authored-by: Avni2000 <77120766+Avni2000@users.noreply.github.com>
- Convert MIME types to user-friendly format (e.g., 'PNG image output' instead of 'Image output of type image/png')
- Add fallback for unknown image types

Co-authored-by: Avni2000 <77120766+Avni2000@users.noreply.github.com>
- Remove unreachable fallback case since only PNG and JPEG are supported
- Simplify conditional to binary choice for clarity

Co-authored-by: Avni2000 <77120766+Avni2000@users.noreply.github.com>
- Document that only image/png and image/jpeg are currently supported
- Makes the intentional constraint explicit for future maintainers

Co-authored-by: Avni2000 <77120766+Avni2000@users.noreply.github.com>
The previous fix removed the isVisible/opacity optimization entirely,
causing all cells to render at full opacity. This made everything slow.

Now: text placeholders (fixes flickering) + opacity optimization (fixes performance).
Text placeholders have consistent dimensions, so opacity changes don't trigger ResizeObserver.

Co-authored-by: Avni2000 <77120766+Avni2000@users.noreply.github.com>
**1. `scrollTop` state is tracked but never used in JSX** — every scroll event calls `setScrollTop()`, which forces a full `ConflictResolver` re-render. This is pure waste.

**2. No `React.memo` on `MergeRow`** — every state change in the parent (scroll, typing, ResizeObserver, drag) re-renders ALL `MergeRow` components. With 100+ rows, each keystroke triggers 100+ component renders.

**3. No `React.memo` on `CellContent`** — even within a single MergeRow re-render, all 3 `CellContent` children fully re-render, including expensive diff computations and markdown rendering.

**4. `getRefCallback(i)` creates a new closure on every render** — React sees a different `ref` function, triggers ResizeObserver unobserve/observe on every render for every row.

**5. `DiffContent` runs `computeLineDiff` without `useMemo`** — expensive LCS computation runs on every render even when inputs haven't changed.

**6. Inline drag handlers in MergeRow** — `onDragStart` closures are recreated each render, defeating any memoization on `CellContent`.

The cascade on each keystroke: type → `setChoices` → ConflictResolver renders → ALL MergeRows render → ALL CellContent children render → ALL diffs recompute → ResizeObserver fires → `setRowHeights` → ANOTHER full render cycle.
[FIX] Perf improvements

* **Refactor**
  * Optimized rendering performance for content cells and merge row operations through strategic memoization of expensive computations.
  * Improved callback efficiency by implementing caching strategies for frequently used event handlers.
  * Enhanced scroll performance in conflict resolution interface by streamlining position tracking mechanisms.
@Avni2000 Avni2000 self-assigned this Feb 11, 2026
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Feb 11, 2026

Warning

Rate limit exceeded

@Avni2000 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 19 minutes and 2 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📝 Walkthrough

Walkthrough

Integrates @tanstack/react-virtual library to replace custom manual virtualization logic in ConflictResolver. Removes isVisible prop from CellContent and MergeRow, adds ImagePlaceholder component for image outputs, refactors component memoization patterns, and restructures drag-state props into primitives. Adjusts styling for virtual rows and image placeholders.

Changes

Cohort / File(s) Summary
Dependency Management
package.json
Added @tanstack/react-virtual runtime dependency.
Virtualization Architecture
src/web/client/ConflictResolver.tsx
Replaced custom height-tracking and ResizeObserver-based virtualization with useVirtualizer from @tanstack/react-virtual. Removed manual row height computation, visibility state logic, and position calculation. Refactored rendering to use virtualRows with absolute positioning and transform-based layout. Restructured drag-state props into primitives (cellDragActive, cellDragSide, cellDragSourceRow, isRowDropTarget, rowDropTargetSide) for stable memoization.
Cell Rendering & Memoization
src/web/client/CellContent.tsx
Renamed internal component to CellContentInner and exported as memoized wrapper via React.memo. Removed isVisible prop and lazy-render path. Added ImagePlaceholder component for image/png and image/jpeg outputs instead of direct image rendering. Introduced useMemo for cell data encoding and diff computation.
Merge Row Props & Drag Handling
src/web/client/MergeRow.tsx
Renamed implementation to MergeRowInner and re-exported as memoized React.memo wrapper. Replaced DraggedCellData and DropTarget interfaces with primitive drag-state fields in props. Removed isVisible prop. Implemented stable drag-start handlers for base/current/incoming cells using useCallback. Added blur handler for content commit. Updated drop-target logic to use new primitive drag-state fields.
Styling Updates
src/web/client/styles.ts
Removed bottom margins from .merge-row variants (conflict-row, unmatched-row). Added .virtual-row utility with padding-bottom. Introduced .image-placeholder styling for consistent text-based placeholder rendering.

Sequence Diagram

sequenceDiagram
    participant ConflictResolver
    participant useVirtualizer as useVirtualizer<br/>(`@tanstack/react-virtual`)
    participant MergeRow
    participant CellContent
    participant ImagePlaceholder

    ConflictResolver->>useVirtualizer: Initialize with rowSize & overscan
    useVirtualizer-->>ConflictResolver: Return virtualRows (visible only)
    
    ConflictResolver->>MergeRow: Render each virtualRow with<br/>drag-state primitives
    Note over MergeRow: cellDragActive, cellDragSide,<br/>cellDragSourceRow, etc.
    
    MergeRow->>CellContent: Render base/current/incoming<br/>cell content
    CellContent->>CellContent: Check output type
    
    alt Image Output
        CellContent->>ImagePlaceholder: Render text placeholder
        ImagePlaceholder-->>CellContent: Return placeholder HTML
    else Non-Image Output
        CellContent->>CellContent: Render markdown/diff/text
    end
    
    CellContent-->>MergeRow: Return rendered cell
    MergeRow-->>ConflictResolver: Return positioned row element
    
    ConflictResolver->>ConflictResolver: Apply absolute positioning<br/>& translateY transform
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 A virtualized hop through rows so tall,
TanStack takes the load, no custom calls,
Placeholders bloom where images once flew,
Memoized wrappers make rendering smooth and true!
The merge dance quickens—fewer trees to paint.

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 12.50% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main objective of the changeset: implementing virtualization of cell rows using TanStack's react-virtual library and refactoring to React callbacks for better performance.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch improve-perf

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/web/client/CellContent.tsx (1)

38-48: ⚠️ Potential issue | 🔴 Critical

Critical: useMemo called after early return violates Rules of Hooks.

The early return on line 38 means useMemo on line 48 is conditionally skipped when cell is undefined. React requires all hooks to be called in the same order on every render. This is also flagged by the Biome linter.

Move the hook above the early return (with a fallback for undefined cell):

Proposed fix
 export function CellContentInner({
     cell,
     cellIndex,
     side,
     isConflict = false,
     compareCell,
     baseCell,
     diffMode = 'base',
     showOutputs = true,
     onDragStart,
     onDragEnd,
 }: CellContentProps): React.ReactElement {
+    const encodedCell = useMemo(() => cell ? encodeURIComponent(JSON.stringify(cell)) : '', [cell]);
+
     if (!cell) {
         return (
             <div className="cell-placeholder">
                 <span className="placeholder-text">(not present)</span>
             </div>
         );
     }
 
     const source = normalizeCellSource(cell.source);
     const cellType = cell.cell_type;
-    const encodedCell = useMemo(() => encodeURIComponent(JSON.stringify(cell)), [cell]);
src/web/client/ConflictResolver.tsx (1)

265-287: ⚠️ Potential issue | 🟠 Major

Fix cell-columns grid to respect showBaseColumn prop.

The .cell-columns grid always renders 3 columns (base/current/incoming) but the column-labels header switches to a 2-column layout when showBaseColumn is false. This creates a visual misalignment where 2 header labels span over 3 content columns.

Add the two-column CSS class conditionally to the cell-columns div:

<div className={`cell-columns${showBaseColumn ? '' : ' two-column'}`}>

The CSS rule .cell-columns.two-column { grid-template-columns: repeat(2, minmax(0, 1fr)); } already exists in styles.ts but is not being applied.

🤖 Fix all issues with AI agents
In `@src/web/client/CellContent.tsx`:
- Around line 108-109: The DiffContent component is missing the diffMode prop in
its parameter destructuring which causes the build error; update the DiffContent
signature to destructure diffMode from DiffContentProps (e.g., function
DiffContent({ source, compareSource, side, diffMode }: DiffContentProps)) and
also add diffMode to the useMemo dependency array (useMemo(() =>
computeLineDiff(compareSource, source, diffMode), [compareSource, source,
diffMode])) so the computeLineDiff call and memoization correctly receive and
react to diffMode changes.
🧹 Nitpick comments (1)
src/web/client/ConflictResolver.tsx (1)

84-86: buildMergeRowsFromSemantic runs on every render but its result is only used as initial state.

initialRows is recomputed on each render even though useState (line 91) and the history initializer (line 92) only consume it on mount. Wrap it in useMemo or move it into the lazy state initializer to avoid unnecessary work during re-renders.

Option: lazy useState initializer
-    const initialRows = conflict.type === 'semantic' && conflict.semanticConflict
-        ? buildMergeRowsFromSemantic(conflict.semanticConflict)
-        : [];
-
     const [choices, setChoices] = useState<Map<number, ResolutionState>>(new Map());
     const [markAsResolved, setMarkAsResolved] = useState(INITIAL_MARK_AS_RESOLVED);
     const [renumberExecutionCounts, setRenumberExecutionCounts] = useState(INITIAL_RENUMBER_EXECUTION_COUNTS);
-    const [rows, setRows] = useState<MergeRowType[]>(initialRows);
-    const [history, setHistory] = useState<HistoryState>(() => ({
+    const [rows, setRows] = useState<MergeRowType[]>(() =>
+        conflict.type === 'semantic' && conflict.semanticConflict
+            ? buildMergeRowsFromSemantic(conflict.semanticConflict)
+            : []
+    );
+    const [history, setHistory] = useState<HistoryState>(() => {
+        const initRows = conflict.type === 'semantic' && conflict.semanticConflict
+            ? buildMergeRowsFromSemantic(conflict.semanticConflict)
+            : [];
+        return {
         entries: [{
             label: 'Initial state',
             snapshot: {
                 choices: cloneChoices(new Map()),
-                rows: cloneRows(initialRows),
+                rows: cloneRows(initRows),
                 markAsResolved: INITIAL_MARK_AS_RESOLVED,
                 renumberExecutionCounts: INITIAL_RENUMBER_EXECUTION_COUNTS,
             },
         }],
         index: 0,
-    }));
+        };
+    });

Note: this calls buildMergeRowsFromSemantic twice on mount. If you want to avoid that, compute it once in a ref or shared initializer.

Comment on lines 108 to 109
function DiffContent({ source, compareSource, side }: DiffContentProps): React.ReactElement {
const diff = useMemo(() => computeLineDiff(compareSource, source), [compareSource, source]);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: diffMode not destructured — causes build failure.

diffMode is declared in DiffContentProps but omitted from the destructuring on line 108. This causes the pipeline error Cannot find name 'diffMode' at line 121 where it's referenced.

Proposed fix
-function DiffContent({ source, compareSource, side }: DiffContentProps): React.ReactElement {
+function DiffContent({ source, compareSource, side, diffMode }: DiffContentProps): React.ReactElement {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function DiffContent({ source, compareSource, side }: DiffContentProps): React.ReactElement {
const diff = useMemo(() => computeLineDiff(compareSource, source), [compareSource, source]);
function DiffContent({ source, compareSource, side, diffMode }: DiffContentProps): React.ReactElement {
const diff = useMemo(() => computeLineDiff(compareSource, source), [compareSource, source]);
🤖 Prompt for AI Agents
In `@src/web/client/CellContent.tsx` around lines 108 - 109, The DiffContent
component is missing the diffMode prop in its parameter destructuring which
causes the build error; update the DiffContent signature to destructure diffMode
from DiffContentProps (e.g., function DiffContent({ source, compareSource, side,
diffMode }: DiffContentProps)) and also add diffMode to the useMemo dependency
array (useMemo(() => computeLineDiff(compareSource, source, diffMode),
[compareSource, source, diffMode])) so the computeLineDiff call and memoization
correctly receive and react to diffMode changes.

@Avni2000 Avni2000 merged commit 42b18e4 into main Feb 11, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants