Skip to content

feat: virtualize session sidebar list#1669

Closed
Michaelyklam wants to merge 1 commit intonesquena:masterfrom
Michaelyklam:fix/issue-500-session-list-virtualization
Closed

feat: virtualize session sidebar list#1669
Michaelyklam wants to merge 1 commit intonesquena:masterfrom
Michaelyklam:fix/issue-500-session-list-virtualization

Conversation

@Michaelyklam
Copy link
Copy Markdown
Contributor

Thinking Path

What Changed

  • Added a no-dependency virtual window helper for the session sidebar, with a default row-height estimate, row buffer, and threshold before virtualization turns on.
  • renderSessionListFromCache() now builds the fresh filtered/grouped session set, renders only the current visible row window plus buffer, and inserts aria-hidden spacers inside date groups to preserve scroll height.
  • Preserved active-session anchoring when a session becomes active or the filter changes, while keeping normal user scroll position stable on subsequent renders.
  • Preserved batch select behavior by selecting from the full currently visible filtered sidebar id set, not just the rows currently mounted in the DOM.
  • Added regression coverage for large list windowing, active-row anchoring, scroll rerenders, filter invalidation, and fresh render slices.
  • Added synthetic browser QA media: Synthetic 1000-session sidebar virtualization

Refs #500.

Why It Matters

  • Power users with hundreds or thousands of conversations no longer pay the DOM cost of mounting every sidebar row at once.
  • The change keeps search, pin/date grouping, active selection, streaming indicators, batch selection, and mobile-safe row click behavior on the existing code path.
  • The approach stays dependency-free and keeps the message-list virtualization problem separate from this smaller sidebar MVP.

Verification

  • node --check static/sessions.js
  • /home/michael/.hermes/hermes-agent/venv/bin/python -m pytest tests/test_issue500_session_list_virtualization.py tests/test_session_lineage_collapse.py tests/test_sidebar_first_turn_visibility.py tests/test_session_batch_select.py tests/test_login_locale.py tests/test_media_inline.py -q → 73 passed
  • git diff --check
  • Browser QA on isolated local WebUI server with 1000 synthetic sessions:
    • initial active session anchor rendered 34 .session-item rows for 1000 sessions; active Synthetic session 450 visible; spinner/streaming state preserved
    • manual scroll near row 800 rendered a new 34-row window around Synthetic session 788
    • search for session 99 narrowed to 11 matching rows with no stale virtual window
    • clearing search re-anchored the active session row
    • screenshot attached above
  • Full isolated pytest tests/ -q was attempted twice. Both runs progressed through the suite but failed in pre-existing/integration test-server/session-state cases unrelated to this static sidebar change (examples: 127.0.0.1:24158 connection refused in test_issue1144_session_time_sync.py, and several old session persistence tests returning 404). The directly failing integration files reran cleanly in isolation on a fresh port: tests/test_login_locale.py tests/test_media_inline.py → 42 passed.

Risks / Follow-ups

  • The sidebar uses an estimated 52px row height, which is appropriate for the existing compact/detailed fixed-ish rows but is still an approximation.
  • This intentionally does not virtualize the message transcript; issue feat(perf): DOM windowing / message virtualization for long sessions #734 should handle variable-height markdown/message windowing separately.
  • Large numbers of date-group headers still render, but the expensive per-session row count is bounded.

Model Used

  • OpenAI Codex CLI provider, gpt-5.5
  • Tool use: shell/git/pytest/node, browser QA with synthetic local state, screenshot evidence capture

@nesquena-hermes
Copy link
Copy Markdown
Collaborator

Initial review — session sidebar virtualization (issue #500 first slice)

Pulled the branch into an isolated worktree against origin/master (v0.51.0) and ran the regression suite. tests/test_issue500_session_list_virtualization.py — 3 passed in 2.35s.

What I checked:

  • Window math (_sessionVirtualWindow) — clamps start to [0, total - visibleRows], computes top/bottom pad as start*itemHeight and (total-end)*itemHeight, and falls back to non-virtualized rendering when total <= threshold (80). The activeIndex re-anchor branch correctly snaps start so the active row is in-window when a new session is selected.
  • Render integration (renderSessionListFromCache) — flattens visible groups into flatSessionRows, then per-group: rows outside the window become groupTopPad / groupBottomPad accumulators that emit a single _sessionVirtualSpacer div per side, so date-group headers stay in real DOM and click handlers (_groupCollapsed, expansion toggles) survive.
  • Scroll preservationlistScrollTopBeforeRender is captured before innerHTML='' and re-applied after the window paints; the dataset.sessionVirtualActiveAnchor guard avoids fighting the active-row anchor.
  • Select-all behaviorselectAllSessions() was correctly updated to source IDs from _sessionVisibleSidebarIds (the flattened cache) rather than only the currently-rendered checkboxes, so virtualized rows still get included.

Looks well-bounded and matches the conservative scope you outlined (sidebar only, message-list virtualization deferred to #734). Queued for full maintainer review.

@nesquena-hermes
Copy link
Copy Markdown
Collaborator

Closed by the v0.51.1 release in PR #1681 (merged at e23ba59). Massive thanks @Michaelyklam — this is now 19 merged PRs across the v0.50.292–v0.51.1 release window, an extraordinary contribution rate. Each PR was per-claim-vs-diff verified against your description and every security-relevant code path checked under independent review (Opus advisor, 6/6 questions clean). Your closes #N references are all accurate, your Thinking Path / What Changed / Why It Matters body template is consistently helpful, and your test coverage is solid behavioral scope (not source-string scaffolding) on every PR.

Live on production: https://github.com/nesquena/hermes-webui/releases/tag/v0.51.1

🚀

nesquena-hermes pushed a commit to Michaelyklam/hermes-webui that referenced this pull request May 5, 2026
nesquena-hermes pushed a commit to Michaelyklam/hermes-webui that referenced this pull request May 5, 2026
…esquena#1669 follow-up

PR nesquena#1669 added DOM virtualization to renderSessionListFromCache() with two issues
for lists below the virtualization threshold (≤80 rows):

1. The unconditional scroll listener triggered renderSessionListFromCache() on
   every rAF, rebuilding the entire list DOM on every scroll event.
2. After each rebuild, scrollTop was only restored when virtualWindow.virtualized
   was true (i.e. total > 80). For lists ≤ 80 rows, scrollTop dropped to 0 on
   every scroll event, producing a 'scroll keeps jumping back' feel.

Fix:
- Always restore scrollTop after re-render when listScrollTopBeforeRender > 0
  (regardless of virtualized flag).
- Short-circuit _scheduleSessionVirtualizedRender when total <=
  SESSION_VIRTUAL_THRESHOLD_ROWS (saves wasteful rebuild on small lists).

Live verified on a 56-session sidebar: scrollTop holds across animation frames.
3 regression tests pin the fix shape.
nesquena-hermes pushed a commit to Michaelyklam/hermes-webui that referenced this pull request May 5, 2026
… hotfix

CHANGELOG.md: full v0.51.2 entry covering 3 PRs + sidebar scroll hotfix
ROADMAP.md: bump version + test count to 4457
TESTING.md: bump version + test count to 4457

Independent review: Opus advisor on stage-299 diff (1336 LOC).
6/6 verification questions verified clean. Verdict: SHIP.
0 MUST-FIX, 2 SHOULD-FIX absorbed in-release (bounded WIKI walk +
URL scheme guard).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants