fix(chat): use synchronous Combine publish for optimistic message inserts#26057
Conversation
…erts
Replace messageManager.messages.append() with
messageManager.batchUpdateMessages { $0.append() } at both optimistic
insert sites in MessageSendCoordinator.sendMessage().
The direct .append() goes through the _modify accessor, which defers the
Combine publish to a Task on the next cooperative executor turn. This
delays the ChatPaginationState update of paginatedVisibleMessages — the
array the view actually renders — causing the sent message to not appear
until the deferred Task fires or another event triggers a synchronous
publish (worst case: the 60-second isSending watchdog).
batchUpdateMessages publishes synchronously, ensuring
paginatedVisibleMessages is up-to-date before SwiftUI re-evaluates.
Closes LUM-929
Co-Authored-By: ashlee@vellum.ai <ashlee@vellum.ai>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
| var userMsg = ChatMessage(role: .user, text: rawText, status: .sent, skillInvocation: messageManager.pendingSkillInvocation, attachments: attachments) | ||
| userMsg.isHidden = hidden | ||
| messageManager.messages.append(userMsg) | ||
| messageManager.batchUpdateMessages { $0.append(userMsg) } |
There was a problem hiding this comment.
🚩 Two messages.append calls remain in ChatActionHandler
While this PR migrates the two .messages.append() calls in MessageSendCoordinator.swift to batchUpdateMessages, there are two remaining .messages.append() calls in ChatActionHandler.swift:159 and ChatActionHandler.swift:1128 that still use the _modify accessor path (deferred Combine publish). These may be intentional — the _modify coalescing is appropriate when followed by additional rapid subscript mutations — but if the goal of this PR is to standardize on batchUpdateMessages for all append operations, these two sites should also be considered for migration.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Those two sites (ChatActionHandler.swift:159 and :1128) use vm.messages.append(), not messageManager.messages.append().
vm.messages is a computed property on ChatViewModel that does NOT have a _modify accessor — it only has get/set. So .append() on it goes through get (copy) + set (synchronous Combine publish). These are already on the synchronous publish path and don't have the deferred publish issue.
Only direct messageManager.messages mutations trigger the _modify accessor with deferred publish, and those only exist in MessageSendCoordinator.
There was a problem hiding this comment.
Verified the full publish pipeline. Clean, minimal, correct fix.
Confirmed the _modify theory:
ChatMessageManager.messageshas three accessors:get(read),set(synchronous publish via_messagesSubject.send()), and_modify(defers publish viascheduleDeferredPublish()→Task).append()on a COW array triggers_modify, NOTset— so the Combine publish gets deferred to the next cooperative executor turnbatchUpdateMessagespublishes synchronously (_messagesSubject.send()inline), cancels any pending deferred publish, and emits exactly one Observation notificationChatPaginationStatesubscribes to_messagesSubject→recomputeVisibleMessages()→paginatedVisibleMessages— this is the array SwiftUI renders. Deferred publish = deferred display.
Closure capture is safe: Both sites capture local ChatMessage structs (userMsg/userMessage). batchUpdateMessages takes a synchronous, non-escaping (inout [ChatMessage]) -> Void. No retain cycle or lifetime issue.
No circular fix risk: PR #23464 introduced both scheduleDeferredPublish and batchUpdateMessages together. It converted all loop mutations to batchUpdateMessages but left these two single-append sites on the raw _modify path. This PR finishes that migration for the append sites.
Follow-up consideration: The PR description correctly notes remaining _modify callers at lines 429, 454, 701, 733, 743-745, 823 (subscript status mutations like messages[idx].status = .sent). These also defer publish but are less user-visible since they update existing messages rather than inserting new ones. Worth a follow-up if status transitions feel laggy.
Ship it. This directly fixes the P2 LUM-929 user-visible regression.
Summary
Fixes LUM-929: sent messages not appearing instantly in the macOS conversation.
The two optimistic insert sites in
MessageSendCoordinator.sendMessage()usedmessageManager.messages.append(), which goes through the_modifyaccessor onChatMessageManager. That accessor defers the Combine publish to aTaskon the next cooperative executor turn viascheduleDeferredPublish(). SinceChatPaginationStatesubscribes to this Combine publisher to updatepaginatedVisibleMessages(the array the view renders), the new message doesn't appear until the deferred Task fires — or worse, until another event triggers a synchronous publish (worst case: the 60-secondisSendingwatchdog).Replacing with
batchUpdateMessages { $0.append(msg) }publishes synchronously, ensuringpaginatedVisibleMessagesis current before SwiftUI re-evaluates. This is the same pattern already used at 14 other mutation sites acrossChatActionHandler,ChatViewModel, andMessageSendCoordinator.Why the
_modifydeferred publish exists (and why it doesn't help here)PR #23464 introduced
scheduleDeferredPublish()to coalesce rapid multi-element mutation loops (e.g.stopGenerating). However, that same PR also converted all those loops tobatchUpdateMessages. The remaining_modifycallers — including these two.append()sites — are single-mutation operations that don't benefit from coalescing. All streaming/action-handler mutations go throughvm.messages[idx](theget+setpath onChatViewModel), which already publishes synchronously.Prompt / plan
Investigated the full display pipeline:
messageManager.messages→ChatPaginationState.recomputeVisibleMessages()→paginatedVisibleMessages→ SwiftUI view. Traced every mutation site to determine which use_modify(deferred) vsget+set(synchronous). Confirmed onlyMessageSendCoordinatoraccessesmessageManager.messagesdirectly.Test plan
_modifyvsget+setHuman review checklist
_modifyaccessor theory: thatmessageManager.messages.append()triggers deferred publish whilebatchUpdateMessagespublishes synchronously (seeChatMessageManager.swiftlines 59–67 vs 84–94)userMsg/userMessageis safe (synchronous, non-escaping closure)messageManager.messagessubscript mutations inMessageSendCoordinator(status changes on existing messages, lines 429/454/701/733/743–745/823) should also be converted as a follow-upLink to Devin session: https://app.devin.ai/sessions/3c6d3caa63a14e42b6b8b64aa5d03778
Requested by: @ashleeradka