Files
buzz/desktop/src
mtoohey c1767345e0 fix(desktop): end an own send's freeze hold when its row lands
Problem:
Review of 3c47951dc ("fix(desktop): keep an own send out of the frozen
timeline tail") raised two findings in MessageTimeline.tsx.

- Warning: the hold on freezing the tail lasted as long as the send's
  bottom pin, not as long as the send. The pin fires only when the tail
  id changes (`newLatestArrived` in useAnchoredScroll), and only firing
  or a channel change cleared it. The window cache sorts rows by
  created_at, so the sent row doesn't become the tail when the last row
  has a later created_at (a sender whose clock runs ahead) or a
  same-second row sorts after it. The pin then stayed armed after the
  sent row rendered, and a reader who scrolled up never froze the tail.
  The next arrival to become the tail fired the pin and pulled them to
  the bottom instead of showing the pill. Before 3c47951dc the freeze
  buffered that arrival, so a stale pin could not fire under a reader.
- Nit: `prepareForOwnMessage` set `semanticAtBottomRef.current = true`,
  which the hold already made redundant, and cancelling the queued
  freeze left behind the `suppressNextSemanticBottomRef` it had set.

Fix:
`useAnchoredScroll` keeps a pending-send flag next to the pin.
`scrollToBottomOnNextUpdate` sets both. The restoration effect clears
the flag on the first append after arming, whether or not it moves the
tail (`newLatestArrived` or an "append" delta), and the channel reset
clears it. `isBottomPinArmed` becomes `isOwnSendPending`, which reads
the flag, and MessageTimeline holds off freezing only while it is set.
The pin itself is unchanged and still waits for a new tail. Once the
hold ends, a reader who scrolls up freezes the tail as before, so later
arrivals wait behind the pill and a stale pin can't fire under them.

This keeps arming and disarming in the hook. Disarming from ChannelPane
after `onSendMessage` settles would need a second call site (the
welcome add-agent flow also arms the pin), and the send's promise isn't
tied to when the sent row reaches the deferred snapshot.

`prepareForOwnMessage` now clears `suppressNextSemanticBottomRef`
instead of setting the semantic ref, so dropping the queued freeze also
drops the suppression it armed. No test covers this: a leftover
suppression only swallowed a bottom report while the send's release
was already making the timeline semantically at the bottom.

Validation:
- New case in `MessageTimeline.ownSend.jsdom-test.mjs`: the tail's
  createdAt is an hour ahead, the send lands above it, the reader
  scrolls up 200 px, and a row from the same fast clock arrives and
  becomes the tail. It expects "1 new message" and the reader's offset
  unchanged. On 3c47951dc it fails: no pill, and the pin moved the
  reader from 756 to the floor at 1052 and rendered the arrival. With
  the fix the reader stays at 756 and the arrival waits behind the pill.
- Mutations: clearing the flag only on a new tail, or never on an
  append, fails only the new case. Removing the hold check fails the
  original "gap after the send" case, as before.
- Messages UI jsdom tests (10) and `src/features/messages` unit tests
  (1530) pass. Full desktop `pnpm test`: 6933 unit and 179 jsdom tests
  pass.
- `tsc --noEmit`, `biome check` on the changed files,
  `check:file-sizes` and `git diff --check` are clean. No browser E2E
  was run for this follow-up.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: mtoohey <mtoohey@squareup.com>
(cherry picked from commit a2a9767de1b4f01879c652b51e9c10e508678aa6)
2026-11-05 03:27:40 +00:00
..