Skip to content

feat(ui): let a host contribute per-component controls and drive selection - #10719

Open
luvkapur wants to merge 5 commits into
masterfrom
feat/compare-host-actions
Open

luvkapur wants to merge 5 commits into
masterfrom
feat/compare-host-actions

Conversation

@luvkapur

Copy link
Copy Markdown
Member

Why

Two extension points that the compare redesign (#10378) left without a home. Both block bit.cloud from moving its change-request view onto the new compare.

Per-component controls

The pre-redesign drawer UI had DrawerWidgets slots, which bit.cloud used to hang review affordances off each component — an "include in review" checkbox, a button into that component's discussion thread. The mounted-panels design replaced drawers and dropped the slots, so today those controls have nowhere to go.

<LaneCompare
  renderComponentActions={useCallback(({ componentId }) => <ReviewControls id={componentId} />, [])}
  ...
/>

It is handed straight to each InlineComponentCompare and rendered at the trailing edge of that component's header.

It is a function, not a node, on purpose. A node per component would be a fresh prop on every parent render, defeating the React.memo that makes a view-mode switch a CSS attribute flip rather than ten panel re-renders. The function is called during each panel's own render, so a stable reference still produces up-to-date controls. The requirement is documented on both props.

Nothing about reviews reaches lane-compare. It passes a context and renders whatever comes back:

export type ComponentActionsContext = {
  name: string;
  componentId: string; // without version — the stable identity of the row
  baseId?: string;
  compareId: string;
};

Selection from the URL

?componentId= and ?file= were read once, into initial state. A host that navigates to a component after the view is mounted — following a link to a discussion attached to one — got nothing.

lane-compare writes its own selection with history.replaceState, which react-router does not observe, so a change in those params can only have come from a real navigation. That makes adopting it unambiguous. The effect is seeded with the mount-time value so it skips the first run, leaving page load to the existing initial-scroll effect (which additionally waits for the diff to load).

Testing

Four specs in component-compare-actions.spec.tsx: the header renders host actions, adds no markup when there are none, is asked with the right component identity, and renders what came back.

teambit.component/ui/component-compare/component-compare - 4 passed

oxlint --deny-warnings clean on both components.

The URL-selection change is not covered by a spec — it needs a router plus a loaded lane diff to be meaningful, and I would rather not assert on a mock of both. Called out here so it gets eyes in review.

🤖 Generated with Claude Code

…ction

Two extension points that the compare redesign left without a home.

**Per-component controls.** The pre-redesign drawer UI had `DrawerWidgets`
slots, which bit.cloud's change-request view used to hang review
affordances off each component — a "include in review" checkbox, a button
into that component's discussion thread. The mounted-panels design
replaced drawers and dropped the slots, so those controls have nowhere to
go and the cloud cannot move onto the new compare without losing them.

`LaneCompare` now takes `renderComponentActions`, handed straight to each
`InlineComponentCompare` and rendered at the trailing edge of that
component's header. It is a function rather than a node on purpose: a node
per component would be a fresh prop on every parent render and would
defeat the `React.memo` that makes a view-mode switch a CSS attribute flip
instead of ten panel re-renders. The contract (must be referentially
stable) is documented on both props.

Nothing about reviews reaches lane-compare — it passes a context
(`name`, `componentId` without version, `baseId`, `compareId`) and renders
whatever comes back.

**Selection from the URL.** `?componentId=` and `?file=` were read once,
into initial state. A host that navigates to a component after the view is
already mounted — following a link to a discussion attached to one — got
nothing. lane-compare writes its own selection with `history.replaceState`,
which react-router does not observe, so a *change* in those params can only
have come from a real navigation; adopting it is unambiguous. The effect is
seeded with the mount-time value so it skips the first run and leaves page
load to the existing initial-scroll effect.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Add host actions and URL-driven selection to component compare

✨ Enhancement 🐞 Bug fix 🧪 Tests 🕐 20-40 Minutes

Grey Divider

AI Description

• Adds a stable host callback for per-component controls in compare headers.
• Synchronizes mounted compare selection and scrolling with externally changed URL parameters.
• Tests action rendering, component identity context, and empty-header behavior.
Diagram

graph TD
  Host["Host UI"] -->|"stable renderer"| Lane["Lane Compare"] -->|"forwards callback"| Inline["Inline Compare"] -->|"renders actions"| Header["Component Header"]
  Nav["URL Navigation"] -->|"componentId + file"| Lane -->|"selects and scrolls"| Panel["Selected Panel"]
Loading
High-Level Assessment

The chosen approach is appropriate: a stable render callback preserves the memoized panel boundary while keeping review-specific behavior outside compare, and observing changed query parameters restores deep-link navigation with minimal API expansion. Passing React nodes or introducing review-specific context would increase rerenders or couple the generic compare UI to host concerns.

Files changed (5) +143 / -1

Enhancement (3) +40 / -1
component-compare.module.scssLay out host controls in component headers +10/-0

Lay out host controls in component headers

• Adds a flex container for trailing host actions while deliberately leaving typography and colors to the host design system.

components/ui/component-compare/component-compare/component-compare.module.scss

component-compare.tsxRender host actions with stable component context +25/-0

Render host actions with stable component context

• Defines the component action context and render callback contract, invokes it within the memoized inline panel, and displays returned controls in the header. The context exposes stable unversioned identity alongside base and compare IDs.

components/ui/component-compare/component-compare/component-compare.tsx

index.tsExport the component action context type +5/-1

Export the component action context type

• Exports ComponentActionsContext from the component-compare package so Lane Compare and external hosts can type their action renderers.

components/ui/component-compare/component-compare/index.ts

Tests (1) +73 / -0
component-compare-actions.spec.tsxTest host-contributed component header actions +73/-0

Test host-contributed component header actions

• Adds focused tests for action rendering, absent-action markup, callback context identity, and callback output. Stubs API diff loading and IntersectionObserver to isolate the header behavior.

components/ui/component-compare/component-compare/component-compare-actions.spec.tsx

Other (1) +30 / -0
lane-compare.tsxForward host actions and adopt external URL selection +30/-0

Forward host actions and adopt external URL selection

• Adds the renderComponentActions extension point and forwards it to every inline component comparison. Reacts to post-mount componentId and file query changes by updating selection and scrolling without interfering with initial-load scrolling.

components/ui/compare/lane-compare/lane-compare.tsx

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Sep 18, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (6) 📘 Rule violations (1) 📜 Skill insights (0)

Grey Divider


Action required

1. Switching comparisons briefly shows old diffs 🐞 Bug ≡ Correctness ⭐ New
Description
useBulkPagedQuery returns fallbackData and fallbackDone from the preceding fallback session
during render because their reset is deferred to a passive effect. When an unsupported host changes
host or base/compare pairs, overlapping identifiers can expose old comparisons while absent
identifiers resolve to null, reaching component panels and the file registry as settled failures
before current-session requests start.
Code

components/ui/hooks/use-bulk-paged-query/use-bulk-paged-query.ts[282]

+    if (onFallback) return fallbackData;
Evidence
A sessionKey change restarts the effect, but the fallback map and completion flag are reset only
after the new session has rendered. During that render, requestedCompareIds already describes the
new pairs while dataByCompareId and done still come from the prior session, so consumers that
treat non-undefined lookups as settled can display an old comparison for an overlapping key or
classify a missing new identifier as failed; registry consumers then turn those failures into empty
registrations.

components/ui/hooks/use-bulk-paged-query/use-bulk-paged-query.ts[175-182]
components/ui/hooks/use-bulk-paged-query/use-bulk-paged-query.ts[204-205]
components/ui/hooks/use-bulk-paged-query/use-bulk-paged-query.ts[220-222]
components/ui/hooks/use-bulk-paged-query/use-bulk-paged-query.ts[281-292]
components/ui/component-compare/component-compare/component-compare.tsx[829-846]
components/ui/hooks/use-bulk-paged-query/use-bulk-paged-query.ts[175-185]
components/ui/hooks/use-bulk-paged-query/use-bulk-paged-query.ts[209-222]
components/ui/hooks/use-bulk-paged-query/use-bulk-paged-query.ts[281-313]
components/ui/component-compare/component-compare/component-compare.tsx[1029-1051]
components/ui/component-compare/component-compare/file-registry.tsx[169-185]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Fallback data and completion state are not associated with their originating session, so a host or pair change can render once using the preceding session's map and completed state. A new fallback session must synchronously expose empty, pending values until its own workers populate data, preventing old results from satisfying new lookups and missing identifiers from being classified as failures.

## Fix Focus Areas
- components/ui/hooks/use-bulk-paged-query/use-bulk-paged-query.ts[175-182]
- components/ui/hooks/use-bulk-paged-query/use-bulk-paged-query.ts[219-222]
- components/ui/hooks/use-bulk-paged-query/use-bulk-paged-query.ts[278-313]

## Recommended Fix
Store the fallback `sessionKey` together with its data and completion state, or otherwise synchronously invalidate the displayed fallback session when the key changes. When the stored key differs from the current `sessionKey`, return an empty map and `done: false` instead of exposing the preceding session, then populate state for the current key from the fallback workers.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Back navigation keeps old selection 🐞 Bug ≡ Correctness ⭐ New
Description
useUrlSelection seeds lastSeenKey from the mount-time URL and only calls apply when the
router-observed key differs from that original value. If a user selects a component through local
state and replaceState, then navigates to an unselected URL matching the initially empty key, the
local component and file selections remain set because the hook ignores the navigation.
Code

components/ui/compare/lane-compare/use-url-selection.ts[R43-52]

+  const currentKey = keyOf(selection);
+  const lastSeenKey = useRef(currentKey);
+  // seeded so a deep link scrolls once the diff has loaded, without a second effect to race with
+  const pending = useRef<UrlSelection | undefined>(selection.componentId ? selection : undefined);
+  const inFlight = useRef<AbortController | undefined>(undefined);
+
+  useEffect(() => {
+    if (currentKey === lastSeenKey.current) return;
+    lastSeenKey.current = currentKey;
+    apply(selection);
Evidence
Lane compare updates local selection and the address bar through replaceState, which React Router
does not observe. Since the hook remembers only the mount-time router key, navigating later to that
same key returns early even though local state was changed in the interim.

components/ui/compare/lane-compare/lane-compare.tsx[285-299]
components/ui/compare/lane-compare/lane-compare.tsx[323-358]
components/ui/compare/lane-compare/use-url-selection.ts[38-57]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

Issue description
A router navigation back to the initial empty URL selection is ignored after local selection changed the URL through `history.replaceState`. The hook's observed key must remain coherent with locally written selection state so real navigation to an empty selection clears both local values.

Fix Focus Areas
- components/ui/compare/lane-compare/use-url-selection.ts[42-57]
- components/ui/compare/lane-compare/lane-compare.tsx[293-331]

Recommended Fix
Synchronize the hook's last-observed selection with locally written selection changes, or pass the current local selection into the hook and compare router selection against it rather than only its mount-time key. Preserve the requirement that `replaceState` itself does not re-apply state, while ensuring a subsequent router navigation with no parameters calls `apply({})`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Changed UI code fails repository lint 📘 Rule violation ≡ Correctness
Description
finish references onAbort before its const declaration, while the adjacent suppression names a
different rule than the configured core check. Running canonical npm run lint therefore reaches
this changed helper and reports no-use-before-define as an error.
Code

components/ui/compare/lane-compare/lane-compare.tsx[R111-112]

+      // eslint-disable-next-line @typescript-eslint/no-use-before-define
+      signal?.removeEventListener('abort', onAbort);
Evidence
Compliance rule 3 requires changed code to pass the canonical lint command. The changed helper uses
onAbort before its constant declaration, while .oxlintrc.json enables the core
no-use-before-define rule as an error and the adjacent comment suppresses a differently named
TypeScript rule.

CLAUDE.md: Code Must Pass the Repository Lint and Type-Checking Configuration: CLAUDE.md: Code Must Pass the Repository Lint and Type-Checking Configuration: CLAUDE.md: Code Must Pass the Repository Lint and Type-Checking Configuration: CLAUDE.md: Code Must Pass the Repository Lint and Type-Checking Configuration
components/ui/compare/lane-compare/lane-compare.tsx[108-115]
.oxlintrc.json[14-19]
package.json[18-25]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new `finish` callback references the later `onAbort` constant, and its suppression targets `@typescript-eslint/no-use-before-define` rather than the configured core `no-use-before-define` rule.
## Fix Focus Areas
- components/ui/compare/lane-compare/lane-compare.tsx[108-115]
## Recommended Fix
Convert `finish` and `onAbort` to function declarations so their mutual references are valid under the configured rule, and remove the ineffective suppression comment.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View action required (2)
4. Cached diffs can miss deep-link scrolling 🐞 Bug ≡ Correctness
Description
useUrlSelection retains the pending target when scrollTo returns false, but its retry effect
only depends on the URL key and loading. When cached lane-diff data renders the previously absent
pane without changing either value, the stored request is never retried and the requested component
is not scrolled into view.
Code

components/ui/compare/lane-compare/use-url-selection.ts[R69-74]

+    // keep the request if the pane is not mounted yet; the next loading change retries it
+    if (scrollTo({ ...target, componentId: target.componentId }, controller.signal)) {
+      pending.current = undefined;
+    }
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [currentKey, loading]);
Evidence
The hook explicitly retains requests after a failed scroll but reruns only for URL or loading
changes. LaneCompareInline returns a blank state without mounting diffPaneRef when no data is
currently present, while its provider documents that Apollo cache merges can later update laneDiff
and componentsToDiff without flipping loadingLaneDiff, proving the pane can appear without
either hook dependency changing.

components/ui/compare/lane-compare/use-url-selection.ts[59-74]
components/ui/compare/lane-compare/lane-compare.tsx[129-132]
components/ui/compare/lane-compare/lane-compare.tsx[663-707]
components/ui/compare/lane-compare/lane-compare.provider.tsx[61-70]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`useUrlSelection` preserves a target when the pane is unavailable, but no dependency tells it when the pane later becomes available. Cached lane-diff updates can render the pane while `loading` remains false, leaving the deep-link scroll pending forever.
## Fix Focus Areas
- components/ui/compare/lane-compare/use-url-selection.ts[59-74]
- components/ui/compare/lane-compare/lane-compare.tsx[343-357]
## Recommended Fix
Pass a pane-availability or diff-content retry token into `useUrlSelection` and include it in the scrolling effect dependencies. Change that token when the real diff pane mounts or its component set becomes available, so a retained target is retried even when the loading state never transitions; add a hook test covering `scrollTo` first returning false and then succeeding after only that retry token changes.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. URL navigation can regress undetected ✓ Resolved 📘 Rule violation ☼ Reliability
Description
The new URL-selection useEffect in LaneCompareInline has no corresponding unit test, while the
added suite covers only host-contributed header actions. Changes to router parameters during loading
or after mount can therefore break selection state, deferred scrolling, or file targeting without
being exercised by CI.
Code

components/ui/compare/lane-compare/lane-compare.tsx[R327-331]

+  useEffect(() => {
+    const key = `${urlComponentId ?? ''}|${urlFile ?? ''}`;
+    // seeded with the mount-time value, so this skips the first run and leaves the initial scroll
+    // below to handle page load.
+    if (key === lastUrlSelection.current) return;
Evidence
PR Compliance ID 2 requires testable behavior to have colocated unit coverage. The PR adds
post-mount URL selection and deferred scrolling at lane-compare.tsx lines 327-346, but the only
added test suite is explicitly scoped to host-contributed component actions and contains no
URL-navigation scenario.

CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests: CLAUDE.md: Prefer Efficient Unit Tests and Minimize Necessary E2E Tests
components/ui/compare/lane-compare/lane-compare.tsx[327-346]
components/ui/component-compare/component-compare/component-compare-actions.spec.tsx[23-75]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The newly added behavior that adopts `componentId` and `file` parameters after mount is not covered by a unit test, despite being testable with a memory router and mocked lane-diff state.
## Fix Focus Areas
- components/ui/compare/lane-compare/lane-compare.tsx[327-346]
- components/ui/compare/lane-compare/lane-compare.spec.tsx[1-1]
## Recommended Fix
Add a colocated unit test that mounts lane compare under a memory router, changes the URL parameters after mount, and verifies that the selected component and file are adopted. Include a loading transition to confirm deferred scrolling occurs once the diff pane becomes available.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

6. Skipped fallback queries stay loading 🐞 Bug ≡ Correctness ⭐ New
Description
useBulkPagedQuery exits the fallback effect when skip is true, while done still depends
exclusively on fallbackDone. This occurs for empty pair lists and inactive API views on a host
already marked as lacking the bulk field, leaving the hook's documented loading state unsettled
despite no request being eligible to run.
Code

components/ui/hooks/use-bulk-paged-query/use-bulk-paged-query.ts[179]

+    if (!onFallback || skip) return undefined;
Evidence
The fallback effect cannot update its completion flag after taking the skip guard, but the fallback
branch of done ignores skip; the returned loading value is the negation of that unresolved flag.
The API provider supplies skip whenever its mounted pane is inactive, and empty pair arrays also
force this path.

components/ui/hooks/use-bulk-paged-query/use-bulk-paged-query.ts[111-126]
components/ui/hooks/use-bulk-paged-query/use-bulk-paged-query.ts[175-182]
components/ui/hooks/use-bulk-paged-query/use-bulk-paged-query.ts[219-222]
components/ui/hooks/use-bulk-paged-query/use-bulk-paged-query.ts[294-313]
components/ui/api-diff-view/api-diff-data-context.tsx[86-93]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Fallback sessions return before setting `fallbackDone` when the query is skipped, but the returned `loading` state still depends on `fallbackDone` and therefore remains true indefinitely.

## Fix Focus Areas
- components/ui/hooks/use-bulk-paged-query/use-bulk-paged-query.ts[178-179]
- components/ui/hooks/use-bulk-paged-query/use-bulk-paged-query.ts[222-222]

## Recommended Fix
Include `skip` in the completion calculation so every skipped session reports `loading: false`, regardless of whether fallback mode is active. Add a fallback-enabled skipped-query test alongside the existing skipped-query coverage.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Dropped requests lose component diffs 🐞 Bug ☼ Reliability ⭐ New
Description
The fallback worker catches every client.query rejection and immediately stores null for the
requested pair instead of using the transient retry budget applied by the bulk paging path. A
temporary transport failure on the compatibility path therefore becomes a settled failed comparison
once fallbackDone is set, leaving that component unavailable for the remainder of the mounted
session.
Code

components/ui/hooks/use-bulk-paged-query/use-bulk-paged-query.ts[R199-201]

+        } catch {
+          // a pair that cannot be compared is reported as failed, exactly as the bulk path reports it
+          item = null;
Evidence
The fallback worker’s catch handles all query failures, including transient transport failures, and
unconditionally stores null, while the bulk paging loop retries rejected page requests up to
MAX_PAGE_RETRIES. The final lookup contract treats null as failed rather than pending, and once
the workers complete, fallbackDone makes that result settled instead of allowing another attempt.

components/ui/hooks/use-bulk-paged-query/use-bulk-paged-query.ts[5-6]
components/ui/hooks/use-bulk-paged-query/use-bulk-paged-query.ts[187-205]
components/ui/hooks/use-bulk-paged-query/use-bulk-paged-query.ts[224-279]
components/ui/hooks/use-bulk-paged-query/use-bulk-paged-query.ts[294-313]
components/ui/hooks/use-bulk-paged-query/use-bulk-paged-query.spec.tsx[91-103]
components/ui/hooks/use-bulk-paged-query/use-bulk-paged-query.ts[156-160]
components/ui/hooks/use-bulk-paged-query/use-bulk-paged-query.ts[193-202]
components/ui/hooks/use-bulk-paged-query/use-bulk-paged-query.ts[209-210]
components/ui/hooks/use-bulk-paged-query/use-bulk-paged-query.ts[294-306]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The compatibility worker converts the first single-pair request rejection into a permanent `null` result, even though the bulk paging path retries transient request failures. Requested pairs should remain unresolved while retries remain and only settle as unavailable after the retry budget is exhausted.

## Fix Focus Areas
- components/ui/hooks/use-bulk-paged-query/use-bulk-paged-query.ts[187-211]
- components/ui/hooks/use-bulk-paged-query/use-bulk-paged-query.ts[220-222]
- components/ui/hooks/use-bulk-paged-query/use-bulk-paged-query.ts[294-313]
- components/ui/hooks/use-bulk-paged-query/use-bulk-paged-query.spec.tsx[91-103]

## Recommended Fix
Add bounded per-pair retries for rejected fallback queries, using the existing transient retry budget or an equivalent explicit policy. Preserve cancellation checks between attempts, keep unresolved pairs loading while a retry remains, and store `null` only after retries are exhausted. Update the fallback tests to verify recovery from a temporary failure and terminal failure after exhaustion of the retry budget.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


8. Removed components lose their identity 🐞 Bug ≡ Correctness
Description
ComponentIdentityProvider derives componentId only from compareId, which lane compare sets to
an empty string for entries with no compare-side component. For removed components, host header
actions therefore receive an empty stable ID and cannot key review controls or discussion links to
the component, even though baseId identifies it.
Code

components/ui/component-compare/component-compare/component-compare.tsx[679]

+        componentId={compareId.split('@')[0]}
Evidence
Lane diff extraction permits a component to exist only on the base side, and lane compare preserves
its base ID while passing an empty compare ID into the panel. The newly added provider then derives
the host-visible identity solely from that empty compare ID.

components/ui/compare/lane-compare/lane-compare.utils.ts[21-27]
components/ui/compare/lane-compare/lane-compare.tsx[361-380]
components/ui/compare/lane-compare/lane-compare.tsx[795-809]
components/ui/component-compare/component-compare/component-compare.tsx[674-699]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Removed components have no compare-side ID, but the component identity context derives its stable ID exclusively from `compareId`. Host-contributed header controls consequently receive an empty `componentId` for those rows.
## Fix Focus Areas
- components/ui/component-compare/component-compare/component-compare.tsx[674-699]
## Recommended Fix
Derive the versionless component identity from `compareId` when available and fall back to `baseId` for removed components. Reuse that derived value for the identity provider, header, and DOM component anchor so all consumers agree on the row identity.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View review recommended (3)
9. Clearing selection scrolls an old target ✓ Resolved 🐞 Bug ≡ Correctness
Description
pendingScroll is assigned only when urlComponentId is present, so removing the component
parameter does not clear an earlier pending request. If selection is set and then cleared during a
lane-diff reload, the retry effect consumes the stale request when loading finishes and scrolls to
the previously selected component.
Code

components/ui/compare/lane-compare/lane-compare.tsx[338]

+    if (urlComponentId) pendingScroll.current = { id: urlComponentId, file: urlFile };
Evidence
The URL effect updates both selected state values but only updates pendingScroll for a truthy
component ID. While loading, the pane is replaced by a skeleton; after loading, the retry effect
consumes any request still held by that ref, proving that a set-then-clear sequence can scroll the
old target.

components/ui/compare/lane-compare/lane-compare.tsx[327-346]
components/ui/compare/lane-compare/lane-compare.tsx[674-686]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Clearing URL-driven selection during loading leaves the previous component in `pendingScroll`, allowing it to scroll after the current URL no longer selects anything.
## Fix Focus Areas
- components/ui/compare/lane-compare/lane-compare.tsx[327-346]
## Recommended Fix
Assign `pendingScroll.current` for every URL selection change, setting it to `undefined` when `urlComponentId` is absent. Ensure the retry effect cannot consume a target from an earlier URL state.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


10. Rapid navigation returns to the old file ✓ Resolved 🐞 Bug ≡ Correctness
Description
The new navigation effect starts scrollToElement for every destination without cancelling the
promise and mutation observer created for the preceding destination. When an older component's
lazily rendered file anchor appears after a newer navigation has already scrolled, that older
callback scrolls the pane back to the obsolete file.
Code

components/ui/compare/lane-compare/lane-compare.tsx[345]

+    scrollToElement(diffPaneRef.current, pending.id, pending.file);
Evidence
A missing file anchor causes scrollToElement to install a mutation observer for up to five
seconds, and its eventual callback scrolls without checking the current selection. Compare panels
initially expose only their component anchor, then stagger mounting of their content; file anchors
therefore appear later and can resolve an older observer after a subsequent URL navigation has
started another scroll.

components/ui/compare/lane-compare/lane-compare.tsx[93-139]
components/ui/compare/lane-compare/lane-compare.tsx[327-346]
components/ui/component-compare/component-compare/component-compare.tsx[651-708]
components/ui/inline-code-compare/inline-code-compare.tsx[80-105]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Successive URL navigations can leave multiple asynchronous file-anchor waits active, allowing an older request to override the latest scroll position.
## Fix Focus Areas
- components/ui/compare/lane-compare/lane-compare.tsx[93-139]
- components/ui/compare/lane-compare/lane-compare.tsx[327-346]
## Recommended Fix
Make each asynchronous scroll request cancellable or associate it with a monotonically increasing selection token. Before every delayed scroll, verify that its token still represents the current URL selection, and cancel or disconnect observers when a newer request starts.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


11. Reload navigation misses its target ✓ Resolved 🐞 Bug ≡ Correctness
Description
useEffect calls scrollToElement immediately without waiting for loadingLaneDiff to clear, and
the helper returns when diffPaneRef.current is null. When router navigation changes the selection
during a later lane-diff reload, the pane is still replaced by the loading skeleton and the
already-completed initial-scroll effect cannot retry after rendering resumes.
Code

components/ui/compare/lane-compare/lane-compare.tsx[337]

+    if (urlComponentId) scrollToElement(diffPaneRef.current, urlComponentId, urlFile);
Evidence
The new effect attempts scrolling without checking lane-diff readiness. While loading, the component
returns before mounting the element assigned to diffPaneRef, and scrollToElement drops calls
with a null pane; after an earlier successful load, initialScrollDone prevents the existing
loading-aware effect from retrying.

components/ui/compare/lane-compare/lane-compare.tsx[111-113]
components/ui/compare/lane-compare/lane-compare.tsx[322-348]
components/ui/compare/lane-compare/lane-compare.tsx[666-677]
components/ui/compare/lane-compare/lane-compare.tsx[744-755]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
URL-driven selection attempts to scroll immediately, even when the lane diff is loading and the diff pane is not mounted. After a previous initial scroll, nothing retries the requested scroll when the pane returns.
## Fix Focus Areas
- components/ui/compare/lane-compare/lane-compare.tsx[322-348]
- components/ui/compare/lane-compare/lane-compare.tsx[666-677]
## Recommended Fix
Track URL-driven selection as a pending scroll and perform it only after `loadingLaneDiff` is false and the diff pane is mounted. Clear the pending request after scrolling, while preserving the existing mount-time initial-scroll behavior.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Context sources
Review mode: 🧠 Deep: This PR introduces substantial independent logic across URL navigation, component identity/actions, GraphQL compatibility fallbacks, concurrency, and data loading, with multiple plausible subtle defects spanning several code paths.

Grey Divider

Tip of the day
💡 Did you know, you can type 'qodo, fix this' on a finding and the fix lands right on your PR

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

Comment thread components/ui/compare/lane-compare/lane-compare.tsx Outdated
Handing the host a context object meant lane-compare threading identity
down through every panel to a callback that could not use hooks of its own.
The panel already knows which component it is; the host's control just had
no way to ask.

`ComponentActions` is now a component type, and `InlineComponentCompare`
wraps the whole panel — header included — in a `ComponentIdentityProvider`.
A host writes a plain component and calls `useComponentCompareIdentity()`:

    function ReviewControls() {
      const { componentId } = useComponentCompareIdentity() ?? {};
      ...
    }

    <LaneCompare ComponentActions={ReviewControls} />

No callback, no threading, and hooks work inside it. A module-scope
component is also stable by construction, which is what keeps the panel's
React.memo intact — the earlier design had to document that requirement
because a render prop could quietly violate it.

Also fixes the URL-driven scroll (Qodo): a navigation can land while the
lane diff is reloading, when the pane is a skeleton and there is nothing to
scroll to. The target is now recorded and performed once the pane is back,
instead of being dropped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment thread components/ui/compare/lane-compare/lane-compare.tsx Outdated
Comment thread components/ui/compare/lane-compare/lane-compare.tsx Outdated
Comment thread components/ui/compare/lane-compare/lane-compare.tsx Outdated
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit b2d71f7

Three findings from review, all on the navigation effect.

A selection that was set and then cleared while the diff reloaded kept its
pending scroll: the URL no longer named a component, but the retry still
jumped to the one it used to name. Every change now replaces the pending
target, clearing included.

Each scroll waits up to five seconds for a lazily mounted anchor, and
nothing cancelled the previous wait. Two quick navigations left two
observers live, and the older one scrolled the pane back the moment its
anchor appeared. Waits now take an `AbortSignal`: starting a scroll
supersedes the one before it, and aborting tears the observer down rather
than leaving it watching for the rest of the timeout.

Both effects moved into `useUrlSelection`. They were racing each other for
the same reason — the mount-time scroll and a later navigation each started
an uncancellable wait — and as one hook there is a single in-flight
request. It also made the behaviour testable without mounting the whole
compare view, which is the third finding: seven specs covering mount-time
selection, post-mount adoption, deferral while loading, retry when the pane
is not mounted, the cleared-target case, supersession, and unmount. The two
bug specs fail without their fix.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
{...dataAttributes}
<ComponentIdentityProvider
name={name}
componentId={compareId.split('@')[0]}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

2. Removed components lose their identity 🐞 Bug ≡ Correctness

ComponentIdentityProvider derives componentId only from compareId, which lane compare sets to
an empty string for entries with no compare-side component. For removed components, host header
actions therefore receive an empty stable ID and cannot key review controls or discussion links to
the component, even though baseId identifies it.
Agent Prompt
## Issue description
Removed components have no compare-side ID, but the component identity context derives its stable ID exclusively from `compareId`. Host-contributed header controls consequently receive an empty `componentId` for those rows.

## Fix Focus Areas
- components/ui/component-compare/component-compare/component-compare.tsx[674-699]

## Recommended Fix
Derive the versionless component identity from `compareId` when available and fall back to `baseId` for removed components. Reuse that derived value for the identity provider, header, and DOM component anchor so all consumers agree on the row identity.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit d03d1e0

1 similar comment
@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit d03d1e0

`loadingLaneDiff` is optional on the compare context; the hook wanted a
plain boolean. Absent means not loading.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Comment on lines +111 to +112
// eslint-disable-next-line @typescript-eslint/no-use-before-define
signal?.removeEventListener('abort', onAbort);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Changed ui code fails repository lint 📘 Rule violation ≡ Correctness

finish references onAbort before its const declaration, while the adjacent suppression names a
different rule than the configured core check. Running canonical npm run lint therefore reaches
this changed helper and reports no-use-before-define as an error.
Agent Prompt
## Issue description
The new `finish` callback references the later `onAbort` constant, and its suppression targets `@typescript-eslint/no-use-before-define` rather than the configured core `no-use-before-define` rule.

## Fix Focus Areas
- components/ui/compare/lane-compare/lane-compare.tsx[108-115]

## Recommended Fix
Convert `finish` and `onAbort` to function declarations so their mutual references are valid under the configured rule, and remove the ineffective suppression comment.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +69 to +74
// keep the request if the pane is not mounted yet; the next loading change retries it
if (scrollTo({ ...target, componentId: target.componentId }, controller.signal)) {
pending.current = undefined;
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentKey, loading]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

2. Cached diffs can miss deep-link scrolling 🐞 Bug ≡ Correctness

useUrlSelection retains the pending target when scrollTo returns false, but its retry effect
only depends on the URL key and loading. When cached lane-diff data renders the previously absent
pane without changing either value, the stored request is never retried and the requested component
is not scrolled into view.
Agent Prompt
## Issue description
`useUrlSelection` preserves a target when the pane is unavailable, but no dependency tells it when the pane later becomes available. Cached lane-diff updates can render the pane while `loading` remains false, leaving the deep-link scroll pending forever.

## Fix Focus Areas
- components/ui/compare/lane-compare/use-url-selection.ts[59-74]
- components/ui/compare/lane-compare/lane-compare.tsx[343-357]

## Recommended Fix
Pass a pane-availability or diff-content retry token into `useUrlSelection` and include it in the scrolling effect dependencies. Change that token when the real diff pane mounts or its component set becomes available, so a retained target is retried even when the loading state never transitions; add a hook test covering `scrollTo` first returning false and then succeeding after only that retry token changes.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit 6e7582b

bit.cloud serves the bulk `compareComponents` and `apiDiffs` resolvers, but
through a bit whose schema predates the fields, so the type system never
exposes them. Against that host every pair fails, the file registry fills
with empty lists, and the whole compare surface renders blank — no Code
view, no panel content.

`useBulkPagedQuery` now takes an optional single-pair query. When a host
rejects the bulk field it switches to one request per pair, a few at a
time, accumulating results as they land so the surface still fills in
progressively. Both providers supply one: `compareComponent` and `apiDiff`,
each with the same selection set as its bulk counterpart, so consumers
cannot tell the two paths apart.

Detection keys on the GraphQL validation error naming the field. A missing
field is rejected before execution, so retrying can never help and the only
useful response is to stop asking — which is also what makes this safe to
distinguish from the transient failures the paging loop already retries.
The result is cached per host+field for the page's lifetime, so later
mounts skip the rejected request rather than each paying for it.

This is a compatibility path, not a second way of doing things: it costs
exactly what the bulk field was introduced to avoid, it is only ever taken
after a host has refused the bulk field, and it retires itself the moment
that host catches up.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
const [fallbackDone, setFallbackDone] = useState(false);

useEffect(() => {
if (!onFallback || skip) return undefined;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

6. Skipped fallback queries stay loading 🐞 Bug ≡ Correctness

useBulkPagedQuery exits the fallback effect when skip is true, while done still depends
exclusively on fallbackDone. This occurs for empty pair lists and inactive API views on a host
already marked as lacking the bulk field, leaving the hook's documented loading state unsettled
despite no request being eligible to run.
Agent Prompt
## Issue description
Fallback sessions return before setting `fallbackDone` when the query is skipped, but the returned `loading` state still depends on `fallbackDone` and therefore remains true indefinitely.

## Fix Focus Areas
- components/ui/hooks/use-bulk-paged-query/use-bulk-paged-query.ts[178-179]
- components/ui/hooks/use-bulk-paged-query/use-bulk-paged-query.ts[222-222]

## Recommended Fix
Include `skip` in the completion calculation so every skipped session reports `loading: false`, regardless of whether fallback mode is active. Add a fallback-enabled skipped-query test alongside the existing skipped-query coverage.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +199 to +201
} catch {
// a pair that cannot be compared is reported as failed, exactly as the bulk path reports it
item = null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

7. Dropped requests lose component diffs 🐞 Bug ☼ Reliability

The fallback worker catches every client.query rejection and immediately stores null for the
requested pair instead of using the transient retry budget applied by the bulk paging path. A
temporary transport failure on the compatibility path therefore becomes a settled failed comparison
once fallbackDone is set, leaving that component unavailable for the remainder of the mounted
session.
Agent Prompt
## Issue description
The compatibility worker converts the first single-pair request rejection into a permanent `null` result, even though the bulk paging path retries transient request failures. Requested pairs should remain unresolved while retries remain and only settle as unavailable after the retry budget is exhausted.

## Fix Focus Areas
- components/ui/hooks/use-bulk-paged-query/use-bulk-paged-query.ts[187-211]
- components/ui/hooks/use-bulk-paged-query/use-bulk-paged-query.ts[220-222]
- components/ui/hooks/use-bulk-paged-query/use-bulk-paged-query.ts[294-313]
- components/ui/hooks/use-bulk-paged-query/use-bulk-paged-query.spec.tsx[91-103]

## Recommended Fix
Add bounded per-pair retries for rejected fallback queries, using the existing transient retry budget or an equivalent explicit policy. Preserve cancellation checks between attempts, keep unresolved pairs loading while a retry remains, and store `null` only after retries are exhausted. Update the fallback tests to verify recovery from a temporary failure and terminal failure after exhaustion of the retry budget.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

]);

const dataByCompareId = useMemo(() => {
if (onFallback) return fallbackData;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Switching comparisons briefly shows old diffs 🐞 Bug ≡ Correctness

useBulkPagedQuery returns fallbackData and fallbackDone from the preceding fallback session
during render because their reset is deferred to a passive effect. When an unsupported host changes
host or base/compare pairs, overlapping identifiers can expose old comparisons while absent
identifiers resolve to null, reaching component panels and the file registry as settled failures
before current-session requests start.
Agent Prompt
## Issue description
Fallback data and completion state are not associated with their originating session, so a host or pair change can render once using the preceding session's map and completed state. A new fallback session must synchronously expose empty, pending values until its own workers populate data, preventing old results from satisfying new lookups and missing identifiers from being classified as failures.

## Fix Focus Areas
- components/ui/hooks/use-bulk-paged-query/use-bulk-paged-query.ts[175-182]
- components/ui/hooks/use-bulk-paged-query/use-bulk-paged-query.ts[219-222]
- components/ui/hooks/use-bulk-paged-query/use-bulk-paged-query.ts[278-313]

## Recommended Fix
Store the fallback `sessionKey` together with its data and completion state, or otherwise synchronously invalidate the displayed fallback session when the key changes. When the stored key differs from the current `sessionKey`, return an empty map and `done: false` instead of exposing the preceding session, then populate state for the current key from the fallback workers.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +43 to +52
const currentKey = keyOf(selection);
const lastSeenKey = useRef(currentKey);
// seeded so a deep link scrolls once the diff has loaded, without a second effect to race with
const pending = useRef<UrlSelection | undefined>(selection.componentId ? selection : undefined);
const inFlight = useRef<AbortController | undefined>(undefined);

useEffect(() => {
if (currentKey === lastSeenKey.current) return;
lastSeenKey.current = currentKey;
apply(selection);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

2. Back navigation keeps old selection 🐞 Bug ≡ Correctness

useUrlSelection seeds lastSeenKey from the mount-time URL and only calls apply when the
router-observed key differs from that original value. If a user selects a component through local
state and replaceState, then navigates to an unselected URL matching the initially empty key, the
local component and file selections remain set because the hook ignores the navigation.
Agent Prompt
Issue description
A router navigation back to the initial empty URL selection is ignored after local selection changed the URL through `history.replaceState`. The hook's observed key must remain coherent with locally written selection state so real navigation to an empty selection clears both local values.

Fix Focus Areas
- components/ui/compare/lane-compare/use-url-selection.ts[42-57]
- components/ui/compare/lane-compare/lane-compare.tsx[293-331]

Recommended Fix
Synchronize the hook's last-observed selection with locally written selection changes, or pass the current local selection into the hook and compare router selection against it rather than only its mount-time key. Preserve the requirement that `replaceState` itself does not re-apply state, while ensuring a subsequent router navigation with no parameters calls `apply({})`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

Code review by qodo was updated up to the latest commit ea4bee5

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.

1 participant