You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Found by manual testing of the shipped 0.0.1b1 wheel.
What happens
The asset gallery renders one tile per row at every viewport width, so a batch of six 160-px thumbnails occupies a single 160-px column with the rest of the pane empty, inside a 70vh scroll box. Resizing the window does not change it.
The row elements are laid out correctly (flex w-full, 1239 px wide) and each contains exactly one tile.
Reproduction
Ingest any batch, open it at /ui/projects/{projectId}/batches/{batchId}, and read the DOM:
The scroller that scrollRef points at is rendered inside <Async>'s children render-prop (line 101-115), so it does not exist until the query resolves. On mount, <Async> is showing skeleton rows and ref.current is null — the effect takes the early return, and no observer is ever attached. Both dependencies are stable (ref is a RefObject, measure is a useCallback over [ref]), so the effect never runs again once the real element mounts. columns stays 1 for the life of the screen.
The arithmetic itself is correct — Math.floor((1239 + 12) / (160 + 12)) is 7. Only the measurement never happens.
The row virtualiser is unaffected because useVirtualizer takes getScrollElement: () => scrollRef.current as a callback and re-reads it, which is why rows virtualise correctly (total height 1032 = 6 × 172) while columns never recover. That contrast is the tell.
Why the tests did not catch it
useColumns' own docstring (lines 186-193) says the one-column fallback is "correct-but-slow rather than wrong, in an environment with no observer (jsdom, notably)". The unit tests in frontend/ui-core/src/screens/gallery.test.tsx therefore run in exactly the state the bug produces and pass. Nothing asserts a column count, and no Playwright spec covers this screen.
Suggested fix
Replace the useRef + effect with a callback ref, so measurement happens when the node actually attaches and cannot be missed:
const[element,setElement]=useState<HTMLDivElement|null>(null);// pass setElement as ref={...}; observe `element` in an effect keyed on it
useVirtualizer's getScrollElement then returns the same state value. An element-keyed effect re-runs on mount and unmount by construction, which is what the current [ref, measure] deps only appear to do.
Narrowing the window re-flows the grid, proving the observer is attached.
A unit test pins that the fallback is reached only when ResizeObserver is genuinely absent, so the jsdom path stays honest rather than hiding a regression.
Row virtualisation still works: the DOM holds only the visible rows plus overscan.
Scope notes
Frontend only; no Python, no migration, openapi.json untouched.
Found by manual testing of the shipped
0.0.1b1wheel.What happens
The asset gallery renders one tile per row at every viewport width, so a batch of six 160-px thumbnails occupies a single 160-px column with the rest of the pane empty, inside a
70vhscroll box. Resizing the window does not change it.The row elements are laid out correctly (
flex w-full, 1239 px wide) and each contains exactly one tile.Reproduction
Ingest any batch, open it at
/ui/projects/{projectId}/batches/{batchId}, and read the DOM:Measured at two widths: 1600 px viewport → scroller 1280 px → 7 columns expected, 1 rendered. 1200 px viewport → scroller 895 px → 5 expected, 1 rendered.
Root cause
frontend/ui-core/src/screens/GalleryScreen.tsx:194-212.useColumnsinitialisescolumnsto1and attaches itsResizeObserverin an effect:The scroller that
scrollRefpoints at is rendered inside<Async>'s children render-prop (line 101-115), so it does not exist until the query resolves. On mount,<Async>is showing skeleton rows andref.currentisnull— the effect takes the early return, and no observer is ever attached. Both dependencies are stable (refis aRefObject,measureis auseCallbackover[ref]), so the effect never runs again once the real element mounts.columnsstays1for the life of the screen.The arithmetic itself is correct —
Math.floor((1239 + 12) / (160 + 12))is 7. Only the measurement never happens.The row virtualiser is unaffected because
useVirtualizertakesgetScrollElement: () => scrollRef.currentas a callback and re-reads it, which is why rows virtualise correctly (total height 1032 = 6 × 172) while columns never recover. That contrast is the tell.Why the tests did not catch it
useColumns' own docstring (lines 186-193) says the one-column fallback is "correct-but-slow rather than wrong, in an environment with no observer (jsdom, notably)". The unit tests infrontend/ui-core/src/screens/gallery.test.tsxtherefore run in exactly the state the bug produces and pass. Nothing asserts a column count, and no Playwright spec covers this screen.Suggested fix
Replace the
useRef+ effect with a callback ref, so measurement happens when the node actually attaches and cannot be missed:useVirtualizer'sgetScrollElementthen returns the same state value. Anelement-keyed effect re-runs on mount and unmount by construction, which is what the current[ref, measure]deps only appear to do.Acceptance criteria
getBoundingClientRectreturns zeros — the same reason annotator: React adapter — SVG render, zoom/pan, asset image, annotation layer #47 kept its risky logic pure).ResizeObserveris genuinely absent, so the jsdom path stays honest rather than hiding a regression.Scope notes
openapi.jsonuntouched.frontend/app/playwright.config.ts(stood up by annotator: DOM-free core test suite + port of v1's 4 Playwright specs (~825 LOC) as the behavioral contract #48) — do not add a second runner. Notetests/scripts/e2e_discipline.test.mjsgates what those specs may do.