## ADDED Requirements ### Requirement: Virtualized rendering `MessageList` SHALL use `react-virtuoso` to virtualize rendering of messages beyond the viewport. Only visible messages and a configurable overscan buffer (default 5 items above/below) SHALL be in the DOM. The virtualizer SHALL support variable-height items (Markdown renders at unpredictable heights). #### Scenario: Long transcript renders with virtualization - **WHEN** a chat has 500 messages - **THEN** only ~20 messages are in the DOM at any time (viewport ± overscan) - **AND** scrolling maintains correct scroll position #### Scenario: Streaming follows bottom by default - **WHEN** a new streaming message arrives and the user is near the bottom - **THEN** the list auto-scrolls to follow the streaming content - **AND** if the user has scrolled up reading history, auto-scroll is suppressed ### Requirement: Smarter tool-call grouping Consecutive tool runs of the same name SHALL collapse into a grouped control with a count header (same as current behavior for ≥3). Additionally, each group SHALL support independent expand/collapse state (not just the current all-or-nothing). User can expand a group to see individual calls, or collapse them back. #### Scenario: Grouped tool calls are individually expandable - **WHEN** a tool group with 5 `view_file` calls renders - **THEN** the collapsed group shows "5 view_file calls" with an expand chevron - **AND** clicking expand shows each call as a separate item - **AND** clicking collapse returns to the grouped header ### Requirement: Stagger entrance animations New messages SHALL enter with a staggered fade-slide animation. Each new message SHALL animate with `opacity: 0 → 1, y: 8 → 0`. Consecutive new messages (from a batch delta or snapshot) SHALL stagger by 40ms between items, up to a max of 500ms total stagger. #### Scenario: Batch of messages arrives - **WHEN** 10 new messages arrive in a `snapshot` frame - **THEN** each message fades in sequentially with a 40ms stagger between them - **AND** animation is skipped (zero-duration) when `prefers-reduced-motion` is set ### Requirement: Message pin/bookmark Users SHALL be able to pin a message by clicking a pin icon in the message's ActionRow. Pinned messages SHALL be tracked locally via URL hash (`#pin=`). Only one message can be pinned at a time. A pinned indicator SHALL show at the top of the message list with a "Jump to pinned" link. #### Scenario: User pins a message - **WHEN** the user clicks the pin icon on a message - **THEN** the URL hash updates to `#pin=` - **AND** a pin indicator appears at the top of the list with "Jump to pinned" link - **AND** clicking the pin icon again unpins and clears the hash #### Scenario: Pinned message navigates on click - **WHEN** the user clicks "Jump to pinned" - **THEN** the list scrolls to the pinned message - **AND** the pinned message briefly highlights