fix(composer): preserve in-progress message across reconnect soft-refresh (LC-346) #361

Merged
longjacksonle merged 1 commit from fix/lc-346-composer-refresh-preserve into main 2026-06-19 02:32:11 +02:00

What

A WebSocket reconnect soft-refresh (LC-318) re-renders all of #main, including the composer. The composer came back seeded from the server-side draft, but the draft autosave PUT is debounced on a 1s idle gap and resets on every keystroke, so it never settles during continuous typing. A long in-progress message was therefore discarded on every reconnect (the entire message when typing without a >1s pause), and on a flaky link the refresh fires repeatedly so the message could never be finished. Caret/focus, a staged-but-unsent attachment, and an active quote/reply bar were lost too.

How

In enterConnected() (server/templates/layout.html), snapshot the live composer immediately before the soft-refresh htmx.ajax: textarea value, caret/selection, focus, hidden file_id, the #lc-staged attachment chip, and the #composer-quote-bar quote/reply chip (with its hidden quote_id). htmx.ajax returns a Promise; in its .then(), once the fresh composer and its IIFEs are mounted, restore the snapshot onto the new nodes (value, caret via setSelectionRange, focus, file_id, staged chip, quote bar), dispatch a synthetic input so the resize/counter listeners re-measure, and call window.__lcRefreshSend() to recompute the Send-button state. When nothing was in progress, restore is skipped so the server-rendered LC-64 draft still stands.

The capture/restore lives at the single soft-refresh site (the only path that replaces a mounted composer) rather than hx-preserve on the composer, which would re-run the sibling composer <script> tags on the #main swap and double-bind their document-level listeners, breaking the LC-156 "never accumulate duplicate listeners" invariant. DM and room composers share the same ids, so one snapshot covers both.

Testing

JS-only edit inside an existing <script> block; no Rust, migrations, cfg gates, or handler contracts changed. just check (server standalone + saas, desktop, clippy both modes, fmt), just test, and just test-saas all green.

#LC-346

## What A WebSocket reconnect soft-refresh (LC-318) re-renders all of `#main`, including the composer. The composer came back seeded from the server-side draft, but the draft autosave PUT is debounced on a 1s idle gap and resets on every keystroke, so it never settles during continuous typing. A long in-progress message was therefore discarded on every reconnect (the entire message when typing without a >1s pause), and on a flaky link the refresh fires repeatedly so the message could never be finished. Caret/focus, a staged-but-unsent attachment, and an active quote/reply bar were lost too. ## How In `enterConnected()` (`server/templates/layout.html`), snapshot the live composer immediately before the soft-refresh `htmx.ajax`: textarea value, caret/selection, focus, hidden `file_id`, the `#lc-staged` attachment chip, and the `#composer-quote-bar` quote/reply chip (with its hidden `quote_id`). `htmx.ajax` returns a Promise; in its `.then()`, once the fresh composer and its IIFEs are mounted, restore the snapshot onto the new nodes (value, caret via `setSelectionRange`, focus, `file_id`, staged chip, quote bar), dispatch a synthetic `input` so the resize/counter listeners re-measure, and call `window.__lcRefreshSend()` to recompute the Send-button state. When nothing was in progress, restore is skipped so the server-rendered LC-64 draft still stands. The capture/restore lives at the single soft-refresh site (the only path that replaces a mounted composer) rather than `hx-preserve` on the composer, which would re-run the sibling composer `<script>` tags on the `#main` swap and double-bind their document-level listeners, breaking the LC-156 "never accumulate duplicate listeners" invariant. DM and room composers share the same ids, so one snapshot covers both. ## Testing JS-only edit inside an existing `<script>` block; no Rust, migrations, cfg gates, or handler contracts changed. `just check` (server standalone + saas, desktop, clippy both modes, fmt), `just test`, and `just test-saas` all green. #LC-346
fix(composer): preserve in-progress message across reconnect soft-refresh
All checks were successful
check-secrets / Kingfisher (push) Successful in 4s
check-secrets / Nosey parker (push) Successful in 5s
check-secrets / TruffleHog (push) Successful in 8s
check-secrets / Kingfisher (pull_request) Successful in 4s
check-secrets / TruffleHog (pull_request) Successful in 4s
check-secrets / Nosey parker (pull_request) Successful in 6s
Create release / Create release from merged PR (pull_request) Has been skipped
Check / clippy + fmt + tests (pull_request) Successful in 5m8s
daab33b734
The LC-318 reconnect soft-refresh re-renders all of #main, including the composer, which came back seeded from the server-side draft. Because the draft autosave PUT is debounced on a 1s idle gap and resets on every keystroke, it never settles during continuous typing, so a long in-progress message was discarded on every reconnect (the whole message when typing without a >1s pause), and on a flaky link the refresh fires repeatedly so the message could never be finished. Caret/focus, a staged-but-unsent attachment, and an active quote/reply bar were lost too.

Snapshot the live composer (textarea value, caret/selection, focus, hidden file_id, the staged-attachment chip, and the quote/reply bar) immediately before the htmx.ajax, then restore it in the returned promise once the fresh composer is mounted; skip restore when nothing was in progress so the server-rendered LC-64 draft still stands. The capture/restore lives at the single soft-refresh site rather than hx-preserve on the composer, which would re-run the sibling composer scripts and double-bind their document-level listeners. DM and room composers share the same ids, so one snapshot covers both.

#LC-346

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
longjacksonle deleted branch fix/lc-346-composer-refresh-preserve 2026-06-19 02:32:12 +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!361
No description provided.