fix(hud): chat panel auto-scroll on streaming append — B-06 tactical (#51) #150

Merged
KofTwentyTwo merged 1 commits from fix/hud-chat-autoscroll-v0.1 into develop 2026-05-13 11:27:43 +00:00
KofTwentyTwo commented 2026-05-12 21:22:31 +00:00 (Migrated from github.com)

Summary

Tactical fix for B-06 (#51) — ChatPanel now pins to the bottom as streaming tokens arrive, so the user does not have to manually scroll while the agent is speaking. Pure DOM/effect work in the React HUD; no Swift, bus, or protocol changes.

Heuristic

  • Content signal: compute the total char-length across all chatEvents. Token deltas append to the same text part (stable event id), so a length-based signal — not chatEvents.length — is what changes on every streamed chunk. This avoids inventing a new bus message just to drive scroll.
  • Layout effect: a useLayoutEffect keyed on that signal re-measures distanceFromBottom = scrollHeight - scrollTop - clientHeight at the moment the content mutates. If the user is within 24 px of the bottom we scroll to scrollHeight via scrollTo({ behavior: 'smooth' }).
  • Respects the user: if they scrolled up to re-read, the pin is broken (distance > 24 px) and we leave them alone. The pin auto-re-arms the next time they scroll back to the bottom — no scroll event listener required, since we re-measure DOM on every content change.
  • JSDOM safety: scrollTo is feature-detected (JSDOM lacks it; fall back to direct scrollTop assignment so production never throws).

Scope left to M-7 (v0.8)

The full fix lands in the M-7 Turn surface once Swift emits turnTextComplete and the HUD hydrates from Turn.listTurns. This PR is the consumer-side DOM behavior that fits in v0.1 per the issue's own scope note. Empirical pixel-level scroll verification (which JSDOM can't perform) is deferred to the M-7 typed-API work.

Test plan

  • pnpm --filter @jarvis/hud test — 101 tests pass, including 4 new ChatPanel.autoscroll.test.tsx cases:
    • AS1: new event when pinned → scrollTo top=scrollHeight.
    • AS2: streaming token append (same event id) → scrollTo fires per token.
    • AS3: user scrolled 500 px up → no scrollTo.
    • AS4: user scrolled back to bottom → next append resumes scroll.
  • bash scripts/build-webview.sh — webview production bundle builds; new asset hash written to App/Resources/webview/index.html.
  • bash scripts/check-app-builds.sh — App target compiles cleanly.
  • All 18 boundary gates green (for s in scripts/check-*.sh; do bash "$s"; done).

JSDOM stubs scrollHeight / scrollTop / clientHeight via Object.defineProperty because the engine does not lay out. The assertion target is the auto-scroll effect's decision logic — which is the behavior under test.

🤖 Generated with Claude Code

## Summary Tactical fix for B-06 (#51) — `ChatPanel` now pins to the bottom as streaming tokens arrive, so the user does not have to manually scroll while the agent is speaking. Pure DOM/effect work in the React HUD; no Swift, bus, or protocol changes. ## Heuristic - **Content signal:** compute the total char-length across all `chatEvents`. Token deltas append to the same text part (stable event `id`), so a length-based signal — not `chatEvents.length` — is what changes on every streamed chunk. This avoids inventing a new bus message just to drive scroll. - **Layout effect:** a `useLayoutEffect` keyed on that signal re-measures `distanceFromBottom = scrollHeight - scrollTop - clientHeight` at the moment the content mutates. If the user is within **24 px** of the bottom we scroll to `scrollHeight` via `scrollTo({ behavior: 'smooth' })`. - **Respects the user:** if they scrolled up to re-read, the pin is broken (`distance > 24 px`) and we leave them alone. The pin auto-re-arms the next time they scroll back to the bottom — no `scroll` event listener required, since we re-measure DOM on every content change. - **JSDOM safety:** `scrollTo` is feature-detected (JSDOM lacks it; fall back to direct `scrollTop` assignment so production never throws). ## Scope left to M-7 (v0.8) The full fix lands in the M-7 Turn surface once Swift emits `turnTextComplete` and the HUD hydrates from `Turn.listTurns`. This PR is the consumer-side DOM behavior that fits in v0.1 per the issue's own scope note. Empirical pixel-level scroll verification (which JSDOM can't perform) is deferred to the M-7 typed-API work. ## Test plan - [x] `pnpm --filter @jarvis/hud test` — 101 tests pass, including 4 new `ChatPanel.autoscroll.test.tsx` cases: - AS1: new event when pinned → `scrollTo top=scrollHeight`. - AS2: streaming token append (same event id) → `scrollTo` fires per token. - AS3: user scrolled 500 px up → no `scrollTo`. - AS4: user scrolled back to bottom → next append resumes scroll. - [x] `bash scripts/build-webview.sh` — webview production bundle builds; new asset hash written to `App/Resources/webview/index.html`. - [x] `bash scripts/check-app-builds.sh` — App target compiles cleanly. - [x] All 18 boundary gates green (`for s in scripts/check-*.sh; do bash "$s"; done`). JSDOM stubs `scrollHeight` / `scrollTop` / `clientHeight` via `Object.defineProperty` because the engine does not lay out. The assertion target is the auto-scroll effect's decision logic — which is the behavior under test. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.