mirror of
https://github.com/block/buzz.git
synced 2026-11-07 01:41:06 +00:00
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)
Buzz
Desktop chat shell with:
- Tauri + React + TypeScript + Vite
- Tailwind CSS
- shadcn/ui-ready shared components
- Biome (lint/format/check)
- Feature-driven frontend structure
Scripts
pnpm dev- run the web frontendpnpm tauri dev- run the desktop apppnpm build- typecheck and build frontendpnpm typecheck- TypeScript checkspnpm lint- Biome lintpnpm format- Biome format (write)pnpm check- Biome check
Structure
src/shared- reusable app-wide code (ui,lib,styles)src/features- feature modules (vertical slices)src/app- top-level app composition