From d3f75701778ee588b6ef76b0ca2acb8f147095ea Mon Sep 17 00:00:00 2001 From: Naiyuan Qing <145280634+NevilleQingNY@users.noreply.github.com> Date: Tue, 14 Apr 2026 18:49:26 +0800 Subject: [PATCH] feat(chat): skeleton while switching to an un-cached session MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Switching to a session whose messages aren't cached showed the empty state (starter prompts) for the ~300ms the fetch took — jarring, because you're clicking into an existing conversation, not starting a new one. Now there's a three-branch render: skeleton while loading, empty state for real new-chat (activeSessionId === null), messages when ready. Cached switches still return data synchronously — no flash. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../chat/components/chat-message-list.tsx | 27 +++++++++++++++++++ .../views/chat/components/chat-window.tsx | 14 +++++++--- 2 files changed, 37 insertions(+), 4 deletions(-) diff --git a/packages/views/chat/components/chat-message-list.tsx b/packages/views/chat/components/chat-message-list.tsx index 882983ca64..3e12b72cf1 100644 --- a/packages/views/chat/components/chat-message-list.tsx +++ b/packages/views/chat/components/chat-message-list.tsx @@ -75,6 +75,33 @@ export function ChatMessageList({ ); } +/** + * Placeholder shown while `chat_message` for a session is being fetched + * (initial refresh, or switching to an un-cached session). Shape roughly + * mirrors an assistant → user → assistant exchange so the window doesn't + * shift under the user when real messages arrive. + */ +export function ChatMessageSkeleton() { + return ( +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ); +} + function toTimelineItem(m: TaskMessagePayload): ChatTimelineItem { return { seq: m.seq, diff --git a/packages/views/chat/components/chat-window.tsx b/packages/views/chat/components/chat-window.tsx index bc4acde04b..076629009c 100644 --- a/packages/views/chat/components/chat-window.tsx +++ b/packages/views/chat/components/chat-window.tsx @@ -29,7 +29,7 @@ import { } from "@multica/core/chat/queries"; import { useCreateChatSession, useMarkChatSessionRead } from "@multica/core/chat/mutations"; import { useChatStore } from "@multica/core/chat"; -import { ChatMessageList } from "./chat-message-list"; +import { ChatMessageList, ChatMessageSkeleton } from "./chat-message-list"; import { ChatInput } from "./chat-input"; import { ChatResizeHandles } from "./chat-resize-handles"; import { useChatResize } from "./use-chat-resize"; @@ -52,11 +52,15 @@ export function ChatWindow() { const { data: members = [] } = useQuery(memberListOptions(wsId)); const { data: sessions = [] } = useQuery(chatSessionsOptions(wsId)); const { data: allSessions = [] } = useQuery(allChatSessionsOptions(wsId)); - const { data: rawMessages } = useQuery( + const { data: rawMessages, isLoading: messagesLoading } = useQuery( chatMessagesOptions(activeSessionId ?? ""), ); // When no active session, always show empty — don't use stale cache const messages = activeSessionId ? rawMessages ?? [] : []; + // Skeleton only shows for an un-cached session fetch. Cached switches + // return data synchronously — no flash. `enabled: false` (new chat) + // keeps isLoading false so the starter prompts aren't hidden. + const showSkeleton = !!activeSessionId && messagesLoading; // Server-authoritative pending task. Survives refresh / reopen / session // switch because it's keyed on sessionId in the Query cache; WS events @@ -374,8 +378,10 @@ export function ChatWindow() {
- {/* Messages or Empty State */} - {hasMessages ? ( + {/* Messages / skeleton / empty state */} + {showSkeleton ? ( + + ) : hasMessages ? (