feat(canvas): show loading state while agent history loads

Track agent history loading in the workspace and render a spinner with a
localized "loading conversation history" message in the assistant feed,
so the empty state no longer flashes before history resolves.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-10 22:42:41 +08:00
parent 0a68551bfd
commit 6f998480bc
4 changed files with 38 additions and 2 deletions
+1
View File
@@ -341,6 +341,7 @@
"conversation": "Chat",
"newChat": "New chat",
"conversationHistory": "History",
"loadingConversationHistory": "Loading conversation history...",
"scrollToLatest": "Scroll to latest",
"searchConversationPlaceholder": "Search conversations",
"deleteConversation": "Delete conversation",
+1
View File
@@ -341,6 +341,7 @@
"conversation": "对话",
"newChat": "新对话",
"conversationHistory": "历史对话",
"loadingConversationHistory": "正在加载历史对话...",
"scrollToLatest": "回到底部",
"searchConversationPlaceholder": "请输入搜索关键词",
"deleteConversation": "删除对话",
@@ -575,6 +575,7 @@ export function CanvasWorkspace({ projectId, shareAccess = ownerWorkspaceAccess
const { project, setProject, projectRef, viewport, setViewport, viewportRef, nodes, setNodes, nodesRef, applyProjectDocument } = useCanvasDocument();
const { selectedId, selectedIds, setSelectedIds, selectedNodes, selectedNode, selectOnlyNode, selectNodes, toggleNodeSelection } = useCanvasSelection(nodes);
const [messages, setMessages] = useState<AgentMessage[]>([]);
const [agentHistoryLoading, setAgentHistoryLoading] = useState(canViewChat);
const [prompt, setPrompt] = useState("");
const [agentStarterConfig, setAgentStarterConfig] = useState<AgentStarterConfig>(() => defaultAgentStarterConfig(t));
const [agentThreads, setAgentThreads] = useState<AgentThreadSummary[]>([]);
@@ -901,17 +902,20 @@ export function CanvasWorkspace({ projectId, shareAccess = ownerWorkspaceAccess
useEffect(() => {
selectedAgentThreadIdRef.current = null;
setSelectedAgentThreadId(null);
setAgentHistoryLoading(canViewChat);
setAgentThreads([]);
setAgentHistorySearch("");
setAgentHistoryOpen(false);
autoScrollAgentFeedRef.current = false;
}, [projectId]);
}, [canViewChat, projectId]);
useEffect(() => {
if (agentHistoryOpen) void loadAgentThreads();
}, [agentHistoryOpen, loadAgentThreads]);
useEffect(() => {
let active = true;
setAgentHistoryLoading(canViewChat);
designGateway
.getProject(projectId)
.then(({ project: nextProject }) => {
@@ -961,7 +965,13 @@ export function CanvasWorkspace({ projectId, shareAccess = ownerWorkspaceAccess
return;
}
setError(toUserMessageText(err, "generateError"));
})
.finally(() => {
if (active) setAgentHistoryLoading(false);
});
return () => {
active = false;
};
}, [applyProject, canViewChat, projectId, startProjectEvents]);
useEffect(() => {
@@ -2649,6 +2659,7 @@ export function CanvasWorkspace({ projectId, shareAccess = ownerWorkspaceAccess
autoScrollAgentFeedRef.current = false;
selectedAgentThreadIdRef.current = newAgentConversationId;
setSelectedAgentThreadId(newAgentConversationId);
setAgentHistoryLoading(false);
setAgentHistoryOpen(false);
setPrompt("");
setMessages([]);
@@ -4124,7 +4135,12 @@ export function CanvasWorkspace({ projectId, shareAccess = ownerWorkspaceAccess
</div>
<div className="assistant-feed" ref={assistantFeedRef} onScroll={updateAssistantScrollButton}>
{!hasCurrentConversation && !agentTimelineActive ? (
{agentHistoryLoading ? (
<div className="assistant-history-loading" role="status" aria-live="polite" data-testid="agent-history-loading">
<Loader2 className="spin" size={20} aria-hidden="true" />
<span>{t("loadingConversationHistory")}</span>
</div>
) : !hasCurrentConversation && !agentTimelineActive ? (
<CanvasAgentEmptyState config={agentStarterConfig} disabled={!canEdit} onSelectPrompt={selectAgentStarterPrompt} />
) : (
<CanvasAgentTimeline messages={messages} nodes={nodes} active={agentTimelineActive} />
+18
View File
@@ -3789,6 +3789,24 @@ button:disabled {
background: #dfe0e2;
}
.assistant-history-loading {
width: 100%;
min-height: 100%;
flex: 1 0 auto;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10px;
color: #8a9099;
font-size: 13px;
line-height: 20px;
}
.assistant-history-loading svg {
color: #646a73;
}
.assistant-scroll-to-end {
position: absolute;
inset-inline: 0;