Summary
MessageWindow.load() (static/js/chatHistory.js) renders only the newest WINDOW_SIZE (50) messages and attaches a sentinel, but creates no spacer representing the _startIdx older messages it did not render. The container's scrollHeight therefore reflects ~50 messages, not the conversation.
The scrollbar thumb is consequently huge and the track short: it tells the user the conversation is ~40× shorter than it is. Scroll position is meaningless as a progress indicator, and a drag of the thumb traverses a distance unrelated to the history.
Measured (ground truth vs. reported)
Corpus: the benchmark's deterministic 2000-message mix, 900×700 viewport.
|
container scrollHeight |
fraction of real conversation |
| all 2000 rendered (ground truth) |
334,600 px |
100% |
MessageWindow at load |
8,401 px |
2.5% |
MessageWindow scrolled to the very top |
13,456 px |
4.0% |
Note the second row: even after paging the entire history in and pruning behind itself, the bar never approaches honesty, because pruned messages are only partially accounted.
Cause
load() sets _startIdx = max(0, messages.length - WINDOW_SIZE), calls _renderTail() and _attachSentinel(), then snaps to bottom. Nothing represents messages [0, _startIdx) in the layout. Spacers exist in this file (.chat-history-spacer, created by _pruneTop/_pruneBottom) but only for nodes evicted after being rendered — never for nodes never rendered at all.
Why this matters beyond cosmetics
- The scroll position is the user's only sense of "where am I in this conversation."
- It also distorts any pixel-based scroll logic: a fixed pixel scroll traverses wildly different numbers of messages under
MessageWindow than under a fully-rendered list. This was hit directly while benchmarking (excursions had to be redriven in messages rather than pixels; see docs/dev/chat-history-benchmark.md).
Suggested direction (not prescriptive — read the geometry first)
Give load() a top spacer sized estimated_height * _startIdx, and refine the estimate as real heights are measured during paging. The estimator is the hard part and it is easy to make it drift; a naive per-message average taken from the newest 50 messages is not representative of the corpus. A related drift bug was found in the benchmark's own hybrid arm (#126) — read that before implementing.
Prerequisite: decide whether an approximate-but-nonzero bar is better than today's honestly-tiny one. It is, but the estimator must not make the thumb jump while scrolling.
Repro
/tmp/.../probe_spacer.py in the benchmark harness style:
import sys; sys.path.insert(0,'tests/bench')
from playwright.sync_api import sync_playwright
import chat_history_bench as B
N = 2000
with sync_playwright() as pw:
br = pw.chromium.launch(headless=True)
p = br.new_page(viewport=B.VIEWPORT); p.set_content(B._HARNESS_HTML)
B.load_arm(p, "naive", N)
truth = p.evaluate("()=>document.getElementById('chat-history').scrollHeight")
p2 = br.new_page(viewport=B.VIEWPORT); p2.set_content(B._HARNESS_HTML)
B.load_arm(p2, "evict", N)
got = p2.evaluate("()=>document.getElementById('chat-history').scrollHeight")
print(truth, got, f"{got/truth:.1%}") # 334600 8401 2.5%
br.close()
Provenance
Found incidentally while building the chat-history virtualization benchmark; not a regression, present since MessageWindow landed. Verified against ground truth (a fully-rendered list of the identical corpus) rather than inferred from the benchmark's spacer arithmetic.
Summary
MessageWindow.load()(static/js/chatHistory.js) renders only the newestWINDOW_SIZE(50) messages and attaches a sentinel, but creates no spacer representing the_startIdxolder messages it did not render. The container'sscrollHeighttherefore reflects ~50 messages, not the conversation.The scrollbar thumb is consequently huge and the track short: it tells the user the conversation is ~40× shorter than it is. Scroll position is meaningless as a progress indicator, and a drag of the thumb traverses a distance unrelated to the history.
Measured (ground truth vs. reported)
Corpus: the benchmark's deterministic 2000-message mix, 900×700 viewport.
scrollHeightMessageWindowat loadMessageWindowscrolled to the very topNote the second row: even after paging the entire history in and pruning behind itself, the bar never approaches honesty, because pruned messages are only partially accounted.
Cause
load()sets_startIdx = max(0, messages.length - WINDOW_SIZE), calls_renderTail()and_attachSentinel(), then snaps to bottom. Nothing represents messages[0, _startIdx)in the layout. Spacers exist in this file (.chat-history-spacer, created by_pruneTop/_pruneBottom) but only for nodes evicted after being rendered — never for nodes never rendered at all.Why this matters beyond cosmetics
MessageWindowthan under a fully-rendered list. This was hit directly while benchmarking (excursions had to be redriven in messages rather than pixels; seedocs/dev/chat-history-benchmark.md).Suggested direction (not prescriptive — read the geometry first)
Give
load()a top spacer sizedestimated_height * _startIdx, and refine the estimate as real heights are measured during paging. The estimator is the hard part and it is easy to make it drift; a naive per-message average taken from the newest 50 messages is not representative of the corpus. A related drift bug was found in the benchmark's own hybrid arm (#126) — read that before implementing.Prerequisite: decide whether an approximate-but-nonzero bar is better than today's honestly-tiny one. It is, but the estimator must not make the thumb jump while scrolling.
Repro
/tmp/.../probe_spacer.pyin the benchmark harness style:Provenance
Found incidentally while building the chat-history virtualization benchmark; not a regression, present since
MessageWindowlanded. Verified against ground truth (a fully-rendered list of the identical corpus) rather than inferred from the benchmark's spacer arithmetic.