Skip to content

perf(web): virtualize long lists behind a shared windowing primitive - #51

Merged
aryasaatvik merged 7 commits into
devfrom
feat/react-virtual-list
Jun 21, 2026
Merged

aryasaatvik merged 7 commits into
devfrom
feat/react-virtual-list

Conversation

@aryasaatvik

Copy link
Copy Markdown
Owner

Summary

Introduces a shared windowed-list primitive (@executor-js/react/components/virtual-list) and adopts it on the web-UI surfaces that mount unbounded or long lists, so they keep a bounded DOM instead of rendering every row at once.

This is M1 of a 2-PR stack. The follow-up adds server-side cursor pagination to tools.list — the one list whose API still returns the whole set — and stacks on top of this branch.

What's here (one commit per surface)

  • VirtualList primitive — @tanstack/react-virtual with dynamic measurement, an onEndReached infinite-scroll trigger (generalized from the runs shell's hand-rolled distance-from-bottom check), optional sticky section headers, and scroll restoration keyed by id. A useWindowedRows hook covers surfaces that can't use <div> rows (real <table>s).
  • Tool source-list (worst case) — a broad source could mount thousands of tool rows at once, each carrying a dropdown-menu portal. The grouped account sections and dotted-name trees are flattened into one windowed list; section headers become sticky virtual items; only near-viewport rows mount.
  • Runs table — windowed via useWindowedRows; the real <table> + sticky <thead> stay, with top/bottom spacer <tr>s for off-screen rows. The two variable-width columns are already capped, so windowing a subset doesn't make table-layout: auto jitter.
  • Run-detail drawer — the logs and tool-calls tabs get their own windowed scroll containers; tool-call expanded state is lifted so it survives a row scrolling out of the window and back.
  • Sticky-header range logic extracted to a pure function with unit tests.

Deliberately not virtualized

The integrations catalog, the service-tokens table, and the org-members list are page-scrolled lists that hold dozens of rows in practice. Virtualizing them would mean introducing an independent bounded scroller — a UX change for negligible benefit. The primitive is ready if any ever grows unbounded.

Gates

  • typecheck: 43/43 packages
  • tests: shared React package 162, execution-history 19 (including new windowing-logic unit tests)
  • oxfmt + oxlint --deny-warnings clean on all changed files

Verification still to do (live browser)

Unit tests cover the windowing math; the visual behaviour needs a real browser (the e2e browser surface is cloud-only). Worth exercising by hand:

  • Sticky section headers pin and swap correctly while scrolling a grouped tool list.
  • Runs-table column widths stay stable while scrolling deep into the keyset list.
  • Detail-drawer logs / tool-calls scroll and measure correctly; expanded tool calls keep their state across scroll.

Shared windowed-list primitive for the web UI virtualization effort:
@tanstack/react-virtual with dynamic measurement, an onEndReached
infinite-scroll trigger generalized from the runs shell's hand-rolled
distance-from-bottom check, optional sticky section headers, and net-new
scroll restoration keyed by id. Pure helpers (isNearBottom + the
scroll-position store) are unit-tested; the component is auto-exported via
the existing ./components/* glob.
The tool tree mounted every row at once — opening a broad source (~3,150
tools for Cloudflare) put thousands of rows, each with a Radix dropdown
portal, in the DOM. Flatten the per-account sections and their dotted-name
trees into a single item list and render it through VirtualList, so only
near-viewport rows mount. Section headers become sticky virtual items;
expand/collapse state is lifted to ToolTree (keyed by section+path) so
sections stay independent; selection scroll uses the virtualizer's
scrollToIndex instead of scrollIntoView on an unmounted row.
The runs table mounted every accumulated row; deep keyset scrolling grew the
DOM unbounded (each row carries a per-row actor-label atom subscription).
Add a useWindowedRows hook to the shared primitive — windowing for surfaces
that can't use <div> rows — and adopt it in RunsShell: the real <table> and
sticky <thead> stay, but only near-viewport rows mount, bracketed by top/
bottom spacer <tr>s. The actor and code columns are already width-capped, so
windowing a subset doesn't make table-layout:auto columns jitter. RunsPage
now hands the shell keyed row descriptors (off-screen runs are cheap element
descriptors that never run their per-row hooks) plus the loading/error status
row and the load-more retry row. Scroll position is preserved across drawer
open/close via scrollRestoreId.
A verbose or agentic run could mount hundreds of log lines or tool-call rows
in the detail drawer at once. The single shared ScrollArea wrapped all three
tabs; since Radix unmounts inactive tabs, give the two pure-list tabs their
own VirtualList scroll containers and keep Properties (mixed content) in a
ScrollArea. ToolCallItem's expanded state is lifted into ToolCallsTab so a row
scrolled out of the window and back keeps its expansion. The interactions list
stays inline on Properties — it's small in practice and embedded in mixed
content, so windowing it isn't worth the complexity.
Pull the "which sticky header is pinned" computation out of VirtualList's
range extractor into a pure resolveActiveStickyIndex(stickyIndices, startIndex)
and unit-test it — the trickiest part of the windowing (it decides which
section header stays mounted and pinned as the user scrolls between sections)
now has direct coverage independent of the DOM-bound virtualizer.
@socket-security

socket-security Bot commented Jun 21, 2026 •

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Added@​tanstack/​react-virtual@​3.14.31001006995100

View full report

@greptile-apps

greptile-apps Bot commented Jun 21, 2026 •

Copy link
Copy Markdown

Greptile Summary

Introduces a shared VirtualList component and useWindowedRows hook (both backed by @tanstack/react-virtual) and adopts them across the three surfaces that mount unbounded or large lists: the tool source list, the runs table, and the run-detail drawer tabs.

  • VirtualList supports dynamic row measurement, optional sticky section headers (via a rangeExtractor + activeStickyRef pattern), an onEndReached infinite-scroll trigger, and scroll restoration keyed by id; useWindowedRows exposes the same engine for <table> contexts where spacer <tr> rows replace absolute positioning.
  • ToolTree flattens per-account sections and dotted-name rows into one windowed list; expanded state is lifted to the parent so section groups stay independent across the window.
  • RunsShell / RunsPage replaces mounted <RunListRow> nodes with RunsShellRow descriptors; only near-viewport rows mount their hooks and portals.
  • Detail drawer splits logs and tool-calls into their own VirtualList scroll containers; expanded state for tool-call items is lifted so it survives rows scrolling out of the window and back.

Confidence Score: 5/5

Safe to merge. The virtualization logic is structurally correct and all three adoption sites behave correctly under normal use. The one subtle edge — scroll restoration firing the onEndReached callback synchronously on remount — only manifests when atom state persists across navigation and the saved offset lands near the estimated bottom, and is guarded by the canLoadMore flag in most cases.

The change is entirely additive — a new primitive plus adoption on three surfaces. Existing behavior (infinite scroll, sticky headers, empty/loading states, expanded tool-call state) is preserved or improved. The pure scroll-math helpers are unit-tested. The scroll-restoration-triggers-loadMore concern is conditional and low-impact; it does not corrupt data or break navigation.

packages/react/src/components/virtual-list.tsx — the useLayoutEffect scroll restoration path and its mirror in useWindowedRows are worth a second look for the synchronous onEndReached trigger described in the inline comments.

Important Files Changed

Filename Overview
packages/react/src/components/virtual-list.tsx New windowing primitive with dynamic measurement, sticky headers, scroll restoration, and an end-reached trigger. Core logic is sound; the scroll-restoration path fires a synchronous DOM scroll event that can spuriously invoke onEndReached on remount.
packages/react/src/components/virtual-list.test.ts Unit tests for isNearBottom, scroll-position store, and resolveActiveStickyIndex. Coverage of the pure helpers is thorough; tests are accurate and pass cleanly.
packages/react/src/components/tool-tree.tsx ToolTreeBody removed; per-account sections and dotted-name rows flattened into a single windowed list. renderItem, getKey, estimateSize, and isSticky all wrapped in useCallback; scrollToIndex replaces the unreliable scrollIntoView for off-screen selected tools.
packages/plugins/execution-history/src/react/shell.tsx Migrated to useWindowedRows; old manual onScroll distance check replaced; spacer <tr> rows bracket the windowed subset correctly. canLoadMore guard properly suppresses end-reached during loading/empty states.
packages/plugins/execution-history/src/react/RunsPage.tsx Rows converted from mounted ReactNodes to RunsShellRow descriptors; loading/error/retry split into statusRow and loadMoreError props. isEmpty guard correctly excludes loading/error states, maintaining pre-existing behavior.
packages/plugins/execution-history/src/react/detail-drawer.tsx Logs and tool-calls tabs switched to VirtualList; expanded state lifted to ToolCallsTab and toggle wrapped in useCallback. Tab content restructured to flex columns so each VirtualList fills the available height correctly.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    subgraph VirtualList["VirtualList (div rows)"]
        VL_virtualizer["useVirtualizer\n(dynamic measure)"]
        VL_range["rangeExtractor\n→ activeStickyRef"]
        VL_render["render virtualItems\n(sticky or absolute)"]
        VL_end["handleScroll\n→ isNearBottom → onEndReached"]
        VL_restore["useLayoutEffect\n← readScrollPosition\n→ el.scrollTop = saved"]
    end

    subgraph useWindowedRows["useWindowedRows (table rows)"]
        WR_virtualizer["useVirtualizer\n(fixed estimateSize)"]
        WR_spacers["paddingTop / paddingBottom\n→ spacer tr rows"]
        WR_end["onScroll\n→ isNearBottom → onEndReached"]
        WR_restore["useLayoutEffect\n← readScrollPosition\n→ el.scrollTop = saved"]
    end

    subgraph scrollStore["Module-level scroll store"]
        SS["Map id→scrollTop\nsaveScrollPosition\nreadScrollPosition\nclearScrollPosition"]
    end

    ToolTree["ToolTree\n(grouped tool list)"] -->|items + isSticky| VL_virtualizer
    DetailDrawer["DetailDrawer\n(logs / tool-calls)"] -->|items| VL_virtualizer
    RunsShell["RunsShell\n(runs table)"] -->|count + onEndReached| WR_virtualizer

    VL_restore <-->|id key| SS
    WR_restore <-->|id key| SS

    VL_virtualizer --> VL_range
    VL_range --> VL_render
    VL_render --> VL_end

    WR_virtualizer --> WR_spacers
    WR_spacers --> WR_end
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
    subgraph VirtualList["VirtualList (div rows)"]
        VL_virtualizer["useVirtualizer\n(dynamic measure)"]
        VL_range["rangeExtractor\n→ activeStickyRef"]
        VL_render["render virtualItems\n(sticky or absolute)"]
        VL_end["handleScroll\n→ isNearBottom → onEndReached"]
        VL_restore["useLayoutEffect\n← readScrollPosition\n→ el.scrollTop = saved"]
    end

    subgraph useWindowedRows["useWindowedRows (table rows)"]
        WR_virtualizer["useVirtualizer\n(fixed estimateSize)"]
        WR_spacers["paddingTop / paddingBottom\n→ spacer tr rows"]
        WR_end["onScroll\n→ isNearBottom → onEndReached"]
        WR_restore["useLayoutEffect\n← readScrollPosition\n→ el.scrollTop = saved"]
    end

    subgraph scrollStore["Module-level scroll store"]
        SS["Map id→scrollTop\nsaveScrollPosition\nreadScrollPosition\nclearScrollPosition"]
    end

    ToolTree["ToolTree\n(grouped tool list)"] -->|items + isSticky| VL_virtualizer
    DetailDrawer["DetailDrawer\n(logs / tool-calls)"] -->|items| VL_virtualizer
    RunsShell["RunsShell\n(runs table)"] -->|count + onEndReached| WR_virtualizer

    VL_restore <-->|id key| SS
    WR_restore <-->|id key| SS

    VL_virtualizer --> VL_range
    VL_range --> VL_render
    VL_render --> VL_end

    WR_virtualizer --> WR_spacers
    WR_spacers --> WR_end
Loading

Reviews (3): Last reviewed commit: "fix(react): key VirtualList's measuremen..." | Re-trigger Greptile

Comment thread packages/plugins/execution-history/src/react/detail-drawer.tsx Outdated
Wrap ToolCallsTab's toggle in useCallback (empty deps; it only calls the
stable setExpanded setter), matching toggleGroup in ToolTree and keeping the
windowed renderItem closure stable if ToolCallItem is ever memoized.
Pass getItemKey to the virtualizer so dynamically-measured row heights are
cached by stable item key, not by index. VirtualList re-flattens its items on
tool-tree expand/collapse/filter; with the default index keying, a reorder
left the prior row's measured height cached at that index until the
ResizeObserver re-measured. Matches TanStack Virtual's guidance for reordering
dynamic lists.
@aryasaatvik
aryasaatvik merged commit 4bb027a into dev Jun 21, 2026
8 checks passed
@aryasaatvik
aryasaatvik deleted the feat/react-virtual-list branch June 21, 2026 22:02
aryasaatvik added a commit that referenced this pull request Jun 23, 2026
…51)

## Summary

Introduces a shared windowed-list primitive
(`@executor-js/react/components/virtual-list`) and adopts it on the
web-UI surfaces that mount unbounded or long lists, so they keep a
bounded DOM instead of rendering every row at once.

This is **M1 of a 2-PR stack**. The follow-up adds server-side cursor
pagination to `tools.list` — the one list whose API still returns the
whole set — and stacks on top of this branch.

## What's here (one commit per surface)

- **VirtualList primitive** — `@tanstack/react-virtual` with dynamic
measurement, an `onEndReached` infinite-scroll trigger (generalized from
the runs shell's hand-rolled distance-from-bottom check), optional
sticky section headers, and scroll restoration keyed by id. A
`useWindowedRows` hook covers surfaces that can't use `<div>` rows (real
`<table>`s).
- **Tool source-list** (worst case) — a broad source could mount
thousands of tool rows at once, each carrying a dropdown-menu portal.
The grouped account sections and dotted-name trees are flattened into
one windowed list; section headers become sticky virtual items; only
near-viewport rows mount.
- **Runs table** — windowed via `useWindowedRows`; the real `<table>` +
sticky `<thead>` stay, with top/bottom spacer `<tr>`s for off-screen
rows. The two variable-width columns are already capped, so windowing a
subset doesn't make `table-layout: auto` jitter.
- **Run-detail drawer** — the logs and tool-calls tabs get their own
windowed scroll containers; tool-call expanded state is lifted so it
survives a row scrolling out of the window and back.
- **Sticky-header range logic** extracted to a pure function with unit
tests.

## Deliberately not virtualized

The integrations catalog, the service-tokens table, and the org-members
list are **page-scrolled** lists that hold dozens of rows in practice.
Virtualizing them would mean introducing an independent bounded scroller
— a UX change for negligible benefit. The primitive is ready if any ever
grows unbounded.

## Gates

- `typecheck`: 43/43 packages
- tests: shared React package 162, execution-history 19 (including new
windowing-logic unit tests)
- `oxfmt` + `oxlint --deny-warnings` clean on all changed files

## Verification still to do (live browser)

Unit tests cover the windowing math; the visual behaviour needs a real
browser (the e2e browser surface is cloud-only). Worth exercising by
hand:
- Sticky section headers pin and swap correctly while scrolling a
grouped tool list.
- Runs-table column widths stay stable while scrolling deep into the
keyset list.
- Detail-drawer logs / tool-calls scroll and measure correctly; expanded
tool calls keep their state across scroll.
aryasaatvik added a commit that referenced this pull request Jun 23, 2026
…51)

## Summary

Introduces a shared windowed-list primitive
(`@executor-js/react/components/virtual-list`) and adopts it on the
web-UI surfaces that mount unbounded or long lists, so they keep a
bounded DOM instead of rendering every row at once.

This is **M1 of a 2-PR stack**. The follow-up adds server-side cursor
pagination to `tools.list` — the one list whose API still returns the
whole set — and stacks on top of this branch.

## What's here (one commit per surface)

- **VirtualList primitive** — `@tanstack/react-virtual` with dynamic
measurement, an `onEndReached` infinite-scroll trigger (generalized from
the runs shell's hand-rolled distance-from-bottom check), optional
sticky section headers, and scroll restoration keyed by id. A
`useWindowedRows` hook covers surfaces that can't use `<div>` rows (real
`<table>`s).
- **Tool source-list** (worst case) — a broad source could mount
thousands of tool rows at once, each carrying a dropdown-menu portal.
The grouped account sections and dotted-name trees are flattened into
one windowed list; section headers become sticky virtual items; only
near-viewport rows mount.
- **Runs table** — windowed via `useWindowedRows`; the real `<table>` +
sticky `<thead>` stay, with top/bottom spacer `<tr>`s for off-screen
rows. The two variable-width columns are already capped, so windowing a
subset doesn't make `table-layout: auto` jitter.
- **Run-detail drawer** — the logs and tool-calls tabs get their own
windowed scroll containers; tool-call expanded state is lifted so it
survives a row scrolling out of the window and back.
- **Sticky-header range logic** extracted to a pure function with unit
tests.

## Deliberately not virtualized

The integrations catalog, the service-tokens table, and the org-members
list are **page-scrolled** lists that hold dozens of rows in practice.
Virtualizing them would mean introducing an independent bounded scroller
— a UX change for negligible benefit. The primitive is ready if any ever
grows unbounded.

## Gates

- `typecheck`: 43/43 packages
- tests: shared React package 162, execution-history 19 (including new
windowing-logic unit tests)
- `oxfmt` + `oxlint --deny-warnings` clean on all changed files

## Verification still to do (live browser)

Unit tests cover the windowing math; the visual behaviour needs a real
browser (the e2e browser surface is cloud-only). Worth exercising by
hand:
- Sticky section headers pin and swap correctly while scrolling a
grouped tool list.
- Runs-table column widths stay stable while scrolling deep into the
keyset list.
- Detail-drawer logs / tool-calls scroll and measure correctly; expanded
tool calls keep their state across scroll.
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