Skip to content

fix(web-client): persist chat transcript across page reloads#1857

Open
john-the-dev wants to merge 2 commits into
sonichi:mainfrom
john-the-dev:fix/web-chat-transcript-persist
Open

fix(web-client): persist chat transcript across page reloads#1857
john-the-dev wants to merge 2 commits into
sonichi:mainfrom
john-the-dev:fix/web-chat-transcript-persist

Conversation

@john-the-dev

Copy link
Copy Markdown
Contributor

Summary

The web-UI conversation transcript (#transcript) was in-DOM only — append-only with no persistence and no restore-on-load — so reloading the page wiped the entire chat. The Tasks list survived (it persists via taskMaploadPersistedTaskMap()), which is exactly why a reload left task entries visible but no chat bubbles, making it look like "history is gone."

This is a long-standing gap, not a new regression.

What changed (src/web-client.ts)

  • Snapshot transcript entries to localStorage (sutando-transcript-v1), capped at the 50 most recent; oversized entries (e.g. data-URL images) are skipped to stay under the storage quota, with a halve-and-retry fallback on QuotaExceededError.
  • Restore on DOMContentLoaded — recreate each bubble, sanitize stored html with DOMPurify, and re-attach a live copy button (the button's onclick can't survive an innerHTML round-trip, so it's stripped on snapshot and re-added on restore).
  • A MutationObserver on #transcript captures every append path (text replies, voice transcripts, images) without editing each call site; snapshots are debounced (400ms) and suppressed during restore.

Interaction with #1856

#1856 (in-flight chat-send persistence) re-appends the user's pending message on load via resumePendingChatSends(). When both land, an in-flight send during a reload would render the user bubble twice (once from transcript-restore, once from resume). Trivial follow-up when the second of the two merges: drop the user-bubble re-append from resumePendingChatSends() since transcript-restore already covers it. No conflict today (separate branches off main).

Test plan

  • npx tsc --noEmit passes
  • Restarted the running web-client onto this code; served HTML contains the fix
  • Manual: send/receive chat, reload page → transcript (user + assistant bubbles) is restored
  • Manual: copy button on a restored assistant bubble works
  • Manual: confirm no unbounded localStorage growth over a long session

Note: UI change — typechecked and smoke-verified the fix is served, but the manual reload behavior needs a human pass (owner is testing live).

🤖 Generated with Claude Code

john-the-dev and others added 2 commits June 30, 2026 07:28
The conversation transcript (#transcript) was in-DOM only — append-only with no
persistence and no restore-on-load — so reloading the web UI wiped the entire
chat. The Tasks list survived via taskMap, which is why a reload left task
entries visible but no chat bubbles.

Snapshot transcript entries to localStorage (capped at 50, oversized data-URL
entries skipped) and restore them on DOMContentLoaded, sanitizing stored html
with DOMPurify and re-attaching live copy buttons. A MutationObserver captures
every append path (text replies, voice transcripts, images) without editing
each call site.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Prevents the "Ask Sutando anything." seed line from duplicating when the
persisted transcript (which itself captures the seed as its first entry)
is restored on top of the freshly-rendered default.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

@cla-assistant check

@bassilkhilo-ag2 bassilkhilo-ag2 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✅ MutationObserver approach is the right way to capture all append paths without touching each call site. 400ms debounce avoids a snapshot per keyframe. TRANSCRIPT_MAX_ENTRY_LEN guard prevents data-URL images from blowing localStorage quota. DOMPurify used on restore. Approve.

@bassilkhilo-ag2 bassilkhilo-ag2 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correct approach: MutationObserver on #transcript debounced at 400ms avoids spamming localStorage on every DOM append, and catches all write paths (text, voice transcript, image) without editing each call site. The quota-exceeded retry (halving the entry count) is practical. The _transcriptRestoring guard prevents the restore pass from triggering a snapshot write, avoiding a needless re-serialization on load. TRANSCRIPT_MAX_ENTRIES=50 and MAX_ENTRY_LEN=20000 are reasonable bounds. ✓

@bassilkhilo-ag2 bassilkhilo-ag2 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Transcript persistence is well-implemented. MutationObserver captures all append paths without touch-every-call-site. DOMPurify on restore is correct defense-in-depth. Quota-exceeded retry (slice to half), 20KB entry size limit, and _transcriptRestoring guard against snapshot feedback-loop are all good defensive choices. LGTM.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants