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.
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.
## 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)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
Tactical fix for B-06 (#51) —
ChatPanelnow 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
chatEvents. Token deltas append to the same text part (stable eventid), so a length-based signal — notchatEvents.length— is what changes on every streamed chunk. This avoids inventing a new bus message just to drive scroll.useLayoutEffectkeyed on that signal re-measuresdistanceFromBottom = scrollHeight - scrollTop - clientHeightat the moment the content mutates. If the user is within 24 px of the bottom we scroll toscrollHeightviascrollTo({ behavior: 'smooth' }).distance > 24 px) and we leave them alone. The pin auto-re-arms the next time they scroll back to the bottom — noscrollevent listener required, since we re-measure DOM on every content change.scrollTois feature-detected (JSDOM lacks it; fall back to directscrollTopassignment so production never throws).Scope left to M-7 (v0.8)
The full fix lands in the M-7 Turn surface once Swift emits
turnTextCompleteand the HUD hydrates fromTurn.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 newChatPanel.autoscroll.test.tsxcases:scrollTo top=scrollHeight.scrollTofires per token.scrollTo.bash scripts/build-webview.sh— webview production bundle builds; new asset hash written toApp/Resources/webview/index.html.bash scripts/check-app-builds.sh— App target compiles cleanly.for s in scripts/check-*.sh; do bash "$s"; done).JSDOM stubs
scrollHeight/scrollTop/clientHeightviaObject.definePropertybecause 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