fix(composer): reconcile a stuck send in place, not by reloading #messages (LC-382) #392

Merged
longjacksonle merged 1 commit from fix/lc-382-inplace-reconcile into main 2026-06-20 03:11:36 +02:00

Follow-up to #390 (LC-382). Reopens/continues the same bug.

Problem with the first fix

The watchdog in #390 reloaded the entire #messages region (htmx.ajax(GET, location, {select:'#messages', swap:'outerHTML'})) when the optimistic placeholder was still pending. That recreates the scroll container and jumps it to the top of history - jarring, and arguably worse than the stuck placeholder.

Better fix: in-place reconcile

  • post_message now returns the new message id in an X-LC-Message-Id response header (still 204, no body).
  • The composer's after-request success handler captures it. If the placeholder for this cid is still pending after the ~4s grace window (the WS broadcast was missed), __lcEchoReconcile fetches just that one message via the existing GET /messages/{id} (get_single_message, the per-viewer single-message fragment) and swaps it into the placeholder by id (target: #msg-pending-{cid}, swap: outerHTML).
  • The placeholder is already at the bottom where the user is, so the canonical message simply takes its place: no full-list reload, no scroll jump.

Why it's safe

  • A missed broadcast is never replayed by the server, so a late WS arrival (and thus a duplicate #msg-{id}) cannot happen past the grace window.
  • Still a no-op once the placeholder is gone: the happy path (WS reconcile already removed it), a quarantine drop (__lcEchoDrop), or a failure flip (__lcEchoFailed cleared data-lc-pending).
  • Happy path cost is one small extra header; the single-message render only runs on the rare fallback.

Testing

  • ./dev/cargo check, just test, just test-saas - all green.

🤖 Generated with Claude Code

Follow-up to #390 (LC-382). Reopens/continues the same bug. ## Problem with the first fix The watchdog in #390 reloaded the entire `#messages` region (`htmx.ajax(GET, location, {select:'#messages', swap:'outerHTML'})`) when the optimistic placeholder was still pending. That recreates the scroll container and jumps it to the **top of history** - jarring, and arguably worse than the stuck placeholder. ## Better fix: in-place reconcile - `post_message` now returns the new message id in an **`X-LC-Message-Id`** response header (still `204`, no body). - The composer's after-request success handler captures it. If the placeholder for this `cid` is still pending after the ~4s grace window (the WS broadcast was missed), `__lcEchoReconcile` fetches **just that one message** via the existing `GET /messages/{id}` (`get_single_message`, the per-viewer single-message fragment) and swaps it into the placeholder by id (`target: #msg-pending-{cid}`, `swap: outerHTML`). - The placeholder is already at the bottom where the user is, so the canonical message simply takes its place: **no full-list reload, no scroll jump.** ## Why it's safe - A missed broadcast is never replayed by the server, so a late WS arrival (and thus a duplicate `#msg-{id}`) cannot happen past the grace window. - Still a no-op once the placeholder is gone: the happy path (WS reconcile already removed it), a quarantine drop (`__lcEchoDrop`), or a failure flip (`__lcEchoFailed` cleared `data-lc-pending`). - Happy path cost is one small extra header; the single-message render only runs on the rare fallback. ## Testing - `./dev/cargo check`, `just test`, `just test-saas` - all green. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
fix(composer): reconcile a stuck send in place, not by reloading #messages (LC-382)
All checks were successful
Check / clippy + fmt + tests (pull_request) Successful in 3m25s
check-secrets / Nosey parker (push) Successful in 3s
check-secrets / Kingfisher (push) Successful in 4s
check-secrets / TruffleHog (push) Successful in 5s
check-secrets / Nosey parker (pull_request) Successful in 5s
check-secrets / Kingfisher (pull_request) Successful in 5s
check-secrets / TruffleHog (pull_request) Successful in 5s
Create release / Create release from merged PR (pull_request) Has been skipped
88209b9b37
The first LC-382 watchdog (#390) reloaded the whole #messages region from the current URL when the optimistic placeholder was still pending. That recreates the scroll container and jumps it to the top of history - jarring, and worse UX than the bug it fixed.

Replace it with a surgical in-place reconcile. post_message now returns the new message id in an X-LC-Message-Id header (still a 204, no body). The composer's after-request success handler captures it and, if the WS broadcast is still missed after the ~4s grace window, __lcEchoReconcile fetches THAT one message (GET /messages/{id}, the existing per-viewer get_single_message fragment) and swaps it into the placeholder by id (target #msg-pending-{cid}, outerHTML). The placeholder is already at the bottom where the user is, so the real message simply takes its place: no full-list reload, no scroll jump.

A missed broadcast is never replayed by the server, so a late WS arrival (and thus a duplicate row) cannot happen past the grace window. Still a no-op once the placeholder is gone - the happy path, a quarantine drop (__lcEchoDrop), or a failure flip (__lcEchoFailed). Server returns one extra small header on the happy path and renders the single message only on the rare fallback.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
longjacksonle deleted branch fix/lc-382-inplace-reconcile 2026-06-20 03:11:36 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/lets-chat!392
No description provided.