## 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=<messageId>`). 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=<messageId>`
- **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
