Skip to content

fix(chat): use paginatedVisibleMessages for empty/skeleton routing to fix blank chat on load#24095

Merged
Jasonnnz merged 1 commit into
mainfrom
devin/1775593181-fix-empty-chat-routing
Apr 7, 2026
Merged

fix(chat): use paginatedVisibleMessages for empty/skeleton routing to fix blank chat on load#24095
Jasonnnz merged 1 commit into
mainfrom
devin/1775593181-fix-empty-chat-routing

Conversation

@devin-ai-integration
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration Bot commented Apr 7, 2026

Summary

Fixes a bug where loading the app sometimes shows a blank chat area (no messages, no skeleton, no greeting) despite a conversation being selected in the sidebar. ChatView's display routing (isEmptyState, shouldShowSkeleton) checked viewModel.messages.isEmpty, but the actual rendering used viewModel.paginatedVisibleMessages — these come from different @Observable objects (ChatMessageManager vs ChatPaginationState) connected via Combine, and can desync when the _modify accessor defers its Combine publish or when all messages are filtered by ChatVisibleMessageFilter. The fix uses paginatedVisibleMessages.isEmpty for both routing checks so branching is consistent with what's actually displayed.

Also removes the unnecessary Combine re-subscription with dropFirst() in resetMessagePagination() — the ChatPaginationState is per-ChatViewModel so the existing subscription from init already tracks the correct messageManager, and the method already calls recomputeVisibleMessages synchronously.

Review & Testing Checklist for Human

  • Build in Xcode — CI skips macOS builds; this PR cannot be verified without a local Xcode build
  • Relaunch test — Quit and relaunch the app 5+ times; verify conversations always load messages (or show skeleton → messages) instead of a blank chat area
  • Conversation switching — Switch between multiple conversations rapidly and verify the correct state always appears (skeleton → messages, or empty state greeting for new conversations)
  • All-filtered edge case — If a conversation has only hidden/subagent messages, the view will now show the empty state greeting instead of a blank screen; confirm this is acceptable UX

Notes

  • Root cause was identified through static analysis — the intermittent nature makes on-demand reproduction difficult. The fix makes the system robust against any desync between messages and paginatedVisibleMessages regardless of specific trigger.
  • resetMessagePagination() is never called externally in the current codebase, so removing its re-subscription is low-risk. The original subscription from init remains active.
  • iOS ChatContentView uses a different routing pattern and is not affected.

Link to Devin session: https://app.devin.ai/sessions/ca7df320069f4c0da87d33cfa3e401ab
Requested by: @Jasonnnz


Open with Devin

… ChatView

The ChatView routing logic (isEmptyState, shouldShowSkeleton) previously
checked viewModel.messages.isEmpty, but the actual rendering used
viewModel.paginatedVisibleMessages. These properties come from different
@observable objects (ChatMessageManager vs ChatPaginationState) connected
via Combine. When messages was non-empty but paginatedVisibleMessages was
empty — due to the _modify accessor's deferred Combine publish or all
messages being filtered by ChatVisibleMessageFilter — the view fell
through to activeConversationContent rendering nothing: the blank chat
area shown in the bug report.

Fix: use paginatedVisibleMessages.isEmpty for both routing checks so the
decision is consistent with what is actually displayed.

Also removes the unnecessary Combine re-subscription with dropFirst() in
resetMessagePagination(). The ChatPaginationState belongs to a specific
ChatViewModel (per-conversation), so the existing subscription already
tracks the correct messageManager. The re-subscription added fragility
without benefit since the method already calls recomputeVisibleMessages
synchronously.

Co-Authored-By: Jason Zhou <jasonczhou3@gmail.com>
@devin-ai-integration
Copy link
Copy Markdown
Contributor Author

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment and CI monitoring

Copy link
Copy Markdown
Contributor Author

@devin-ai-integration devin-ai-integration Bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 1 potential issue.

View 2 additional findings in Devin Review.

Open in Devin Review

Comment on lines 105 to 111
private var isEmptyState: Bool {
viewModel.messages.isEmpty && viewModel.isHistoryLoaded
viewModel.paginatedVisibleMessages.isEmpty && viewModel.isHistoryLoaded
}

private var shouldShowSkeleton: Bool {
viewModel.messages.isEmpty && !viewModel.isHistoryLoaded
viewModel.paginatedVisibleMessages.isEmpty && !viewModel.isHistoryLoaded
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

🚩 Subtle behavior change: empty state now shown when messages exist but are all hidden

The switch from viewModel.messages.isEmpty to viewModel.paginatedVisibleMessages.isEmpty at clients/macos/vellum-assistant/Features/Chat/ChatView.swift:106 and :110 introduces a semantic change. If a conversation has messages but they are all hidden (subagent notifications, hidden messages, or messages without renderable content filtered by ChatVisibleMessageFilter), paginatedVisibleMessages will be empty while messages would not be.

Previously, this would render activeConversationContent (empty message list + composer). Now it renders the empty state view (greeting + built-in composer). Both paths provide a working composer, so the user can still interact. This is likely an intentional UX improvement — showing the empty state greeting when nothing is visible is arguably better than an empty message list. However, the reviewer should confirm this edge case (all-hidden messages) was considered.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is an intentional improvement, already documented in the PR description under the "All-filtered edge case" checklist item. When all messages are filtered (hidden/subagent/phantom), showing the empty state greeting with a working composer is better UX than showing a blank message list with a composer — the user gets context that the conversation is empty from their perspective rather than staring at a blank screen.

@Jasonnnz Jasonnnz merged commit 96b5943 into main Apr 7, 2026
7 checks passed
@Jasonnnz Jasonnnz deleted the devin/1775593181-fix-empty-chat-routing branch April 7, 2026 20:37
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