import { useEffect, useMemo, useRef, useState } from "react"; import { ReplyCard } from "@/components/agents/ported/ReplyCard"; import { ShellCommand } from "@/components/agents/ported/ShellCommand"; import { ToolExecution } from "@/components/agents/ported/ToolExecution"; import type { Message } from "@/lib/agents/types"; import { ChunkRenderer } from "./ChunkRenderer"; import { ReasoningBlock } from "./ReasoningBlock"; import { buildRenderItems, summarizeExploration, type RenderItem } from "./renderItems"; import { SubagentGroup } from "@/components/agents/subagents"; import { summarizeChangedFiles } from "./summarizeChangedFiles"; import { TurnChangedFilesCard } from "./TurnChangedFilesCard"; import type { ApprovalCallbacks, ChangedFileSummaryItem } from "./types"; export function AgentMessage({ message, isStreaming, isMarkdownLive, projectPath, ...callbacks }: { message: Message; isStreaming?: boolean; isMarkdownLive?: boolean; projectPath?: string; } & ApprovalCallbacks) { const renderItems = useMemo( () => buildRenderItems(message.chunks, message.id), [message.chunks, message.id], ); const changedFiles = useMemo(() => summarizeChangedFiles(message.chunks), [message.chunks]); const changedFilesTotals = useMemo(() => { let additions = 0; let deletions = 0; for (const item of changedFiles) { additions += item.additions; deletions += item.deletions; } return { additions, deletions }; }, [changedFiles]); const changedFilesByPath = useMemo(() => { const byPath = new Map(); for (const file of changedFiles) { byPath.set(file.filePath, file); } return byPath; }, [changedFiles]); const exploredGroupIds = useMemo( () => renderItems .filter((item): item is Extract => item.type === "explored-group") .map((item) => item.id), [renderItems], ); const hasExploredGroups = exploredGroupIds.length > 0; const [expandedExploredGroups, setExpandedExploredGroups] = useState>({}); const wasExplorationLiveRef = useRef(false); useEffect(() => { const isLive = !!isStreaming; if (!hasExploredGroups) { setExpandedExploredGroups({}); wasExplorationLiveRef.current = isLive; return; } if (isLive) { const next: Record = {}; for (const id of exploredGroupIds) { next[id] = true; } setExpandedExploredGroups(next); wasExplorationLiveRef.current = true; return; } const shouldAutoCollapse = wasExplorationLiveRef.current; setExpandedExploredGroups((prev) => { const next: Record = {}; for (const id of exploredGroupIds) { next[id] = shouldAutoCollapse ? false : (prev[id] ?? false); } const prevKeys = Object.keys(prev); const nextKeys = Object.keys(next); if (prevKeys.length !== nextKeys.length) return next; for (const key of nextKeys) { if (prev[key] !== next[key]) return next; } return prev; }); wasExplorationLiveRef.current = false; }, [hasExploredGroups, isStreaming, exploredGroupIds, message.id]); return (
{renderItems.map((item, index) => { switch (item.type) { case "reasoning-item": { const reasoningChunk = item.chunk.kind === "reasoning" ? item.chunk : null; const isLastItem = index === renderItems.length - 1; return (
); } case "explored-group": { const summary = summarizeExploration(item.chunks); const isExpanded = expandedExploredGroups[item.id] ?? false; return (
{isExpanded && (
{item.chunks.map((chunk, chunkIndex) => (
))}
)}
); } case "subagent-group": return ; case "edit-item": { const fullFileDiff = item.chunk.diffData ? changedFilesByPath.get(item.chunk.diffData.filePath) : undefined; return (
); } case "shell-item": return (
); case "reply-item": return (
); case "tool-item": return (
); case "text-chunk": return (
); } })} {changedFiles.length > 0 && !isStreaming && ( )}
); }