Repository navigation
bug(desktop): "Return to latest" is pulled back by the reading anchor; in-flight history loads drop later requests #4882
Description
What happened
Two defects in Desktop transcript history navigation. Both are user-visible, and together they are the transcript-scroll-cost.spec.ts:239 flake that has been failing Desktop e2e on main (run 33962670601) and on unrelated PR branches since the spec landed in #4796.
1. "Return to latest" is pulled back to the Turn the reader just left.
The tail range does arrive, and is immediately replaced by the head range again. ChatView pins the scroller to the tail only after await props.returnToLatest.onClick() resolves, but the tail batch reaches the store before that promise settles. In that window the scroller is still unpinned, so its reading-anchor report writes the top Turn back, and the restoreRange effect in app-shell.tsx (which re-runs on every messages change) finds that Turn missing from the new range and calls loadAround(anchor). In 60/60 local runs the tail was resident for 1–243 ms before being swapped back. The test only passes when Playwright's poll happens to land inside that window, which a loaded CI runner misses more often.
For a user: scroll far back into history, click 返回最新消息 — the transcript flashes the tail and jumps back to where they were, with the notice still showing.
2. A history load in flight silently drops the requests behind it.
loadTranscriptHistory in app-shell.tsx guards with historyLoadPendingRef and returns when a load is pending, without queueing or retrying. The scroller asks for history on every reader movement, so one wheel gesture fires several earlier requests; when the in-flight one settles, nothing re-asks, and paging stops until the reader moves again. Same shape for latest: clicking the notice while an earlier load is settling is a dead click.
For a user: fast wheel scrolling through old history occasionally stalls one page short.
How to reproduce
Defect 1 reproduces on main 411512b under load:
cd apps/desktop
npx playwright test e2e/transcript-scroll-cost.spec.ts -g "paging back" \
--config e2e/playwright.config.ts --repeat-each=20 --workers=4
# 3 failed / 17 passed locally (macOS); every run swaps tail -> head, the
# passing ones only polled inside the window.
Instrumented timeline of one failing run (renderer console + main-process replica log, ms):
666603 test clicks 返回最新消息
666659 loadTranscriptHistory('latest') starts range hasNewer=true durableThrough=239 oldest=0 newest=19
666676 reading anchor set: turn-prompt-rail-1/0 <- scroller report, unpinned
666735 batch hasNewer=false frags=20 ids 220..239 <- tail resident
666749 restoreRange -> controller.loadAround(0) <- anchor not in range
666761 loadLatest resolves -> ChatView pinToTail -> anchor cleared (too late)
666811 batch hasNewer=true frags=20 ids 0..19 <- back at the head
...30 s later: expect(turn-prompt-rail-120).toHaveCount(1) times out
Defect 2 showed up once the first was fixed (1/60): ten consecutive earlier requests dropped by the guard during one wheel gesture, the in-flight load settled, no further load, expect.poll(turns.first()).not.toBe(...) timed out at transcript-scroll-cost.spec.ts:263.
Environment
- Maka commit:
411512bd9(main, 2026-09-05) - OS: macOS (Darwin 25.5) for the local repro; CI failures on ubuntu runners
- Surface: Desktop
- Node.js: v26.8.1
Logs, screenshots, or additional context
- CI failures: https://github.com/apache/maka/actions/runs/33962670601 (
main), https://github.com/apache/maka/actions/runs/33969566059 - feat(desktop): preserve transcript continuity across bounded ranges #4560 rebuilds this area and clears the anchor in
app-shell.tsxbefore the load; locally that ordering still loses the race (the scroller re-reports the anchor 17 ms after the load starts, see timeline). Pinning before the load is what closes it. - The guard in defect 2 is a useful throttle for
earlier(2979 drops in 20 runs); it only needs to remember the last request, not go away.
Activity
orangeCatDeveloper commentedon Sep 6, 2026
Cross-Session repro for the second half of the title (in-flight history loads dropping later requests), fixed by 76692da on #4883:
loadBeforein flight).pending.isCurrent()— false on B — so the queue is cleared without replaying. B's navigation is gone.A stale A result could also land on B: the replay used A's captured
controller/isCurrent/setPending/onError, not B's.#4883's second commit keys the gate per controller (
WeakMap<object, TranscriptHistoryGate>): each Session's in-flight load and queued request stay to themselves, same-Session queue semantics unchanged. Covered inapps/desktop/src/main/__tests__/app-shell-session-ui-state.test.ts("starts another Session history load without waiting behind an in-flight load elsewhere" and the six sibling cases).