Add follow head toggle to fork choice viz#344
Conversation
Greptile SummaryThis PR adds a "Follow Head" toggle to the fork choice visualization, gating auto-scroll behind a boolean and detecting user scroll intent through wheel, pointer, touch, and keyboard events. The implementation is well-structured and the bounds-clamped scroll-target calculation is an improvement over the original. All findings are P2 (the heuristic timeout for Confidence Score: 4/5Safe to merge; all findings are non-blocking P2 quality/robustness suggestions. Only P2 issues found — the heuristic timeout and incomplete key list are edge-case robustness concerns, not correctness bugs. Core fork-choice fetch/render logic is unchanged. No files require special attention.
|
| Filename | Overview |
|---|---|
| crates/net/rpc/static/fork_choice.html | Adds a "Follow Head" toggle with scroll-intent detection via wheel/pointer/touch/keyboard events; logic is solid with minor P2 issues around the heuristic isProgrammaticScroll timeout, incomplete keydown coverage, and a missing passive hint on pointerdown. |
Flowchart
%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Poll: fetchAndRender] --> B[render data]
B --> C{headNode found?}
C -- Yes --> D[Compute currentHeadScrollTarget]
C -- No --> E[currentHeadScrollTarget = null]
D --> F{followHead?}
F -- Yes --> G[scrollToHead + markProgrammaticScroll]
F -- No --> H[No scroll]
I[User scroll event] --> J{isProgrammaticScroll?}
J -- Yes --> K[Update lastScrollTop only]
J -- No --> L{scrollingUp AND not isAtHead?}
L -- Yes --> M[setFollowHead false]
L -- No --> N{not followHead AND isAtHead?}
N -- Yes --> O[setFollowHead true]
N -- No --> P[No state change]
Q[Toggle button click] --> R[setFollowHead toggle + scrollToHead if enabling]
S[wheel / pointerdown / touchstart / keydown up] --> T[markUserScrollIntent: isProgrammaticScroll=false]
Prompt To Fix All With AI
Fix the following 3 code review issues. Work through them one at a time, proposing concise fixes.
---
### Issue 1 of 3
crates/net/rpc/static/fork_choice.html:399-403
**Heuristic timeout may misclassify browser scroll events**
`isProgrammaticScroll` is reset after `TRANSITION_DURATION + 150` ms (650 ms), but CSS smooth-scroll duration is browser-controlled and not guaranteed to match `TRANSITION_DURATION`. On slow devices or with large scroll distances, the animation can easily exceed 650 ms, causing the tail-end scroll events to be treated as user-initiated. If those events happen to reduce `scrollTop` (e.g. scroll target is above current position), `setFollowHead(false)` could fire spuriously while the programmatic scroll is still completing.
### Issue 2 of 3
crates/net/rpc/static/fork_choice.html:654-658
**Downward scroll keys not covered by `markUserScrollIntent`**
`ArrowDown`, `PageDown`, `End`, and `Space` all scroll the container but are absent from the keydown handler. While downward keys won't trigger the `scrollingUp` check (so follow-head won't be wrongly disabled), the gap becomes a problem if a programmatic scroll's 650 ms timeout has already expired while the animation is still running: a keyboard-down key press won't reset `isProgrammaticScroll`, so the flag stays false and the concurrent scroll event is treated as user intent. Adding all scroll-related keys makes the guard complete.
```suggestion
window.addEventListener("keydown", event => {
if (["ArrowUp", "ArrowDown", "PageUp", "PageDown", "Home", "End", " "].includes(event.key)) {
markUserScrollIntent();
}
});
```
### Issue 3 of 3
crates/net/rpc/static/fork_choice.html:652
**`pointerdown` listener missing `{ passive: true }`**
The `wheel` and `touchstart` handlers are correctly marked passive, but `pointerdown` is not. Since `markUserScrollIntent` never calls `preventDefault()`, the listener has no reason to be non-passive, and omitting the hint forces the browser to wait for the handler to return before it can process touch/pointer scroll, causing avoidable input latency on mobile or stylus input.
```suggestion
container.addEventListener("pointerdown", markUserScrollIntent, { passive: true });
```
Reviews (1): Last reviewed commit: "Add follow head toggle to fork choice vi..." | Re-trigger Greptile
| programmaticScrollTimeout = setTimeout(() => { | ||
| isProgrammaticScroll = false; | ||
| }, TRANSITION_DURATION + 150); | ||
| } | ||
|
|
There was a problem hiding this comment.
Heuristic timeout may misclassify browser scroll events
isProgrammaticScroll is reset after TRANSITION_DURATION + 150 ms (650 ms), but CSS smooth-scroll duration is browser-controlled and not guaranteed to match TRANSITION_DURATION. On slow devices or with large scroll distances, the animation can easily exceed 650 ms, causing the tail-end scroll events to be treated as user-initiated. If those events happen to reduce scrollTop (e.g. scroll target is above current position), setFollowHead(false) could fire spuriously while the programmatic scroll is still completing.
Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/net/rpc/static/fork_choice.html
Line: 399-403
Comment:
**Heuristic timeout may misclassify browser scroll events**
`isProgrammaticScroll` is reset after `TRANSITION_DURATION + 150` ms (650 ms), but CSS smooth-scroll duration is browser-controlled and not guaranteed to match `TRANSITION_DURATION`. On slow devices or with large scroll distances, the animation can easily exceed 650 ms, causing the tail-end scroll events to be treated as user-initiated. If those events happen to reduce `scrollTop` (e.g. scroll target is above current position), `setFollowHead(false)` could fire spuriously while the programmatic scroll is still completing.
How can I resolve this? If you propose a fix, please make it concise.Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
|
Great work! 🔥 |
🗒️ Description / Motivation
What Changed
crates/net/rpc/static/fork_choice.htmlCorrectness / Behavior Guarantees
Tests Added / Run
git diff --checknode -e "const fs=require('fs'); const html=fs.readFileSync('crates/net/rpc/static/fork_choice.html','utf8'); const script=html.match(/<script>([\\s\\S]*)<\\/script>/)[1]; new Function(script); console.log('script syntax ok');"Related Issues / PRs