import type { Message } from "@langchain/langgraph-sdk"; import type { BaseStream } from "@langchain/langgraph-sdk/react"; import { ChevronUpIcon, Loader2Icon } from "lucide-react"; import { useCallback, useEffect, useMemo, useRef } from "react"; import { Conversation, ConversationContent, } from "@/components/ai-elements/conversation"; import { Button } from "@/components/ui/button"; import { useI18n } from "@/core/i18n/hooks"; import { buildTokenDebugSteps, type TokenUsageInlineMode, } from "@/core/messages/usage-model"; import { extractContentFromMessage, extractPresentFilesFromMessage, extractTextFromMessage, getAssistantTurnCopyData, getAssistantTurnUsageMessages, getMessageGroups, getStreamingMessageLookup, hasContent, hasPresentFiles, hasReasoning, isAssistantMessageGroupStreaming, } from "@/core/messages/utils"; import { useRehypeSplitWordsIntoSpans } from "@/core/rehype"; import type { Subtask } from "@/core/tasks"; import { useUpdateSubtask } from "@/core/tasks/context"; import { parseSubtaskResult } from "@/core/tasks/subtask-result"; import type { AgentThreadState } from "@/core/threads"; import { cn } from "@/lib/utils"; import { ArtifactFileList } from "../artifacts/artifact-file-list"; import { CopyButton } from "../copy-button"; import { StreamingIndicator } from "../streaming-indicator"; import { MarkdownContent } from "./markdown-content"; import { MessageGroup } from "./message-group"; import { MessageListItem } from "./message-list-item"; import { MessageTokenUsageDebugList, MessageTokenUsageList, } from "./message-token-usage"; import { MessageListSkeleton } from "./skeleton"; import { SubtaskCard } from "./subtask-card"; export const MESSAGE_LIST_DEFAULT_PADDING_BOTTOM = 24; const LOAD_MORE_HISTORY_THROTTLE_MS = 1200; function LoadMoreHistoryIndicator({ isLoading, hasMore, loadMore, }: { isLoading?: boolean; hasMore?: boolean; loadMore?: () => void; }) { const { t } = useI18n(); const sentinelRef = useRef(null); const timeoutRef = useRef | null>(null); const lastLoadRef = useRef(0); const throttledLoadMore = useCallback(() => { if (!hasMore || isLoading) { return; } const now = Date.now(); const remaining = LOAD_MORE_HISTORY_THROTTLE_MS - (now - lastLoadRef.current); if (remaining <= 0) { lastLoadRef.current = now; loadMore?.(); return; } if (timeoutRef.current) { return; } timeoutRef.current = setTimeout(() => { timeoutRef.current = null; if (!hasMore || isLoading) { return; } lastLoadRef.current = Date.now(); loadMore?.(); }, remaining); }, [hasMore, isLoading, loadMore]); useEffect(() => { const element = sentinelRef.current; if (!element || !hasMore) { return; } const observer = new IntersectionObserver( ([entry]) => { if (entry?.isIntersecting) { throttledLoadMore(); } }, { rootMargin: "120px 0px 0px 0px", }, ); observer.observe(element); return () => { observer.disconnect(); }; }, [hasMore, throttledLoadMore]); useEffect(() => { return () => { if (timeoutRef.current) { clearTimeout(timeoutRef.current); } }; }, []); if (!hasMore && !isLoading) { return null; } return (
); } export function MessageList({ className, threadId, thread, paddingBottom = MESSAGE_LIST_DEFAULT_PADDING_BOTTOM, tokenUsageInlineMode = "off", hasMoreHistory, loadMoreHistory, isHistoryLoading, }: { className?: string; threadId: string; thread: BaseStream; paddingBottom?: number; tokenUsageInlineMode?: TokenUsageInlineMode; hasMoreHistory?: boolean; loadMoreHistory?: () => void; isHistoryLoading?: boolean; }) { const { t } = useI18n(); const rehypePlugins = useRehypeSplitWordsIntoSpans(thread.isLoading); const updateSubtask = useUpdateSubtask(); const messages = thread.messages; const groupedMessages = getMessageGroups(messages); const turnUsageMessagesByGroupIndex = getAssistantTurnUsageMessages(groupedMessages); const tokenDebugSteps = useMemo( () => buildTokenDebugSteps(messages, t), [messages, t], ); const streamingMessages = useMemo( () => getStreamingMessageLookup( messages, thread.isLoading, thread.getMessagesMetadata, ), [messages, thread.getMessagesMetadata, thread.isLoading], ); const renderAssistantCopyButton = useCallback( (messages: Message[], isStreaming: boolean) => { const clipboardData = getAssistantTurnCopyData(messages, { isStreaming }); if (!clipboardData) { return null; } return (
); }, [], ); const renderTokenUsage = useCallback( ({ messages, turnUsageMessages, inlineDebug = true, debugMessageIds, }: { messages: Message[]; turnUsageMessages?: Message[] | null; inlineDebug?: boolean; debugMessageIds?: string[]; }) => { if (tokenUsageInlineMode === "per_turn") { return ( ); } if (tokenUsageInlineMode === "step_debug" && inlineDebug) { const messageIds = new Set( debugMessageIds ?? messages .filter((message) => message.type === "ai") .map((message) => message.id) .filter((id): id is string => typeof id === "string"), ); return ( messageIds.has(step.messageId), )} /> ); } return null; }, [thread.isLoading, tokenDebugSteps, tokenUsageInlineMode], ); if (thread.isThreadLoading && messages.length === 0) { return ; } return ( {groupedMessages.map((group, groupIndex) => { const turnUsageMessages = turnUsageMessagesByGroupIndex[groupIndex]; if (group.type === "human" || group.type === "assistant") { return (
{group.messages.map((msg) => { return ( ); })} {renderTokenUsage({ messages: group.messages, turnUsageMessages, })} {group.type === "assistant" && renderAssistantCopyButton( group.messages, isAssistantMessageGroupStreaming( group.messages, streamingMessages, ), )}
); } else if (group.type === "assistant:clarification") { const message = group.messages[0]; if (message && hasContent(message)) { return (
{renderTokenUsage({ messages: group.messages, turnUsageMessages, })}
); } return null; } else if (group.type === "assistant:present-files") { const files: string[] = []; for (const message of group.messages) { if (hasPresentFiles(message)) { const presentFiles = extractPresentFilesFromMessage(message); files.push(...presentFiles); } } return (
{group.messages[0] && hasContent(group.messages[0]) && ( )} {renderTokenUsage({ messages: group.messages, turnUsageMessages, })}
); } else if (group.type === "assistant:subagent") { const tasks = new Set(); for (const message of group.messages) { if (message.type === "ai") { for (const toolCall of message.tool_calls ?? []) { if (toolCall.name === "task") { const task: Subtask = { 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); } } } else if (message.type === "tool") { const taskId = message.tool_call_id; if (taskId) { const parsed = parseSubtaskResult( extractTextFromMessage(message), ); updateSubtask({ id: taskId, ...parsed }); } } } const results: React.ReactNode[] = []; const subagentDebugMessageIds: string[] = []; if (tasks.size > 0) { results.push(
{t.subtasks.executing(tasks.size)}
, ); } for (const message of group.messages.filter( (message) => message.type === "ai", )) { if (hasReasoning(message)) { results.push( step.messageId === message.id, )} showTokenDebugSummaries={ tokenUsageInlineMode === "step_debug" } />, ); } else if (message.id) { subagentDebugMessageIds.push(message.id); } const taskIds = message.tool_calls ?.filter((toolCall) => toolCall.name === "task") .map((toolCall) => toolCall.id); for (const taskId of taskIds ?? []) { results.push( , ); } } return (
{results} {renderTokenUsage({ messages: group.messages, turnUsageMessages, debugMessageIds: subagentDebugMessageIds, })}
); } return (
group.messages.some( (message) => message.id === step.messageId, ), )} showTokenDebugSummaries={tokenUsageInlineMode === "step_debug"} /> {renderTokenUsage({ messages: group.messages, turnUsageMessages, inlineDebug: false, })}
); })} {thread.isLoading && }
); }