refactor(chat): replace timeline scroll engine with anchored-turn LegendList

Sending a message now parks that message near the top of the viewport
and streams the reply into reserved end space below it, instead of
jumping to the bottom and chasing it.

- swap @tanstack/react-virtual for @legendapp/list in the chat timeline; the
  streaming tail becomes a normal list row rather than a separately rendered
  block, so one component owns the scroll position
- add timelineScrollAnchoring: pure anchored-turn geometry plus the three
  scroll modes (following-end / anchoring-new-turn / free-scrolling)
- replace useChatAutoFollow with useChatTimelineScroll, which opts out of
  automatic movement on real gestures via a generation counter instead of the
  timer windows the old implementation needed to recognise its own writes
- move the load-older button, question/permission cards, recap, status row and
  bottom spacer into the list header/footer, since the list owns its container
- extract useScrollShadow so the shadows can attach to that container

maintainScrollAtEnd and maintainVisibleContentPosition replace the manual
prepend anchor-hold and the mobile quiet-window prepend deferral.

Validated: workspace type-check, lint, web build, ui tests per file.
Scroll behaviour itself is unverified and needs manual testing on web, desktop
and iOS.
This commit is contained in:
Bohdan Triapitsyn
2026-08-25 01:10:00 +03:00
parent 3b7d5e1c34
commit d2d8669564
18 changed files with 1739 additions and 1536 deletions
@@ -14,7 +14,7 @@ import { useThemeSystem } from '@/contexts/useThemeSystem';
import { cn } from '@/lib/utils';
import { useChatSurfaceMode } from './useChatSurfaceMode';
import type { AnimationHandlers, ContentChangeReason } from '@/hooks/useChatAutoFollow';
import type { AnimationHandlers, ContentChangeReason } from '@/hooks/useChatTimelineScroll';
import MessageBody from './message/MessageBody';
import type { AgentMentionInfo } from './message/types';
import type { StreamPhase, ToolPopupContent } from './message/types';