fix(tui): focus-aware key routing and streaming auto-scroll - #713
Conversation
- proposal.md: motivation and scope for fixing auto-scroll - design.md: technical approach for focus-aware key routing - specs/tui-event-routing/spec.md: 3 requirements, 6 scenarios - tasks.md: 5 implementation groups, 10 tasks
Restructure useInput() to only intercept keys relevant to the focused panel, allowing key events to bubble through to the message list when the inputBar is focused. This fixes auto-scroll during streaming responses that was blocked by the app-level handler capturing all keys. - Global keys (Tab, Escape) always handled at app level - InputBar focused: only intercept Tab, Escape, history nav; pass through all other keys to child components - Message list focused: intercept navigation keys for manual scroll; pass through everything else Signed-off-by: madz
Move completed change to openspec/changes/archive/2026-08-09-fix-tui-event-capture-auto-scroll. Signed-off-by: madz
The ScrollView's onContentHeightChange callback doesn't fire reliably when bubbles update via pub/sub (no parent re-render). Now the bubble publishes a scroll-to-bottom event when its content grows during streaming, and MessageList handles the actual scroll.
Auto-Scroll Audit: Fundamental Flaws1.
|
| # | Flaw | Severity | Fix |
|---|---|---|---|
| 1 | remeasureItem then getBottomOffset race |
High | Remove remeasureItem call |
| 2 | isUserScrollingRef never refreshed after mount |
High | Check position inline in handler |
| 3 | Dual scroll paths (pub/sub + callback) | Medium | Use only onContentHeightChange |
| 4 | Pub/sub updates bypass ScrollView measurement | Medium | Accept batching or trigger parent re-render |
| 5 | No onScroll sync back to React state |
Low | Add onScroll callback |
Additional Audit Findings6.
|
- Remove remeasureItem call from handleContentHeightChange (race condition) - Replace stale isUserScrollingRef with inline scroll position check - Remove dual scroll path (pub/sub scroll-to-bottom) — use onContentHeightChange only - Add onScroll callback to sync ScrollView state back to React - Clean up unused isUserScrollingRef declaration
… updates - Remove focus=false from ControlledScrollView (Ink defers unfocused renders) - Replace time-based throttle with offset-based dedup (prevents dropping real updates) - Remove unused SCROLL_THROTTLE_MS constant
…w re-measures and scrolls
Why the re-render change was neededThe streaming auto-scroll fix required an additional change beyond just calling The problem:
The fix: Calling This is the same mechanism that makes the scroll update when you tab between panels — tabbing causes a re-render, which rebuilds children, which triggers the measurement chain. Key insight: The pub/sub pattern works great for bubble-level re-renders (only the changed bubble re-renders), but it bypasses the parent entirely, which means the ScrollView never knows content has grown. The re-render bridge is necessary to connect the two systems. |
Description
The TUI's app-level
useInput()hook now routes key events based on focus state, allowing the message list to receive key events and auto-scroll during streaming. Additionally, streaming bubbles now publish scroll-to-bottom events via pub/sub, and message list bubble borders have been removed for performance.Type of Change
Testing
Coverage
Checklist
npm run lintpasses