Skip to content

Fix content flash when reopening a chat with new messages#53

Merged
mcintyre94 merged 2 commits intomainfrom
improve-ui-reopening-laggy-experience-47fbcd7e
Mar 10, 2026
Merged

Fix content flash when reopening a chat with new messages#53
mcintyre94 merged 2 commits intomainfrom
improve-ui-reopening-laggy-experience-47fbcd7e

Conversation

@mcintyre94
Copy link
Copy Markdown
Owner

Summary

  • Adds streamEventUUIDsData: Data? to SpriteChat (SwiftData field + helpers)
  • Persists processedEventUUIDs alongside messages on every persistMessages() call
  • Restores processedEventUUIDs in loadSession() so reconnect deduplication works on a fresh VM

Problem

When reopening a chat where Claude had been running, ChatViewModel was freshly created with empty processedEventUUIDs. reconnectToServiceLogs checks hasPriorEvents = !processedEventUUIDs.isEmpty — since it was always false, it cleared the last assistant message's content before replaying the full log. This caused:

  1. A jarring flash where only the user message was visible
  2. A laggy/slow experience as the full log was replayed from scratch to refill the content

Fix

By persisting the UUID set alongside messages, the fresh VM loads the correct processedEventUUIDs on reopen. Reconnect then sees hasPriorEvents = true, skips clearing, and skips all already-seen events during replay — completing nearly instantly. Only genuinely new events (if Claude was still running) get appended.

SwiftData migration is automatic — streamEventUUIDsData defaults to nil for existing chats, which gracefully falls back to the previous behaviour on first reopen.

Test plan

  • Send a message to Claude, navigate away before it finishes, come back — full chat history stays visible during reconnect, new content appends smoothly
  • Send a message, let Claude finish, navigate away and back — content loads immediately with no flash, no reconnect needed (service gone)
  • Existing chats with no stored UUIDs (nil) still work correctly on first reopen

🤖 Generated with Claude Code

When reopening a chat where Claude had been running, a fresh ChatViewModel
had empty processedEventUUIDs, causing reconnectToServiceLogs to clear the
last assistant message's content before replaying logs. This produced a
jarring flash where only the user message was visible before the assistant
response slowly refilled.

Fix: persist processedEventUUIDs alongside messages in SpriteChat. On
loadSession, restore the UUID set so reconnect correctly identifies all
already-seen events, skips them, and only appends genuinely new content.
Reconnect also completes much faster since the full log replay is skipped.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@claude
Copy link
Copy Markdown

claude bot commented Mar 7, 2026

Code review

Two issues found.

1. Bug: Persisted UUID set can be overwritten with an empty set (ChatViewModel.swift line 412)

processedEventUUIDs is reset to [] at the start of each executeClaudeCommand call, before any events are processed. persistMessages is then called unconditionally with that empty set, overwriting whatever valid UUIDs were saved from the prior turn.

Concrete failure scenario:

  1. Turn N completes - UUIDs correctly saved to disk.
  2. User sends Turn N+1 - processedEventUUIDs = [].
  3. User navigates away (or connection fails early) - detach/interrupt calls persistMessages - saves [] to disk, overwriting the valid UUID set.
  4. User reopens the chat - loadStreamEventUUIDs() returns [] - hasPriorEvents is false - the content flash this PR intended to fix still occurs.

The same race exists via the periodic persist inside processServiceStream (every ~1s) if the first tick fires before any events arrive.

Suggested fix - guard the save in persistMessages so a reset-empty set never overwrites valid prior UUIDs:

if !processedEventUUIDs.isEmpty {
    chat.saveStreamEventUUIDs(processedEventUUIDs)
}

let persisted = messages.map { $0.toPersisted() }
guard let chat = fetchChat(modelContext: modelContext) else { return }
chat.saveMessages(persisted)
chat.saveStreamEventUUIDs(processedEventUUIDs)
try? modelContext.save()


2. CLAUDE.md: New model/view model logic requires unit tests

The PR adds loadStreamEventUUIDs() and saveStreamEventUUIDs() (new model methods) and modifies loadSession() and persistMessages() in ChatViewModel (view model logic), but no tests are added.

CLAUDE.md rule: "Add new unit tests when adding or modifying logic (models, parsers, utilities, view models)"

The existing test suite already covers SpriteChat serialization round-trips (SpriteChatMigrationTests.swift) and ChatViewModel session-restore behaviour - the new UUID persistence path fits naturally into those suites.

}
func loadStreamEventUUIDs() -> Set<String> {
guard let data = streamEventUUIDsData else { return [] }
return (try? JSONDecoder().decode(Set<String>.self, from: data)) ?? []
}
func saveStreamEventUUIDs(_ uuids: Set<String>) {
streamEventUUIDsData = try? JSONEncoder().encode(uuids)

Guards saveStreamEventUUIDs in persistMessages so an empty
processedEventUUIDs (reset at the start of a new turn) never
overwrites a valid set saved from a prior turn.

Adds unit tests for SpriteChat UUID round-trip and ChatViewModel
persist/restore behaviour, including the empty-set guard.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@mcintyre94 mcintyre94 merged commit 856ae2e into main Mar 10, 2026
2 checks passed
@mcintyre94 mcintyre94 deleted the improve-ui-reopening-laggy-experience-47fbcd7e branch March 10, 2026 23:59
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.

1 participant