nesquena/hermes-webui

Streaming rendering broken for months: content flickers on settle, user cannot scroll up during streaming

Open

#6,414 opened on Jul 22, 2026

 (3 comments) (0 reactions) (0 assignees)Python (2,309 forks)github user discovery
bughelp wantedperformanceprioritysprint-candidatestreamingux

Repository metrics

Stars
 (16,904 stars)
PR merge metrics
 (Avg merge 14h 31m) (314 merged PRs in 30d)

Description

Streaming rendering is broken: content flickers on settle, user can't scroll up during streaming

Versions affected: v0.52.0 through latest v0.52.76 (confirmed on v0.52.0, v0.52.41, v0.52.76)

Environment: Linux, Chrome/Firefox latest, Desktop


I've spent significant time debugging these issues. They make the WebUI nearly unusable for daily use. I'm genuinely disappointed — this has been going on for dozens of releases without a fix.

Issue 1: Content flickers on settle

When a streaming response finishes ("done" event), the worklog rows get re-rendered. The deferred DOM approach (introduced in #5839) builds lightweight shell elements during streaming, then replaces them with real DOM post-processed nodes on settle. This replacement triggers a layout reflow that visibly flickers the entire activity pane.

The root cause is in _materializeDeferredWorklogRows() (ui.js) — the deferred rows are built via requestAnimationFrame, causing the swap to span two frames and producing a visible flash.

Issue 2: User cannot scroll up during streaming

When the user scrolls up to read history while a response is streaming, the scroll listener's _programmaticScroll guard in messages.js suppresses the unpin:

if(_programmaticScroll) return;

During streaming, renderMessages({preserveScroll:true}) fires every ~100ms. Each call sets _programmaticScroll=true for ~16ms (deferred clear via rAF + setTimeout(0)). This means _programmaticScroll is active ~16% of the time constantly — the user's upward scroll events are swallowed, _messageUserUnpinned never gets set, and the next preserveScroll render yanks the viewport back to the bottom.

Wheel/touch intent bypasses exist but don't help when the guard fires before the intent is registered.

Issue 3: Scroll position jumps on large scrolls after settle

Even when the user successfully scrolls up, the worklog collapse on settle ("Done in Xs" summary) reduces content height by hundreds of pixels. The absolute-pixel scroll restoration (scrollTop = maxTop - bottom) can't compensate for the height change, causing the viewport to jump upward by the collapse delta.

The keepSettledWorklogOpen mechanism (#5260) tries to prevent this but it's a single-shot token that doesn't cover all settle paths.

Attempted workarounds

I tried:

  • chat_activity_display_mode: compact_worklog / transparent_stream / hide_all_activity — none fix it
  • Adding contain: style layout to activity groups — breaks overflow-anchor
  • Disabling deferred worklog DOM entirely — helps flicker but not scroll
  • Keeping worklog expanded after settle — helps scroll but makes activity area always visible

None is a complete solution. The rendering pipeline needs a redesign to avoid the full-DOM-replacement settle dance. A virtual-scroll or incremental-update approach (like every other chat UI uses) would be far more stable.

What's needed

The core problems are:

  1. Don't replace the streaming DOM wholesale on settle. Incremental updates only.
  2. Don't let _programmaticScroll suppress real user input. User intent detection should take priority over render bookkeeping.
  3. Either keep worklog height stable across settle, or use semantic scroll anchoring instead of absolute pixel positions.

Six dozen releases and none of these have been addressed. This needs attention.

Contributor guide