Skip to content

feat(openapi-sync): virtualize spec diff rendering + spec change block navigation#7848

Open
abhishek-bruno wants to merge 1 commit intousebruno:mainfrom
abhishek-bruno:feat/spec-diff-virtualization
Open

feat(openapi-sync): virtualize spec diff rendering + spec change block navigation#7848
abhishek-bruno wants to merge 1 commit intousebruno:mainfrom
abhishek-bruno:feat/spec-diff-virtualization

Conversation

@abhishek-bruno
Copy link
Copy Markdown
Member

@abhishek-bruno abhishek-bruno commented Apr 24, 2026

Description

JIRA

Screen.Recording.2026-04-24.at.1.33.25.PM.mov

Summary

  • Replace Diff2Html.html() + innerHTML with a virtualized row renderer (react-virtuoso), so only visible rows are in the DOM. Unblocks the main thread on large specs — 50k-line diffs no longer freeze the app.
  • Add Next/Previous navigation across change blocks, with a counter ("3 of 12 changes") and a focused-row indicator bar.
  • Extract parsing into buildRows.js (pure, testable), word-level highlighting into highlightCache.js, and the row component into DiffRow.js.
  • Add a button-level spinner on "View Spec Diff" so there's immediate feedback while the modal mounts.

Why

On large specs (tested with 50k-line diffs), the previous flow hung the renderer's main thread for seconds — the modal appeared unresponsive, the close button didn't react, and the app felt frozen. The new virtualized renderer keeps DOM size bounded regardless of diff size and defers parsing via setTimeout(0) so the spinner paints first.

Notes

  • New buildRows.js is covered by 8 unit tests including a regression test pinning the change-block count against fixture shapes.
  • SpecDiffModal and DiffRow both compile cleanly under React Compiler — all manual useMemo/useCallback dropped except where they earn their keep.
  • The styled-components diff is large because the entire .d2h-* class stack is replaced with a custom .diff-row / .diff-cell-* grid layout.

Test plan

  • Open a spec with a small diff (<200 lines) — modal renders, Next/Prev navigation cycles through changes, row highlighting works.
  • Open a large spec (10k+ lines) — spinner shows on the button, then in the modal, then the diff renders. Main thread stays responsive during parse.
  • Rapid open/close — no stuck spinners, no console errors.
  • Theme switch while modal is open — colors update without re-parsing.
  • Paired del+ins rows show word-level red/green highlights inline.
  • Hunk headers (@@ ... @@) render as full-width rows and don't count as change blocks.

Contribution Checklist:

  • I've used AI significantly to create this pull request
  • The pull request only addresses one issue or adds one feature.
  • The pull request does not introduce any breaking changes
  • I have added screenshots or gifs to help explain the change if applicable.
  • I have read the contribution guidelines.
  • Create an issue and link to the pull request.

Note: Keeping the PR small and focused helps make it easier to review and merge. If you have multiple changes you want to make, please consider submitting them as separate pull requests.

Publishing to New Package Managers

Please see here for more information.

Summary by CodeRabbit

Release Notes

  • New Features

    • Virtualized diff rendering for improved performance with large OpenAPI specs.
    • Word-level change highlighting for more granular difference visibility.
    • Previous/next navigation for change blocks in spec diff view.
  • Improvements

    • Enhanced visual styling and layout for the diff modal.
    • Improved loading state feedback when opening spec diffs.

- Added a new SpecDiffModal component to display differences between current and updated specs.
- Introduced buildRows function to flatten parsed diff data for rendering.
- Created DiffRow component for rendering individual rows in the diff view.
- Implemented highlightCache for efficient word-level diff highlighting.
- Enhanced user experience with navigation controls for changes and loading indicators.
- Added tests for buildRows functionality to ensure accurate diff representation.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 24, 2026

Walkthrough

This PR refactors the OpenAPI spec diff modal from rendering Diff2Html-generated HTML to a custom virtualized row-based renderer. It adds utilities for parsing diffs into flat row arrays, caching word-level highlights, and rendering individual rows, along with change block navigation and updated styling.

Changes

Cohort / File(s) Summary
Virtualized Diff Rendering
packages/bruno-app/src/components/OpenAPISyncTab/SpecDiffModal/DiffRow.js
New memoized component for rendering side-by-side diff rows with word-level highlighting via dangerouslySetInnerHTML; handles special 'hunk' layout and applies focused/kind-based styling.
Diff Processing & Caching
packages/bruno-app/src/components/OpenAPISyncTab/SpecDiffModal/buildRows.js, packages/bruno-app/src/components/OpenAPISyncTab/SpecDiffModal/highlightCache.js
buildRows transforms Diff2Html parsed output into flat row arrays and change block metadata for navigation; highlightCache memoizes word-level diff fragments via Diff2Html.diffHighlight with fallback escaping.
Modal Refactoring
packages/bruno-app/src/components/OpenAPISyncTab/SpecDiffModal/index.js
Switches from DOM-injected HTML to virtualized Virtuoso list; adds diff parsing with error handling, change block navigation (prev/next buttons with wrapping), and updated header layout with navigation counter.
Styling Overhaul
packages/bruno-app/src/components/OpenAPISyncTab/StyledWrapper.js
Removes all .d2h-* Diff2Html rules; adds grid-based column headers, flex layouts, diff row/cell styling with kind-based colors, focused-row visual state (left yellow bar), hunk height constraints, and modal header/navigation styles.
Loading State
packages/bruno-app/src/components/OpenAPISyncTab/SyncReviewPage/index.js
Defers modal open with setTimeout(…, 0) to allow "View Spec Diff" button spinner to render; adds isOpeningSpecDiff flag and button state management.
Test Suite
packages/bruno-app/src/components/OpenAPISyncTab/SpecDiffModal/__tests__/buildRows.spec.js
Comprehensive Jest tests for buildRows (edge cases, row pairing, change block segmentation, multi-hunk handling) and wrapIndex (modulo wrapping behavior).

Sequence Diagram

sequenceDiagram
    participant User
    participant SpecDiffModal
    participant Diff2Html
    participant buildRows
    participant highlightCache
    participant Virtuoso
    participant DiffRow

    User->>SpecDiffModal: Open modal with unified diff
    SpecDiffModal->>Diff2Html: parse(unifiedDiff)
    Diff2Html-->>SpecDiffModal: parsed hunks array
    SpecDiffModal->>buildRows: buildRows(parsed)
    buildRows-->>SpecDiffModal: { rows, changeBlocks }
    SpecDiffModal->>highlightCache: createHighlightCache()
    highlightCache-->>SpecDiffModal: cache instance
    SpecDiffModal->>Virtuoso: render with rows array
    Virtuoso->>DiffRow: render each row item
    DiffRow->>highlightCache: getWordDiff(left, right)
    highlightCache-->>DiffRow: HTML diff fragments
    DiffRow-->>Virtuoso: rendered row markup
    User->>SpecDiffModal: Click prev/next button
    SpecDiffModal->>buildRows: wrapIndex(currentIdx, length)
    buildRows-->>SpecDiffModal: wrapped index
    SpecDiffModal->>Virtuoso: scrollToIndex(activeBlock)
    Virtuoso-->>User: scroll to active change
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly Related PRs

  • #7467: Modifies OpenAPISyncTab spec-diff UI components (SpecDiffModal/index.js and StyledWrapper.js) with overlapping changes.
  • #7483: Tweaks badge rendering in SpecDiffModal/index.js, directly affected by this PR's modal refactor.
  • #7279: Introduces the initial Diff2Html-based SpecDiffModal logic that this PR replaces with custom virtualized rendering.

Suggested Reviewers

  • helloanoop
  • lohit-bruno
  • naman-bruno
  • bijin-bruno

Poem

🎢 Hunks now virtualize in rows,
Word-diffs dance where focus glows,
Navigation blocks take flight,
Left meets right in custom light! ✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main changes: virtualization of spec diff rendering and addition of change block navigation, matching the core objectives.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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
Copy Markdown
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: 5

🧹 Nitpick comments (6)
packages/bruno-app/src/components/OpenAPISyncTab/SpecDiffModal/__tests__/buildRows.spec.js (2)

73-87: Nit: test-numbering scheme goes 1, 2, 3, 4, (unnumbered), 5, 6, 7.

The "matched del+ins pair" test on Line 73 breaks the 1..6 sequence. Either number it (e.g. 4b. or renumber 5→5 and push) or drop numbers entirely — the descriptive name is already enough. No logic impact.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/bruno-app/src/components/OpenAPISyncTab/SpecDiffModal/__tests__/buildRows.spec.js`
around lines 73 - 87, Update the test title for the "matched del+ins pair →
paired row with leftKind=del, rightKind=ins" case to remove or adjust the stray
numbering so the test sequence is consistent; locate the test that calls
buildRows(parsed) (the test block asserting
rows[1].leftKind/rightKind/leftText/rightText etc.) and edit its description
string to either add a matching numeric label (e.g., "4b.") or remove numbers
entirely so it follows the surrounding tests' naming scheme.

19-147: Consider covering the "unknown line type" safety path.

buildRows has an explicit safety step for unrecognized line.type values (Pass 1, the i++ fallback). It's defensive today but not tested — a test feeding { type: 'unknown', content: '...' } would pin down the contract and guard against a future refactor silently dropping the i++ and infinite-looping.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/bruno-app/src/components/OpenAPISyncTab/SpecDiffModal/__tests__/buildRows.spec.js`
around lines 19 - 147, The test suite misses a case asserting buildRows handles
unexpected line.type values and advances the loop (the Pass 1 i++ fallback); add
a unit test that calls buildRows with a parsed hunk containing a line object
like { type: 'unknown', content: '...' } and assert it returns without hanging
and produces rows/changeBlocks (e.g., no infinite loop and same empty-output
behavior or a predictable placeholder), thereby exercising the safety path in
the buildRows implementation and ensuring the i++ fallback remains covered.
packages/bruno-app/src/components/OpenAPISyncTab/SpecDiffModal/buildRows.js (1)

139-145: Nit: row.leftKind === 'hunk' is already covered by !isChanged.

Hunk rows have leftKind: 'hunk', rightKind: 'hunk' — neither is del/ins, so isChanged is false and !isChanged triggers. The explicit hunk check is a no-op. Leaving it for readability is fine, but if you want it leaner:

♻️ Optional simplification
   rows.forEach((row, idx) => {
     const isChanged = row.leftKind === 'del' || row.rightKind === 'ins';

-    if (row.leftKind === 'hunk' || !isChanged) {
+    if (!isChanged) {
       currentBlock = null;
       return;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/bruno-app/src/components/OpenAPISyncTab/SpecDiffModal/buildRows.js`
around lines 139 - 145, The condition "row.leftKind === 'hunk' || !isChanged" is
redundant because hunk rows yield !isChanged already; in the rows.forEach loop
simplify the branch by removing the explicit hunk check and only test !isChanged
(i.e., replace the if (row.leftKind === 'hunk' || !isChanged) { currentBlock =
null; return; } with a single !isChanged check) so currentBlock is cleared for
unchanged/hunk rows while keeping changed-row logic intact; refer to the
rows.forEach loop and the isChanged, row.leftKind/row.rightKind, and
currentBlock identifiers when making the change.
packages/bruno-app/src/components/OpenAPISyncTab/SpecDiffModal/index.js (3)

109-128: Consider adding data-testid to the nav buttons for Playwright.

Guidelines call for data-testid on testable elements. Next/Previous change navigation is exactly the kind of behavior a regression test would want to click on — title attributes aren't stable locators.

♻️ Suggested tweak
                 <button
                   type="button"
                   className="spec-diff-nav-btn"
                   onClick={() => goToChange(currentIndex - 1)}
                   disabled={changeCount === 0}
                   title="Previous change"
+                  data-testid="spec-diff-nav-prev"
                 >
                   <IconChevronUp size={14} strokeWidth={1.75} /> Previous
                 </button>
                 <button
                   type="button"
                   className="spec-diff-nav-btn"
                   onClick={() => goToChange(currentIndex + 1)}
                   disabled={changeCount === 0}
                   title="Next change"
+                  data-testid="spec-diff-nav-next"
                 >
                   <IconChevronDown size={14} strokeWidth={1.75} /> Next
                 </button>

As per coding guidelines: "Add data-testid to testable elements for Playwright."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/bruno-app/src/components/OpenAPISyncTab/SpecDiffModal/index.js`
around lines 109 - 128, Add stable Playwright testids to the navigation buttons:
update the two buttons that call goToChange(currentIndex - 1) and
goToChange(currentIndex + 1) (they have className "spec-diff-nav-btn" and use
currentIndex/changeCount) to include data-testid attributes (e.g.
data-testid="spec-diff-prev-btn" and data-testid="spec-diff-next-btn") so tests
can reliably select the Previous and Next change controls.

59-71: Small UX thought: auto-scroll to the first change when the diff loads.

setCurrentIndex(0) is set on parse (Line 47) but we never call scrollToIndex for it, so the user always lands at the top of the file and has to hit Next to jump to the first change. For small diffs this is fine; for large specs where the interesting changes are thousands of lines deep, it's a friction point.

Optional — not a blocker — but easy win:

♻️ Sketch
         setRows(built.rows);
         setChangeBlocks(built.changeBlocks);
         setCurrentIndex(0);
         cache.clear();
+        // Jump to the first change once Virtuoso is mounted.
+        if (built.changeBlocks.length > 0) {
+          // defer until after the Virtuoso has rendered
+          requestAnimationFrame(() => {
+            virtuosoRef.current?.scrollToIndex({
+              index: built.changeBlocks[0].startIdx,
+              align: 'center',
+              behavior: 'auto'
+            });
+          });
+        }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/bruno-app/src/components/OpenAPISyncTab/SpecDiffModal/index.js`
around lines 59 - 71, The current behavior sets setCurrentIndex(0) when parsing
but never scrolls the virtuoso to that item; update the logic so the first
change is auto-scrolled when diffs load by invoking the same scroll behavior
used in goToChange for index 0 (or calling goToChange(0)) right after
changeBlocks are populated (e.g., in the parse completion handler or a useEffect
that watches changeBlocks length), ensuring you use
virtuosoRef.current?.scrollToIndex with the same align/behavior logic and then
setCurrentIndex(0) so the user lands on the first change instead of the top of
the file.

29-57: Missing cache in the effect dependency array.

The effect references cache (Line 48, cache.clear()) but the dependency array on Line 57 only lists [specDrift?.unifiedDiff]. cache is stable (from useState(createHighlightCache)) so this is safe at runtime, but it'll trip react-hooks/exhaustive-deps and relies on readers knowing that invariant.

♻️ Proposed fix
     return () => clearTimeout(timer);
-  }, [specDrift?.unifiedDiff]);
+  }, [specDrift?.unifiedDiff, cache]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/bruno-app/src/components/OpenAPISyncTab/SpecDiffModal/index.js`
around lines 29 - 57, The effect uses the stable `cache` (created by
`useState(createHighlightCache)`) but does not list it in the dependency array,
which trips the react-hooks linter; update the dependency array for the
useEffect that parses `specDrift.unifiedDiff` to include `cache` (i.e., deps
should be `[specDrift?.unifiedDiff, cache]`), or if you intentionally rely on
its stability, explicitly document that by memoizing/creating `cache` with
`useRef`/`useMemo` or add an inline eslint-disable comment; adjust around the
block referencing `Diff2Html.parse`, `buildRows`, `setRows`, `setChangeBlocks`,
`setCurrentIndex`, `setIsRendering`, and `setParseError` accordingly so the
linter no longer flags the missing dependency.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/bruno-app/src/components/OpenAPISyncTab/SpecDiffModal/buildRows.js`:
- Around line 94-120: The pairing logic in buildRows.js assumes Diff2Html emits
deletions before insertions, which can cause unpaired solo rows for inputs like
[insert, delete]; update the code around the deletions/insertions collection
(the blocks that fill deletions and insertions and the subsequent pairing loop
that calls pairedChangeRow, soloDeletionRow, soloInsertionRow) to either assert
or log a dev-only warning when an insertion is encountered before a preceding
deletion (i.e., unexpected ordering), and add a one-line comment documenting the
required invariant (Diff2Html emits del before ins) so future readers know the
assumption; use the existing variable names deletions, insertions,
pairedChangeRow, soloDeletionRow, and soloInsertionRow to locate the exact place
to add the check/comment.

In
`@packages/bruno-app/src/components/OpenAPISyncTab/SpecDiffModal/highlightCache.js`:
- Around line 11-13: The JSDoc above getWordDiff incorrectly says
"Character-level diff" while the function uses diffHighlight with matching:
'words'; update the comment to "Word-level diff" (or similar) to match the
implementation and other docs; locate the comment directly above the getWordDiff
function and change the wording so it consistently describes word-level diffing
and mentions diffHighlight/matching: 'words' if helpful.

In `@packages/bruno-app/src/components/OpenAPISyncTab/SpecDiffModal/index.js`:
- Around line 74-80: renderItem inside SpecDiffModal is recreated on every
render causing Virtuoso to receive a new function reference; wrap renderItem
with React.useCallback so its identity is stable and only changes when its
dependencies change (rows, activeBlock, cache) — update the renderItem
declaration to use useCallback((index) => <DiffRow row={rows[index]}
active={!!activeBlock && index >= activeBlock.startIdx && index <=
activeBlock.endIdx} cache={cache} />, [rows, activeBlock, cache]).
- Around line 90-105: The wrapper element with class spec-diff-header-left is
unused in StyledWrapper.js; either add a style block for .spec-diff-header-left
inside StyledWrapper (e.g., to control layout/spacing/stacking of
.spec-diff-badges and .spec-diff-subtitle) or remove the wrapper in
SpecDiffModal to simplify markup; update StyledWrapper.js to include a
.spec-diff-header-left rule (targeting spacing, flex/column behavior, and
gap/margin for its children like .spec-diff-badges and .spec-diff-subtitle) if
you want explicit left-side layout control.

In `@packages/bruno-app/src/components/OpenAPISyncTab/StyledWrapper.js`:
- Around line 1532-1536: The current selector "div:last-child" in StyledWrapper
(targeting children of .text-diff-container) is too broad and unintentionally
matches .text-diff-loading and .text-diff-empty; replace the fallback selector
with the specific testid selector (only keep
div[data-testid='virtuoso-scroller']) so only the Virtuoso scroller receives
flex: 1 1 auto; min-height: 0, and remove the broader div:last-child rule. Also
make the theme color usage consistent for the yellow shadow: either remove the
"|| '#f59e0b'" fallback where theme.colors.text.yellow is used (so all usages
rely on the required theme field) or add the same fallback everywhere—update the
box-shadow occurrences that reference theme.colors.text.yellow to match
(.text-diff-loading/.text-diff-empty context is relevant).

---

Nitpick comments:
In
`@packages/bruno-app/src/components/OpenAPISyncTab/SpecDiffModal/__tests__/buildRows.spec.js`:
- Around line 73-87: Update the test title for the "matched del+ins pair →
paired row with leftKind=del, rightKind=ins" case to remove or adjust the stray
numbering so the test sequence is consistent; locate the test that calls
buildRows(parsed) (the test block asserting
rows[1].leftKind/rightKind/leftText/rightText etc.) and edit its description
string to either add a matching numeric label (e.g., "4b.") or remove numbers
entirely so it follows the surrounding tests' naming scheme.
- Around line 19-147: The test suite misses a case asserting buildRows handles
unexpected line.type values and advances the loop (the Pass 1 i++ fallback); add
a unit test that calls buildRows with a parsed hunk containing a line object
like { type: 'unknown', content: '...' } and assert it returns without hanging
and produces rows/changeBlocks (e.g., no infinite loop and same empty-output
behavior or a predictable placeholder), thereby exercising the safety path in
the buildRows implementation and ensuring the i++ fallback remains covered.

In `@packages/bruno-app/src/components/OpenAPISyncTab/SpecDiffModal/buildRows.js`:
- Around line 139-145: The condition "row.leftKind === 'hunk' || !isChanged" is
redundant because hunk rows yield !isChanged already; in the rows.forEach loop
simplify the branch by removing the explicit hunk check and only test !isChanged
(i.e., replace the if (row.leftKind === 'hunk' || !isChanged) { currentBlock =
null; return; } with a single !isChanged check) so currentBlock is cleared for
unchanged/hunk rows while keeping changed-row logic intact; refer to the
rows.forEach loop and the isChanged, row.leftKind/row.rightKind, and
currentBlock identifiers when making the change.

In `@packages/bruno-app/src/components/OpenAPISyncTab/SpecDiffModal/index.js`:
- Around line 109-128: Add stable Playwright testids to the navigation buttons:
update the two buttons that call goToChange(currentIndex - 1) and
goToChange(currentIndex + 1) (they have className "spec-diff-nav-btn" and use
currentIndex/changeCount) to include data-testid attributes (e.g.
data-testid="spec-diff-prev-btn" and data-testid="spec-diff-next-btn") so tests
can reliably select the Previous and Next change controls.
- Around line 59-71: The current behavior sets setCurrentIndex(0) when parsing
but never scrolls the virtuoso to that item; update the logic so the first
change is auto-scrolled when diffs load by invoking the same scroll behavior
used in goToChange for index 0 (or calling goToChange(0)) right after
changeBlocks are populated (e.g., in the parse completion handler or a useEffect
that watches changeBlocks length), ensuring you use
virtuosoRef.current?.scrollToIndex with the same align/behavior logic and then
setCurrentIndex(0) so the user lands on the first change instead of the top of
the file.
- Around line 29-57: The effect uses the stable `cache` (created by
`useState(createHighlightCache)`) but does not list it in the dependency array,
which trips the react-hooks linter; update the dependency array for the
useEffect that parses `specDrift.unifiedDiff` to include `cache` (i.e., deps
should be `[specDrift?.unifiedDiff, cache]`), or if you intentionally rely on
its stability, explicitly document that by memoizing/creating `cache` with
`useRef`/`useMemo` or add an inline eslint-disable comment; adjust around the
block referencing `Diff2Html.parse`, `buildRows`, `setRows`, `setChangeBlocks`,
`setCurrentIndex`, `setIsRendering`, and `setParseError` accordingly so the
linter no longer flags the missing dependency.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8837cc4a-28af-4af9-a14c-0f199f440677

📥 Commits

Reviewing files that changed from the base of the PR and between 1f5f726 and 9d83829.

📒 Files selected for processing (7)
  • packages/bruno-app/src/components/OpenAPISyncTab/SpecDiffModal/DiffRow.js
  • packages/bruno-app/src/components/OpenAPISyncTab/SpecDiffModal/__tests__/buildRows.spec.js
  • packages/bruno-app/src/components/OpenAPISyncTab/SpecDiffModal/buildRows.js
  • packages/bruno-app/src/components/OpenAPISyncTab/SpecDiffModal/highlightCache.js
  • packages/bruno-app/src/components/OpenAPISyncTab/SpecDiffModal/index.js
  • packages/bruno-app/src/components/OpenAPISyncTab/StyledWrapper.js
  • packages/bruno-app/src/components/OpenAPISyncTab/SyncReviewPage/index.js

Comment on lines +94 to +120
// Collect the next run of deletions, then the run of insertions that
// immediately follows. Pair them 1:1 into side-by-side change rows;
// any leftovers spill as solo rows.
//
// e.g. del A, del B, del C, ins X, ins Y
// → (A ↔ X) (B ↔ Y) (C ↔ ∅)
const deletions = [];
while (i < lines.length && lines[i].type === 'delete') {
deletions.push(lines[i]);
i++;
}
const insertions = [];
while (i < lines.length && lines[i].type === 'insert') {
insertions.push(lines[i]);
i++;
}

const pairCount = Math.min(deletions.length, insertions.length);
for (let p = 0; p < pairCount; p++) {
rows.push(pairedChangeRow(deletions[p], insertions[p]));
}
for (let p = pairCount; p < deletions.length; p++) {
rows.push(soloDeletionRow(deletions[p]));
}
for (let p = pairCount; p < insertions.length; p++) {
rows.push(soloInsertionRow(insertions[p]));
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Pairing assumes Diff2Html's del-before-ins ordering — document or assert.

The collector walks deletions then insertions, so input like [insert, delete] at the same position produces two solo rows (one empty-on-left, one empty-on-right) instead of one paired change row with word-level highlight. Diff2Html normalizes to delins in practice, so this is fine today, but the invariant is load-bearing and only implied by the comment example on Line 98.

A one-line comment upgrade (or a dev-only console.warn on unexpected ordering) would future-proof this against a Diff2Html behavior change.

#!/bin/bash
# Confirm Diff2Html emits delete-before-insert consistently.
rg -nP -C3 'type\s*:\s*.insert|LineType\.Insert' packages/bruno-app/public/static/diff2Html*.js 2>/dev/null | head -60
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/bruno-app/src/components/OpenAPISyncTab/SpecDiffModal/buildRows.js`
around lines 94 - 120, The pairing logic in buildRows.js assumes Diff2Html emits
deletions before insertions, which can cause unpaired solo rows for inputs like
[insert, delete]; update the code around the deletions/insertions collection
(the blocks that fill deletions and insertions and the subsequent pairing loop
that calls pairedChangeRow, soloDeletionRow, soloInsertionRow) to either assert
or log a dev-only warning when an insertion is encountered before a preceding
deletion (i.e., unexpected ordering), and add a one-line comment documenting the
required invariant (Diff2Html emits del before ins) so future readers know the
assumption; use the existing variable names deletions, insertions,
pairedChangeRow, soloDeletionRow, and soloInsertionRow to locate the exact place
to add the check/comment.

Comment on lines +11 to +13
// Character-level diff for a paired deletion + insertion row. Returns
// { left, right } HTML strings with <del>/<ins> wrapping changed substrings.
getWordDiff(leftContent, rightContent) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Doc nit: comment says "character-level" but config is matching: 'words'.

Small inconsistency between the JSDoc on Line 11–12 ("Character-level diff…") and the actual diffHighlight call on Line 30 which uses matching: 'words'. The rest of the file (and the buildRows docstring) consistently calls this "word-level," so a one-word fix keeps the story straight.

📝 Proposed doc tweak
-    // Character-level diff for a paired deletion + insertion row. Returns
-    // { left, right } HTML strings with <del>/<ins> wrapping changed substrings.
+    // Word-level diff for a paired deletion + insertion row. Returns
+    // { left, right } HTML strings with <del>/<ins> wrapping changed tokens.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@packages/bruno-app/src/components/OpenAPISyncTab/SpecDiffModal/highlightCache.js`
around lines 11 - 13, The JSDoc above getWordDiff incorrectly says
"Character-level diff" while the function uses diffHighlight with matching:
'words'; update the comment to "Word-level diff" (or similar) to match the
implementation and other docs; locate the comment directly above the getWordDiff
function and change the wording so it consistently describes word-level diffing
and mentions diffHighlight/matching: 'words' if helpful.

Comment on lines +74 to +80
const renderItem = (index) => (
<DiffRow
row={rows[index]}
active={!!activeBlock && index >= activeBlock.startIdx && index <= activeBlock.endIdx}
cache={cache}
/>
);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check whether React Compiler is actually enabled for this package and how it treats the file.
fd -t f 'babel.config*' packages/bruno-app -x cat {} \;
fd -t f '.babelrc*' packages/bruno-app -x cat {} \;
fd -t f 'next.config*' packages/bruno-app -x cat {} \;
rg -nP 'babel-plugin-react-compiler|react-compiler' packages/bruno-app -C2
rg -nP '"use memo"|"use no memo"' packages/bruno-app/src/components/OpenAPISyncTab/SpecDiffModal

Repository: usebruno/bruno

Length of output: 969


🏁 Script executed:

cat -n packages/bruno-app/src/components/OpenAPISyncTab/SpecDiffModal/index.js | head -100

Repository: usebruno/bruno

Length of output: 4558


🏁 Script executed:

# Check if there's a PR description or commit mentioning React Compiler
git log --oneline --all --grep="React Compiler" -- packages/bruno-app/src/components/OpenAPISyncTab/SpecDiffModal | head -20

Repository: usebruno/bruno

Length of output: 40


🏁 Script executed:

# Search for how renderItem dependencies (activeBlock, cache, rows) are declared
rg -n "activeBlock|cache.*=|const rows" packages/bruno-app/src/components/OpenAPISyncTab/SpecDiffModal/index.js

Repository: usebruno/bruno

Length of output: 291


Add useCallback to stabilize renderItem reference for Virtuoso.

renderItem recreates on every SpecDiffModal render because it depends on dynamic values (activeBlock changes when currentIndex changes, rows changes on re-parse). React Compiler cannot hoist closures with unstable dependencies, so the function reference changes each time, forcing Virtuoso to potentially re-work visible rows.

Wrap with useCallback:

Suggested fix
const renderItem = useCallback((index) => (
  <DiffRow
    row={rows[index]}
    active={!!activeBlock && index >= activeBlock.startIdx && index <= activeBlock.endIdx}
    cache={cache}
  />
), [rows, activeBlock, cache]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/bruno-app/src/components/OpenAPISyncTab/SpecDiffModal/index.js`
around lines 74 - 80, renderItem inside SpecDiffModal is recreated on every
render causing Virtuoso to receive a new function reference; wrap renderItem
with React.useCallback so its identity is stable and only changes when its
dependencies change (rows, activeBlock, cache) — update the renderItem
declaration to use useCallback((index) => <DiffRow row={rows[index]}
active={!!activeBlock && index >= activeBlock.startIdx && index <=
activeBlock.endIdx} cache={cache} />, [rows, activeBlock, cache]).

Comment on lines +90 to +105
<div className="spec-diff-header">
<div className="spec-diff-header-left">
<div className="spec-diff-badges">
<div>Endpoint Changes:</div>
{modifiedCount > 0 && <StatusBadge status="warning">Updated: {modifiedCount}</StatusBadge>}
{addedCount > 0 && <StatusBadge status="success">Added: {addedCount}</StatusBadge>}
{removedCount > 0 && <StatusBadge status="danger">Removed: {removedCount}</StatusBadge>}
{versionLabel && <StatusBadge>{versionLabel}</StatusBadge>}
</div>

<p className="spec-diff-subtitle">
{specDrift?.storedSpecMissing
? 'The current spec file is missing. The full remote spec is shown below.'
: 'Side-by-side diff of your current spec vs the updated spec from the spec URL.'}
</p>
<p className="spec-diff-subtitle">
{specDrift?.storedSpecMissing
? 'The current spec file is missing. The full remote spec is shown below.'
: 'Side-by-side diff of your current spec vs the updated spec from the spec URL.'}
</p>
</div>
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Confirm whether .spec-diff-header-left is styled anywhere.
rg -nP '\bspec-diff-header-left\b' packages/bruno-app/src

Repository: usebruno/bruno

Length of output: 183


🏁 Script executed:

#!/bin/bash
# Find StyledWrapper file in or near the SpecDiffModal directory
fd -type f "StyledWrapper" packages/bruno-app/src/components/OpenAPISyncTab/SpecDiffModal/

Repository: usebruno/bruno

Length of output: 229


🏁 Script executed:

#!/bin/bash
# If not found in SpecDiffModal, search more broadly
fd -type f "StyledWrapper" packages/bruno-app/src/components/OpenAPISyncTab/

Repository: usebruno/bruno

Length of output: 229


🏁 Script executed:

#!/bin/bash
# Also check for any CSS files in the directory
fd -type f "\.(css|scss)" packages/bruno-app/src/components/OpenAPISyncTab/SpecDiffModal/

Repository: usebruno/bruno

Length of output: 229


🏁 Script executed:

#!/bin/bash
# Find StyledWrapper file
fd "StyledWrapper" packages/bruno-app/src/components/OpenAPISyncTab/SpecDiffModal/

Repository: usebruno/bruno

Length of output: 40


🏁 Script executed:

#!/bin/bash
# Search for any style-related files (js, css, scss) in the SpecDiffModal directory
ls -la packages/bruno-app/src/components/OpenAPISyncTab/SpecDiffModal/

Repository: usebruno/bruno

Length of output: 483


🏁 Script executed:

#!/bin/bash
# Search more broadly for StyledWrapper in OpenAPISyncTab
fd "StyledWrapper" packages/bruno-app/src/components/OpenAPISyncTab/

Repository: usebruno/bruno

Length of output: 124


🏁 Script executed:

#!/bin/bash
# Get file size
wc -l packages/bruno-app/src/components/OpenAPISyncTab/StyledWrapper.js

Repository: usebruno/bruno

Length of output: 129


🏁 Script executed:

#!/bin/bash
# Search for spec-diff-header styles in StyledWrapper.js
rg -n "spec-diff-header" packages/bruno-app/src/components/OpenAPISyncTab/StyledWrapper.js

Repository: usebruno/bruno

Length of output: 89


🏁 Script executed:

#!/bin/bash
# Read the spec-diff-header style definition and surrounding area
sed -n '1675,1720p' packages/bruno-app/src/components/OpenAPISyncTab/StyledWrapper.js

Repository: usebruno/bruno

Length of output: 1350


🏁 Script executed:

#!/bin/bash
# Also search the entire file for any occurrence of spec-diff-header-left
rg -n "spec-diff-header-left" packages/bruno-app/src/components/OpenAPISyncTab/StyledWrapper.js

Repository: usebruno/bruno

Length of output: 40


.spec-diff-header-left has no styles defined in StyledWrapper.js.

The JSX uses spec-diff-header-left as a wrapper for the badges and subtitle, but it's not styled anywhere. The parent .spec-diff-header sets display: flex; align-items: flex-end; justify-content: space-between;, so the unstyled child will render correctly with default block behavior, but if your intent was to control stacking or spacing on the left side, add the style or remove the wrapper.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/bruno-app/src/components/OpenAPISyncTab/SpecDiffModal/index.js`
around lines 90 - 105, The wrapper element with class spec-diff-header-left is
unused in StyledWrapper.js; either add a style block for .spec-diff-header-left
inside StyledWrapper (e.g., to control layout/spacing/stacking of
.spec-diff-badges and .spec-diff-subtitle) or remove the wrapper in
SpecDiffModal to simplify markup; update StyledWrapper.js to include a
.spec-diff-header-left rule (targeting spacing, flex/column behavior, and
gap/margin for its children like .spec-diff-badges and .spec-diff-subtitle) if
you want explicit left-side layout control.

Comment on lines +1532 to 1536
> div[data-testid='virtuoso-scroller'],
> div:last-child {
flex: 1 1 auto;
min-height: 0;
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

Does react-virtuoso always render its scroller element with data-testid="virtuoso-scroller" in latest version?

💡 Result:

Yes—at least in the current upstream implementation, the underlying scroller element rendered by react-virtuoso’s default Scroller component includes data-testid="virtuoso-scroller". In the repository source, the buildScroller implementation renders ScrollerComponent with data-testid="virtuoso-scroller" (and also data-virtuoso-scroller={true}) on the scroller DOM node. Caveat: react-virtuoso supports customization of the Scroller element via props/components; if you replace ScrollerComponent, you may no longer get this test id.

Citations:


🏁 Script executed:

# Find theme definitions to check for theme.colors.text.yellow
fd -t f -e 'js' -e 'ts' -e 'json' | xargs rg 'colors.*text.*yellow|theme.*colors' --type-list | head -20

Repository: usebruno/bruno

Length of output: 448


🏁 Script executed:

# Search for theme files in the Bruno app
find . -path '*/theme*' -o -name '*theme*' -o -name '*colors*' | grep -E '\.(js|ts|json)$' | head -20

Repository: usebruno/bruno

Length of output: 1104


🏁 Script executed:

# Look for the StyledWrapper.js file and check its imports/context
fd -t f 'StyledWrapper.js' -E 'node_modules'

Repository: usebruno/bruno

Length of output: 22991


🏁 Script executed:

# Check the component structure and how loading/empty states are rendered
rg 'text-diff-loading|text-diff-empty' -B 2 -A 2

Repository: usebruno/bruno

Length of output: 3071


🏁 Script executed:

# Check theme files for yellow color definition
cat packages/bruno-app/src/themes/dark/dark.js | head -100

Repository: usebruno/bruno

Length of output: 2243


🏁 Script executed:

# Check light theme
cat packages/bruno-app/src/themes/light/light.js | head -100

Repository: usebruno/bruno

Length of output: 2296


🏁 Script executed:

# Search for theme.colors.text.yellow pattern in StyledWrapper
rg 'colors\.text\.yellow' packages/bruno-app/src/components/OpenAPISyncTab/

Repository: usebruno/bruno

Length of output: 651


🏁 Script executed:

# Check for color-mix usage and any fallbacks in StyledWrapper
rg 'color-mix' packages/bruno-app/src/components/OpenAPISyncTab/StyledWrapper.js -A 2 -B 2

Repository: usebruno/bruno

Length of output: 993


🏁 Script executed:

# Get the exact line 1559 from StyledWrapper.js
sed -n '1555,1565p' packages/bruno-app/src/components/OpenAPISyncTab/StyledWrapper.js

Repository: usebruno/bruno

Length of output: 426


🏁 Script executed:

# Search for theme.colors.text mapping/definition
rg 'theme\.colors\.text|colors\.text\s*=' packages/bruno-app/src -A 5 -B 2 | head -100

Repository: usebruno/bruno

Length of output: 8945


🏁 Script executed:

# Look for where yellow is mapped to theme
rg 'text.*yellow|yellow.*text' packages/bruno-app/src/themes/ -A 2 -B 2

Repository: usebruno/bruno

Length of output: 507


🏁 Script executed:

# Check package.json for Electron version
cat packages/bruno-app/package.json | rg 'electron' -A 2 -B 2

Repository: usebruno/bruno

Length of output: 40


🏁 Script executed:

# Check for Electron minimum version or compatibility notes
rg 'electron' packages/bruno-app/src -l | head -5

Repository: usebruno/bruno

Length of output: 219


🏁 Script executed:

# Search for where yellow is added to theme colors
rg 'yellow' packages/bruno-app/src/themes/ -B 3 -A 3

Repository: usebruno/bruno

Length of output: 9236


🏁 Script executed:

# Check the theme index to see how colors are mapped
cat packages/bruno-app/src/themes/index.js | head -80

Repository: usebruno/bruno

Length of output: 2082


🏁 Script executed:

# Check the package.json for Electron version
cat packages/bruno-app/package.json | grep -A 5 -B 5 '"electron"'

Repository: usebruno/bruno

Length of output: 40


🏁 Script executed:

# Search for any mentions of color-mix or browser compatibility
rg 'color-mix|caniuse|browser|support' packages/bruno-app -i | head -20

Repository: usebruno/bruno

Length of output: 2371


🏁 Script executed:

# Get electron version from package.json
cat packages/bruno-app/package.json | jq '.devDependencies.electron' 2>/dev/null || grep -A 1 '"electron"' packages/bruno-app/package.json

Repository: usebruno/bruno

Length of output: 63


🏁 Script executed:

# Check for color-mix fallbacks or polyfills
rg 'color-mix' packages/bruno-app/src -B 5 -A 5 | grep -E 'fallback|polyfill|rgba|rgba' -i

Repository: usebruno/bruno

Length of output: 360


🏁 Script executed:

# Verify the exact line 1559 context - does it have fallback?
sed -n '1555,1565p' packages/bruno-app/src/components/OpenAPISyncTab/StyledWrapper.js

Repository: usebruno/bruno

Length of output: 426


🏁 Script executed:

# Check all other yellow usages to confirm fallback pattern
rg 'colors\.text\.yellow' packages/bruno-app/src/components/OpenAPISyncTab/StyledWrapper.js -B 1 -A 1

Repository: usebruno/bruno

Length of output: 581


🏁 Script executed:

# Look at the parent structure to confirm .text-diff-loading/.text-diff-empty are direct children
rg -A 30 'text-diff-container' packages/bruno-app/src/components/OpenAPISyncTab/SpecDiffModal/index.js | head -50

Repository: usebruno/bruno

Length of output: 1462


The :last-child fallback selector will match loading/empty states when rendered.

When isRendering is true, .text-diff-loading becomes the last child of .text-diff-container; when there's a parse error or no rows to display, .text-diff-empty becomes the last child. Both will receive flex: 1 1 auto; min-height: 0; as a side effect. While these styles don't break anything (the divs are already flex-centered), the selector is broader than intended.

Since react-virtuoso reliably renders with data-testid="virtuoso-scroller", consider tightening to just the testid selector unless you need the fallback for custom Virtuoso implementations in this codebase.

Additionally, note the inconsistency: Line 1559 applies theme.colors.text.yellow without a fallback in the box-shadow, while lines 1564–1565 use it with || '#f59e0b'. Since yellow is a required theme field (all themes define it), either remove all fallbacks or keep them consistent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/bruno-app/src/components/OpenAPISyncTab/StyledWrapper.js` around
lines 1532 - 1536, The current selector "div:last-child" in StyledWrapper
(targeting children of .text-diff-container) is too broad and unintentionally
matches .text-diff-loading and .text-diff-empty; replace the fallback selector
with the specific testid selector (only keep
div[data-testid='virtuoso-scroller']) so only the Virtuoso scroller receives
flex: 1 1 auto; min-height: 0, and remove the broader div:last-child rule. Also
make the theme color usage consistent for the yellow shadow: either remove the
"|| '#f59e0b'" fallback where theme.colors.text.yellow is used (so all usages
rely on the required theme field) or add the same fallback everywhere—update the
box-shadow occurrences that reference theme.colors.text.yellow to match
(.text-diff-loading/.text-diff-empty context is relevant).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant