2026-02-09 21:40:20 +08:00
|
|
|
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";
|
2026-01-16 09:13:02 +08:00
|
|
|
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";
|
|
|
|
|
|
2026-02-09 21:40:20 +08:00
|
|
|
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,
|
2026-03-02 20:49:41 +08:00
|
|
|
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;
|
2026-01-16 09:13:02 +08:00
|
|
|
thread: UseStream<AgentThreadState>;
|
2026-03-02 20:49:41 +08:00
|
|
|
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">
|
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") {
|
2026-01-29 01:25:05 +08:00
|
|
|
const message = group.messages[0];
|
|
|
|
|
if (message && hasContent(message)) {
|
|
|
|
|
return (
|
2026-02-09 16:24:01 +08:00
|
|
|
<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}
|
|
|
|
|
/>
|
2026-01-29 01:25:05 +08:00
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
return null;
|
2026-02-07 16:14:48 +08:00
|
|
|
} else if (group.type === "assistant:present-files") {
|
2026-01-23 13:24:03 +08:00
|
|
|
const files: string[] = [];
|
2026-01-18 13:07:56 +08:00
|
|
|
for (const message of group.messages) {
|
|
|
|
|
if (hasPresentFiles(message)) {
|
2026-01-23 13:24:03 +08:00
|
|
|
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]) && (
|
2026-02-09 16:24:01 +08:00
|
|
|
<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") {
|
2026-02-07 18:42:24 +08:00
|
|
|
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") {
|
2026-02-07 18:42:24 +08:00
|
|
|
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",
|
2026-02-07 18:42:24 +08:00
|
|
|
};
|
|
|
|
|
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(),
|
|
|
|
|
});
|
2026-02-07 18:06:22 +08:00
|
|
|
} 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>
|
|
|
|
|
);
|
|
|
|
|
}
|