fix(chat): anchored-scroll behavior parity with the reference model

Six reported defects, one root theme — our port diverged from the
reference semantics:

- a user gesture no longer collapses the reserved anchored end space
  (that snap-to-bottom was the 'anchoring vanishes on a micro-scroll');
  it only disarms the follow/anchor machinery, and anchor remeasures
  after the user takes over can no longer re-position the viewport
- the anchor arms synchronously BEFORE the send reaches the store, and
  the claim compares against the arm-time baseline — arming a frame
  after the optimistic row committed is why anchoring 'rarely worked'
  and sending from mid-history did nothing
- gestures are selective (wheel up, touch/pointer away from the end,
  PageUp/Home/ArrowUp), so scrolling toward the end or clicking a row
  at the live edge no longer kills follow
- 'at end' is a tight 40px band against the full content length instead
  of the list's half-viewport isNearEnd, so the scroll-to-bottom pill
  appears when the viewport actually leaves the end
- reaching the end re-arms follow without knocking an anchored turn out
  of its mode, and the overlay scrollbar suppression follows scroll
  ownership rather than the anchor's mere existence
- the status/working row moved out of the list footer to a fixed spot
  above the composer: inside the list it walked down with every
  streamed line, and its unmount was part of the end-of-stream jerk
This commit is contained in:
Bohdan Triapitsyn
2026-08-25 01:35:45 +03:00
parent 42a8eafafc
commit 955b2cc812
5 changed files with 120 additions and 42 deletions
@@ -1289,6 +1289,11 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
...additionalParts.flatMap(p => p.attachments ?? []),
];
// Arm the timeline anchor BEFORE the optimistic user row can commit;
// arming after (or a frame later) races the commit and the anchor
// never claims the new message.
scrollToBottom?.();
const sendPromise = sendMessage(
primaryText,
providerIdToSend,
@@ -1307,14 +1312,6 @@ const ChatInputComponent: React.FC<ChatInputProps> = ({
}
};
if (typeof window === 'undefined') {
scrollToBottom?.();
} else {
window.requestAnimationFrame(() => {
scrollToBottom?.();
});
}
void sendPromise.then(() => {
// Record what this session was pointed at, so the work-status panel
// can show it as a context source long after the message scrolled