Files
deer-flow/frontend/src/components/workspace/messages/message-list.tsx

207 lines
7.2 KiB
TypeScript
Raw Normal View History

import type { Message } from "@langchain/langgraph-sdk";
2026-01-15 23:40:21 +08:00
import type { UseStream } from "@langchain/langgraph-sdk/react";
import {
Conversation,
ConversationContent,
} from "@/components/ai-elements/conversation";
2026-02-07 16:14:48 +08:00
import { useI18n } from "@/core/i18n/hooks";
2026-01-16 22:35:20 +08:00
import {
2026-01-25 00:35:42 +08:00
extractContentFromMessage,
2026-01-16 22:35:20 +08:00
extractPresentFilesFromMessage,
2026-02-07 16:14:48 +08:00
extractTextFromMessage,
2026-01-16 22:35:20 +08:00
groupMessages,
2026-01-25 00:35:42 +08:00
hasContent,
2026-01-16 22:35:20 +08:00
hasPresentFiles,
2026-02-07 16:14:48 +08:00
hasReasoning,
2026-01-16 22:35:20 +08:00
} from "@/core/messages/utils";
2026-01-25 00:35:42 +08:00
import { useRehypeSplitWordsIntoSpans } from "@/core/rehype";
2026-02-07 16:14:48 +08:00
import type { Subtask } from "@/core/tasks";
import { useUpdateSubtask } from "@/core/tasks/context";
import type { AgentThreadState } from "@/core/threads";
2026-01-15 23:40:21 +08:00
import { cn } from "@/lib/utils";
2026-01-17 00:02:03 +08:00
import { ArtifactFileList } from "../artifacts/artifact-file-list";
2026-01-15 23:40:21 +08:00
import { StreamingIndicator } from "../streaming-indicator";
import { MarkdownContent } from "./markdown-content";
2026-01-15 23:40:21 +08:00
import { MessageGroup } from "./message-group";
import { MessageListItem } from "./message-list-item";
import { MessageListSkeleton } from "./skeleton";
2026-02-07 16:14:48 +08:00
import { SubtaskCard } from "./subtask-card";
2026-01-15 23:40:21 +08:00
export function MessageList({
className,
2026-01-17 15:09:44 +08:00
threadId,
2026-01-15 23:40:21 +08:00
thread,
messages,
2026-01-22 00:26:11 +08:00
paddingBottom = 160,
2026-01-15 23:40:21 +08:00
}: {
className?: string;
2026-01-17 15:09:44 +08:00
threadId: string;
thread: UseStream<AgentThreadState>;
messages: Message[];
2026-01-22 00:26:11 +08:00
paddingBottom?: number;
2026-01-15 23:40:21 +08:00
}) {
2026-02-07 16:14:48 +08:00
const { t } = useI18n();
2026-01-25 00:35:42 +08:00
const rehypePlugins = useRehypeSplitWordsIntoSpans(thread.isLoading);
2026-02-07 16:14:48 +08:00
const updateSubtask = useUpdateSubtask();
2026-01-15 23:40:21 +08:00
if (thread.isThreadLoading) {
return <MessageListSkeleton />;
}
return (
<Conversation
2026-01-17 11:02:33 +08:00
className={cn("flex size-full flex-col justify-center", className)}
2026-01-15 23:40:21 +08:00
>
2026-02-07 16:14:48 +08:00
<ConversationContent className="mx-auto w-full max-w-(--container-width-md) gap-8 pt-12">
fix(frontend): no half-finished citations, correct state when SSE ends Citations: - In shouldShowCitationLoading, treat any unreplaced [cite-N] in cleanContent as show-loading (no body). Fixes Ultra and other modes when refs arrive before the <citations> block in the stream. - Single rule: hasUnreplacedCitationRefs(cleanContent) => true forces loading; then isLoading && hasCitationsBlock(rawContent) for streaming indicator. SSE end state: - When stream finishes, SDK may set isLoading=false before client state has the final message content, so UI stayed wrong until refresh. - Store onFinish(state) as finalState in chat page; clear when stream starts. - Pass messagesOverride={finalState.messages} to MessageList when not loading so the list uses server-complete messages right after SSE ends (no refresh). Chore: - Stop tracking .githooks/pre-commit; add .githooks/ to .gitignore (local only). Co-authored-by: Cursor <cursoragent@cursor.com> --- fix(前端): 杜绝半成品引用,SSE 结束时展示正确状态 引用: - shouldShowCitationLoading 中只要 cleanContent 仍含未替换的 [cite-N] 就 只显示加载、不渲染正文,解决流式时引用块未到就出现 [cite-1] 的问题。 - 规则:hasUnreplacedCitationRefs(cleanContent) 为真则一律显示加载; 此外 isLoading && hasCitationsBlock 用于流式时显示「正在整理引用」。 SSE 结束状态: - 流结束时 SDK 可能先置 isLoading=false,客户端 messages 尚未包含 最终内容,导致需刷新才显示正确。 - 在对话页保存 onFinish(state) 为 finalState,流开始时清空。 - 非加载时向 MessageList 传入 messagesOverride={finalState.messages}, 列表在 SSE 结束后立即用服务端完整消息渲染,无需刷新。 杂项: - 取消跟踪 .githooks/pre-commit,.gitignore 增加 .githooks/(仅本地)。
2026-02-09 15:15:20 +08:00
{groupMessages(messages, (group) => {
2026-01-18 13:07:56 +08:00
if (group.type === "human" || group.type === "assistant") {
return (
<MessageListItem
key={group.id}
message={group.messages[0]!}
isLoading={thread.isLoading}
/>
);
2026-02-07 16:14:48 +08:00
} else if (group.type === "assistant:clarification") {
const message = group.messages[0];
if (message && hasContent(message)) {
return (
<MarkdownContent
feat(frontend): unify citation logic and prevent half-finished citations - Add SafeCitationContent as single component for citation-aware body: useParsedCitations + shouldShowCitationLoading; show loading until citations complete, then render body with createCitationMarkdownComponents. Supports optional remarkPlugins, rehypePlugins, isHuman, img. - Refactor MessageListItem: assistant message body now uses SafeCitationContent only; remove duplicate useParsedCitations, shouldShowCitationLoading, createCitationMarkdownComponents and CitationsLoadingIndicator logic. Human messages keep plain AIElementMessageResponse (no citation parsing). - Use SafeCitationContent for clarification, present-files (message-list), thinking steps and write_file loading (message-group), subtask result (subtask-card). Artifact markdown preview keeps same guard (shouldShowCitationLoading) with ArtifactFilePreview. - Unify loading condition: shouldShowCitationLoading(rawContent, cleanContent, isLoading) is the single source of truth. Show loading when (isLoading && hasCitationsBlock(rawContent)) or when (hasCitationsBlock(rawContent) && hasUnreplacedCitationRefs(cleanContent)) so Pro/Ultra modes also show "loading citations" and half-finished [cite-N] never appear. - message-group write_file: replace hasCitationsBlock + threadIsLoading with shouldShowCitationLoading(fileContent, cleanContent, threadIsLoading && isLast) for consistency. - citations/utils: parse incomplete <citations> during streaming; remove isCitationsBlockIncomplete; keep hasUnreplacedCitationRefs internal; document display rule in file header. Co-authored-by: Cursor <cursoragent@cursor.com> --- feat(前端): 统一引用逻辑并杜绝半成品引用 - 新增 SafeCitationContent 作为引用正文的唯一出口:内部使用 useParsedCitations + shouldShowCitationLoading,在引用未就绪时只显示 「正在整理引用」,就绪后用 createCitationMarkdownComponents 渲染正文; 支持可选 remarkPlugins、rehypePlugins、isHuman、img。 - 重构 MessageListItem:助手消息正文仅通过 SafeCitationContent 渲染, 删除重复的 useParsedCitations、shouldShowCitationLoading、 createCitationMarkdownComponents、CitationsLoadingIndicator 等逻辑; 用户消息仍用 AIElementMessageResponse,不做引用解析。 - 澄清、present-files(message-list)、思考步骤与 write_file 加载 (message-group)、子任务结果(subtask-card)均使用 SafeCitationContent;Artifact 的 markdown 预览仍用同一 guard shouldShowCitationLoading,正文由 ArtifactFilePreview 渲染。 - 统一加载条件:shouldShowCitationLoading(rawContent, cleanContent, isLoading) 为唯一判断。在「流式中且已有引用块」或「有引用块且 cleanContent 中仍有未替换的 [cite-N]」时仅显示加载,从而在 Pro/Ultra 下也能看到「正在整理引用」,且永不出现半成品 [cite-N]。 - message-group 的 write_file:用 shouldShowCitationLoading( fileContent, cleanContent, threadIsLoading && isLast) 替代 hasCitationsBlock + threadIsLoading,与其他场景一致。 - citations/utils:流式时解析未闭合的 <citations>;移除 isCitationsBlockIncomplete;hasUnreplacedCitationRefs 保持内部使用; 在文件头注释中说明展示规则。
2026-02-09 15:01:51 +08:00
key={group.id}
content={extractContentFromMessage(message)}
isLoading={thread.isLoading}
rehypePlugins={rehypePlugins}
/>
);
}
return null;
2026-02-07 16:14:48 +08:00
} else if (group.type === "assistant:present-files") {
const files: string[] = [];
2026-01-18 13:07:56 +08:00
for (const message of group.messages) {
if (hasPresentFiles(message)) {
const presentFiles = extractPresentFilesFromMessage(message);
files.push(...presentFiles);
2026-01-16 22:35:20 +08:00
}
}
2026-01-15 23:40:21 +08:00
return (
2026-01-25 00:35:42 +08:00
<div className="w-full" key={group.id}>
{group.messages[0] && hasContent(group.messages[0]) && (
<MarkdownContent
feat(frontend): unify citation logic and prevent half-finished citations - Add SafeCitationContent as single component for citation-aware body: useParsedCitations + shouldShowCitationLoading; show loading until citations complete, then render body with createCitationMarkdownComponents. Supports optional remarkPlugins, rehypePlugins, isHuman, img. - Refactor MessageListItem: assistant message body now uses SafeCitationContent only; remove duplicate useParsedCitations, shouldShowCitationLoading, createCitationMarkdownComponents and CitationsLoadingIndicator logic. Human messages keep plain AIElementMessageResponse (no citation parsing). - Use SafeCitationContent for clarification, present-files (message-list), thinking steps and write_file loading (message-group), subtask result (subtask-card). Artifact markdown preview keeps same guard (shouldShowCitationLoading) with ArtifactFilePreview. - Unify loading condition: shouldShowCitationLoading(rawContent, cleanContent, isLoading) is the single source of truth. Show loading when (isLoading && hasCitationsBlock(rawContent)) or when (hasCitationsBlock(rawContent) && hasUnreplacedCitationRefs(cleanContent)) so Pro/Ultra modes also show "loading citations" and half-finished [cite-N] never appear. - message-group write_file: replace hasCitationsBlock + threadIsLoading with shouldShowCitationLoading(fileContent, cleanContent, threadIsLoading && isLast) for consistency. - citations/utils: parse incomplete <citations> during streaming; remove isCitationsBlockIncomplete; keep hasUnreplacedCitationRefs internal; document display rule in file header. Co-authored-by: Cursor <cursoragent@cursor.com> --- feat(前端): 统一引用逻辑并杜绝半成品引用 - 新增 SafeCitationContent 作为引用正文的唯一出口:内部使用 useParsedCitations + shouldShowCitationLoading,在引用未就绪时只显示 「正在整理引用」,就绪后用 createCitationMarkdownComponents 渲染正文; 支持可选 remarkPlugins、rehypePlugins、isHuman、img。 - 重构 MessageListItem:助手消息正文仅通过 SafeCitationContent 渲染, 删除重复的 useParsedCitations、shouldShowCitationLoading、 createCitationMarkdownComponents、CitationsLoadingIndicator 等逻辑; 用户消息仍用 AIElementMessageResponse,不做引用解析。 - 澄清、present-files(message-list)、思考步骤与 write_file 加载 (message-group)、子任务结果(subtask-card)均使用 SafeCitationContent;Artifact 的 markdown 预览仍用同一 guard shouldShowCitationLoading,正文由 ArtifactFilePreview 渲染。 - 统一加载条件:shouldShowCitationLoading(rawContent, cleanContent, isLoading) 为唯一判断。在「流式中且已有引用块」或「有引用块且 cleanContent 中仍有未替换的 [cite-N]」时仅显示加载,从而在 Pro/Ultra 下也能看到「正在整理引用」,且永不出现半成品 [cite-N]。 - message-group 的 write_file:用 shouldShowCitationLoading( fileContent, cleanContent, threadIsLoading && isLast) 替代 hasCitationsBlock + threadIsLoading,与其他场景一致。 - citations/utils:流式时解析未闭合的 <citations>;移除 isCitationsBlockIncomplete;hasUnreplacedCitationRefs 保持内部使用; 在文件头注释中说明展示规则。
2026-02-09 15:01:51 +08:00
content={extractContentFromMessage(group.messages[0])}
isLoading={thread.isLoading}
2026-01-25 00:35:42 +08:00
rehypePlugins={rehypePlugins}
feat(frontend): unify citation logic and prevent half-finished citations - Add SafeCitationContent as single component for citation-aware body: useParsedCitations + shouldShowCitationLoading; show loading until citations complete, then render body with createCitationMarkdownComponents. Supports optional remarkPlugins, rehypePlugins, isHuman, img. - Refactor MessageListItem: assistant message body now uses SafeCitationContent only; remove duplicate useParsedCitations, shouldShowCitationLoading, createCitationMarkdownComponents and CitationsLoadingIndicator logic. Human messages keep plain AIElementMessageResponse (no citation parsing). - Use SafeCitationContent for clarification, present-files (message-list), thinking steps and write_file loading (message-group), subtask result (subtask-card). Artifact markdown preview keeps same guard (shouldShowCitationLoading) with ArtifactFilePreview. - Unify loading condition: shouldShowCitationLoading(rawContent, cleanContent, isLoading) is the single source of truth. Show loading when (isLoading && hasCitationsBlock(rawContent)) or when (hasCitationsBlock(rawContent) && hasUnreplacedCitationRefs(cleanContent)) so Pro/Ultra modes also show "loading citations" and half-finished [cite-N] never appear. - message-group write_file: replace hasCitationsBlock + threadIsLoading with shouldShowCitationLoading(fileContent, cleanContent, threadIsLoading && isLast) for consistency. - citations/utils: parse incomplete <citations> during streaming; remove isCitationsBlockIncomplete; keep hasUnreplacedCitationRefs internal; document display rule in file header. Co-authored-by: Cursor <cursoragent@cursor.com> --- feat(前端): 统一引用逻辑并杜绝半成品引用 - 新增 SafeCitationContent 作为引用正文的唯一出口:内部使用 useParsedCitations + shouldShowCitationLoading,在引用未就绪时只显示 「正在整理引用」,就绪后用 createCitationMarkdownComponents 渲染正文; 支持可选 remarkPlugins、rehypePlugins、isHuman、img。 - 重构 MessageListItem:助手消息正文仅通过 SafeCitationContent 渲染, 删除重复的 useParsedCitations、shouldShowCitationLoading、 createCitationMarkdownComponents、CitationsLoadingIndicator 等逻辑; 用户消息仍用 AIElementMessageResponse,不做引用解析。 - 澄清、present-files(message-list)、思考步骤与 write_file 加载 (message-group)、子任务结果(subtask-card)均使用 SafeCitationContent;Artifact 的 markdown 预览仍用同一 guard shouldShowCitationLoading,正文由 ArtifactFilePreview 渲染。 - 统一加载条件:shouldShowCitationLoading(rawContent, cleanContent, isLoading) 为唯一判断。在「流式中且已有引用块」或「有引用块且 cleanContent 中仍有未替换的 [cite-N]」时仅显示加载,从而在 Pro/Ultra 下也能看到「正在整理引用」,且永不出现半成品 [cite-N]。 - message-group 的 write_file:用 shouldShowCitationLoading( fileContent, cleanContent, threadIsLoading && isLast) 替代 hasCitationsBlock + threadIsLoading,与其他场景一致。 - citations/utils:流式时解析未闭合的 <citations>;移除 isCitationsBlockIncomplete;hasUnreplacedCitationRefs 保持内部使用; 在文件头注释中说明展示规则。
2026-02-09 15:01:51 +08:00
className="mb-4"
/>
2026-01-25 00:35:42 +08:00
)}
<ArtifactFileList files={files} threadId={threadId} />
</div>
2026-01-15 23:40:21 +08:00
);
2026-02-07 16:14:48 +08:00
} else if (group.type === "assistant:subagent") {
const tasks = new Set<Subtask>();
2026-02-07 16:14:48 +08:00
for (const message of group.messages) {
if (message.type === "ai") {
for (const toolCall of message.tool_calls ?? []) {
if (toolCall.name === "task") {
const task: Subtask = {
2026-02-07 16:14:48 +08:00
id: toolCall.id!,
subagent_type: toolCall.args.subagent_type,
description: toolCall.args.description,
prompt: toolCall.args.prompt,
status: "in_progress",
};
updateSubtask(task);
tasks.add(task);
2026-02-07 16:14:48 +08:00
}
}
} else if (message.type === "tool") {
const taskId = message.tool_call_id;
if (taskId) {
const result = extractTextFromMessage(message);
if (result.startsWith("Task Succeeded. Result:")) {
updateSubtask({
id: taskId,
status: "completed",
result: result
.split("Task Succeeded. Result:")[1]
?.trim(),
});
} else if (result.startsWith("Task failed.")) {
updateSubtask({
id: taskId,
status: "failed",
error: result.split("Task failed.")[1]?.trim(),
});
} else if (result.startsWith("Task timed out")) {
updateSubtask({
id: taskId,
status: "failed",
error: result,
});
2026-02-07 16:14:48 +08:00
} else {
updateSubtask({
id: taskId,
status: "in_progress",
});
}
}
}
}
const results: React.ReactNode[] = [];
for (const message of group.messages.filter(
(message) => message.type === "ai",
)) {
if (hasReasoning(message)) {
results.push(
<MessageGroup
key={"thinking-group-" + message.id}
messages={[message]}
isLoading={thread.isLoading}
/>,
);
}
2026-02-08 23:44:36 +08:00
results.push(
<div
key="subtask-count"
className="text-muted-foreground font-norma pt-2 text-sm"
>
{t.subtasks.executing(tasks.size)}
</div>,
);
2026-02-07 16:14:48 +08:00
const taskIds = message.tool_calls?.map(
(toolCall) => toolCall.id,
);
for (const taskId of taskIds ?? []) {
results.push(
<SubtaskCard
key={"task-group-" + taskId}
taskId={taskId!}
isLoading={thread.isLoading}
/>,
);
}
}
return (
<div
key={"subtask-group-" + group.id}
className="relative z-1 flex flex-col gap-2"
>
{results}
</div>
);
2026-01-18 13:07:56 +08:00
}
return (
<MessageGroup
key={"group-" + group.id}
messages={group.messages}
isLoading={thread.isLoading}
/>
);
})}
2026-01-15 23:40:21 +08:00
{thread.isLoading && <StreamingIndicator className="my-4" />}
2026-01-22 00:26:11 +08:00
<div style={{ height: `${paddingBottom}px` }} />
2026-01-15 23:40:21 +08:00
</ConversationContent>
</Conversation>
);
}