Skip to content

fix: prevent sticky sidebar hover drag state#1730

Merged
1 commit merged intonesquena:masterfrom
Michaelyklam:fix/sidebar-hover-drag-state
May 6, 2026
Merged

fix: prevent sticky sidebar hover drag state#1730
1 commit merged intonesquena:masterfrom
Michaelyklam:fix/sidebar-hover-drag-state

Conversation

@Michaelyklam
Copy link
Copy Markdown
Contributor

Thinking Path

  • The sidebar rows already use pointer movement to distinguish click/tap from drag/scroll gestures.
  • Plain desktop hover also emits pointermove, so the previous implementation could mark a row as .dragging before any press started.
  • Once several rows had stale drag styling, they appeared to remain lit/faded until the next sidebar rerender cleared them all at once.
  • The fix is to make drag detection owned by an active pointer press, not by ambient hover movement.

What Changed

  • Added _pointerActive tracking to sidebar session rows.
  • Only allow pointermove to add .dragging after a valid pointerdown starts on that row.
  • Clear pointer/drag state on pointerup, pointercancel, and pointerleave.
  • Added regression coverage so plain hover movement cannot reintroduce the drag-state path.
  • Updated one Node-based source test helper to write temporary .cjs scripts instead of passing the full sessions.js source through node -e, which now exceeds the shell argument limit during the full suite.

Why It Matters

  • Chat cards now highlight only while actually hovered and return to normal immediately when hover leaves.
  • Drag/scroll gesture suppression still works when the user actually presses and moves a row.
  • The full local suite remains runnable as sessions.js grows.

Verification

  • browser_console synthetic QA on an isolated WebUI server:
    • plain PointerEvent('pointermove') on a session row: hoverDrag: false
    • pointerdown + movement on the same row: pressDrag: true
    • source guard present: sourceHasGuard: true
  • Browser screenshot evidence:
    Sidebar hover QA
  • /home/michael/.hermes/hermes-agent/venv/bin/python -m pytest tests/test_issue856_pinned_indicator_layout.py tests/test_issue500_session_list_virtualization.py -q12 passed
  • env -u HERMES_CONFIG_PATH -u HERMES_WEBUI_HOST /home/michael/.hermes/hermes-agent/venv/bin/python -m pytest tests/ -q4551 passed, 2 skipped, 3 xpassed, 1 warning, 8 subtests passed
  • git diff --check → clean

Risks / Follow-ups

  • Low risk: the change is scoped to row-level pointer gesture bookkeeping.
  • The only behavioral tradeoff is that leaving a row while pressed now clears the local drag state, which prevents stale drag styling and should match user expectations.

Model Used

  • OpenAI Codex / gpt-5.5
  • Tool use: file edits, pytest, browser console QA, screenshot capture, git/GitHub CLI

@nesquena-hermes
Copy link
Copy Markdown
Collaborator

Summary

Pulled the diff for this branch (fix/sidebar-hover-drag-state) and walked the four files. The pointer-state fix is correct and surgically scoped: on mouse, pointermove fires for plain hover as well as press-and-drag, so without a press flag the row could enter .dragging without ever having a pointerdown. The _pointerActive gate is the right shape for that.

Code reference

Reading static/sessions.js:2509-2549 on the branch, the relevant new lines are:

let _pointerActive=false;
// ...
el.onpointerdown=(e)=>{
  if(e.pointerType==='mouse' && e.button!==0) return;
  _pointerActive=true;
  // ...
  if(_clearDragTimer){clearTimeout(_clearDragTimer);_clearDragTimer=null;}
  el.classList.remove('dragging');
};
el.onpointermove=(e)=>{
  // Plain hover also dispatches pointermove. Only mark a row as dragging
  // after an actual press starts on this row [...]
  if(!_pointerActive) return;
  // ...
};
el.onpointercancel=_clearPointerDragState;
el.onpointerleave=()=>{ if(_pointerActive) _clearPointerDragState(); };

The behavior matches the existing pointerup path on static/sessions.js:2547-2553 — on release it still resets _pointerActive, and the _clearDragTimer 50ms tail still gives the pointerup tap-vs-drag detection one frame to read _isDragging.

Diagnosis

A few observations:

  1. The pointerdown handler now also defensively clears el.classList.remove('dragging') and any pending _clearDragTimer. That handles the rare case where a stale row was rerendered with a residual class — good.
  2. pointerleave only clears state when _pointerActive is set. This means a hover-without-press leaving the row is a no-op (correct; nothing to clean up), and a press that drags off the row triggers _clearPointerDragState() so the trailing 50ms drag-clear schedules — also matches the post-pointerup cleanup for a confirmed drag.
  3. The previous behavior could leave .dragging on a row mid-hover if pointermove's 5px threshold tripped from cursor jitter alone. That class only cleared on the next sidebar rerender (e.g. SSE list update), which explains the "stale fade until rerender" symptom in the body.

Verification

tests/test_issue856_pinned_indicator_layout.py:119-127 is a static source-shape regression — it asserts the _pointerActive token guard and the .session-item.dragging:hover style hook are both present. That test is the right shape for this kind of UI invariant; it'll fail loudly if a future refactor inlines the move handler again. The Node-test rewrite in tests/test_issue500_session_list_virtualization.py (writing a temp .cjs instead of passing source via node -e) is unrelated to the bug but a legitimate hardening — the v8 ARG_MAX edge is a real failure mode as sessions.js grows.

All three CI checks (3.11/3.12/3.13) are green. Looks good to merge.

@nesquena-hermes nesquena-hermes closed this pull request by merging all changes into nesquena:master in 85d0279 May 6, 2026
pull Bot pushed a commit to soitun/hermes-webui that referenced this pull request May 6, 2026
pull Bot pushed a commit to soitun/hermes-webui that referenced this pull request May 6, 2026
…p + test-isolation fix

Constituent PRs:
- nesquena#1725 (@Michaelyklam) — simplify compact Activity row summary
- nesquena#1726 (@Michaelyklam) — delegate generic provider catalogs to Hermes CLI (slice of nesquena#1240)
- nesquena#1727 (@Michaelyklam) — link Claude Code OAuth in onboarding (closes nesquena#1362)
- nesquena#1728 (@starship-s) — preserve profile context when starting chats
- nesquena#1729 (@Michaelyklam) — persist compact Activity disclosure state
- nesquena#1730 (@Michaelyklam) — prevent sticky sidebar hover drag state
- nesquena#1732 (@Sanjays2402) — unpin scroll on small upward motion during streaming (closes nesquena#1731)

Plus 2 in-stage absorbed fixes:
- test-isolation fix: monkeypatch.setattr(config, 'cfg', X) survives PR nesquena#1728's
  path/mtime-aware get_config() reload. Mandatory before tag (Opus stage-302).
- Opus SHOULD-FIX #1: _lastScrollTop reset on session switch (nesquena#1732 follow-up).

Tests: 4537 → 4584 passing (+47). 0 regressions. Full suite ~128s. Stably green.

Pre-release verification:
- All 7 PRs CI-green individually + rebased onto master
- pytest 4584 passed, 0 failed (multiple runs)
- node -c clean on all 4 modified .js files
- 11/11 browser API endpoints PASS on isolated port 8789
- 20 QA tests via webui_qa_agent.sh PASS
- Opus advisor: SHIP, 5/5 verification clean, 0 MUST-FIX, 1 SHOULD-FIX absorbed
  (_lastScrollTop reset), 1 SHOULD-FIX deferred (nesquena#1736 — _clear_anthropic_env_values
  race, onboarding-time-only)

Closes nesquena#1362, nesquena#1731.
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