| 313 | |
| 314 | /** Exported for testing. Mutates anchorRef when the window needs to advance. */ |
| 315 | export function computeSliceStart(collapsed: ReadonlyArray<{ |
| 316 | uuid: string; |
| 317 | }>, anchorRef: { |
| 318 | current: SliceAnchor; |
| 319 | }, cap = MAX_MESSAGES_WITHOUT_VIRTUALIZATION, step = MESSAGE_CAP_STEP): number { |
| 320 | const anchor = anchorRef.current; |
| 321 | const anchorIdx = anchor ? collapsed.findIndex(m => m.uuid === anchor.uuid) : -1; |
| 322 | // Anchor found → use it. Anchor lost → fall back to stored index |
| 323 | // (clamped) so collapse-regrouping uuid churn doesn't reset to 0. |
| 324 | let start = anchorIdx >= 0 ? anchorIdx : anchor ? Math.min(anchor.idx, Math.max(0, collapsed.length - cap)) : 0; |
| 325 | if (collapsed.length - start > cap + step) { |
| 326 | start = collapsed.length - cap; |
| 327 | } |
| 328 | // Refresh anchor from whatever lives at the current start — heals a |
| 329 | // stale uuid after fallback and captures a new one after advancement. |
| 330 | const msgAtStart = collapsed[start]; |
| 331 | if (msgAtStart && (anchor?.uuid !== msgAtStart.uuid || anchor.idx !== start)) { |
| 332 | anchorRef.current = { |
| 333 | uuid: msgAtStart.uuid, |
| 334 | idx: start |
| 335 | }; |
| 336 | } else if (!msgAtStart && anchor) { |
| 337 | anchorRef.current = null; |
| 338 | } |
| 339 | return start; |
| 340 | } |
| 341 | const MessagesImpl = ({ |
| 342 | messages, |
| 343 | tools, |