import { memo, type ReactNode, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, } from 'react'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import { Bot, BookOpen, Check, CheckCircle2, ChevronDown, CircleAlert, Copy, ExternalLink, GitFork, Globe2, Hammer, Layers3, ListChecks, LoaderCircle, Pencil, RotateCcw, Search, ShieldCheck, Terminal, Workflow, Wrench, XCircle, } from 'lucide-react'; import { toast } from 'sonner'; import { Button } from '@/components/ui/button'; import { openConversationExternalUrl, openConversationLocalWebFile, openableConversationLocalWebPath, resolveConversationMarkdownWebPath, safeConversationExternalUrl, showConversationLinkContextMenu, } from '@/lib/conversation-links'; import { cn } from '@/lib/utils'; import { selectCodingConversationSnapshot, useCodingConversationStore, type CodingConversationStoreState, } from '@/stores/coding-conversations'; import type { ConversationBoundaryNode, ConversationCompactionNode, ConversationContentBlock, ConversationMessageNode, ConversationNode, ConversationNoticeNode, ConversationRunState, ConversationSubagentNode, ConversationToolNode, KnownToolDetails, SubagentDetailsV1, } from '../../../shared/coding-conversation-contracts'; import { CodingAttachmentPreview } from './CodingAttachmentPreview'; const EMPTY_NODES: ConversationNode[] = []; const IDLE_RUN: ConversationRunState = { status: 'idle' }; const INITIAL_WINDOW = 120; const WINDOW_STEP = 100; const LONG_OUTPUT_CHARS = 4_000; const STREAMING_THINKING_WINDOW_CHARS = 16_000; const PROGRESS_PREVIEW_MAX_CHARS = 360; const PROGRESS_ROLL_CHAR_STEP = 6; const NODE_STATUS_LABELS: Record = { declared: '已声明', waiting: '等待中', running: '执行中', complete: '已完成', error: '失败', aborted: '已中止', queued: '排队中', skipped: '已跳过', }; type TextualBlock = Exclude; type ThinkingBlock = Extract; type ProcessItem = | { kind: 'thinking'; id: string; block: ThinkingBlock } | { kind: 'assistant-commentary'; id: string; node: ConversationMessageNode; blocks: ConversationContentBlock[] } | { kind: 'tool'; id: string; node: ConversationToolNode } | { kind: 'compaction'; id: string; node: ConversationCompactionNode } | { kind: 'retry'; id: string; node: ConversationBoundaryNode } | { kind: 'subagent'; id: string; node: ConversationSubagentNode } | { kind: 'notice'; id: string; node: ConversationNoticeNode }; interface TimelineTurn { id: string; user?: ConversationMessageNode; process: ProcessItem[]; final?: ConversationMessageNode; } function statusLabel(status: string): string { return NODE_STATUS_LABELS[status] ?? status; } const MarkdownText = memo(function MarkdownText({ text, compact = false, muted = false, }: { text: string; compact?: boolean; muted?: boolean; }) { const openExternal = useCallback((href: string) => { void openConversationExternalUrl(href) .catch(() => toast.error('链接无法打开')); }, []); const openLocalWebFile = useCallback((path: string) => { void openConversationLocalWebFile(path) .catch(() => toast.error('本地网页无法打开')); }, []); const showLinkContextMenu = useCallback(( request: Parameters[0], ) => { void showConversationLinkContextMenu(request) .catch(() => toast.error('链接菜单无法打开')); }, []); return (
(

{children}

), h2: ({ children }) => (

{children}

), h3: ({ children }) => (

{children}

), h4: ({ children }) => (

{children}

), p: ({ children }) => (

{children}

), a: ({ href, children }) => { const safeHref = safeConversationExternalUrl(href); if (!safeHref) return {children}; return ( { event.preventDefault(); openExternal(safeHref); }} onContextMenu={(event) => { event.preventDefault(); showLinkContextMenu({ kind: 'external', target: safeHref }); }} > {children} ); }, strong: ({ children }) => ( {children} ), ul: ({ children }) => (
    {children}
), ol: ({ children }) => (
    {children}
), li: ({ children }) =>
  • {children}
  • , blockquote: ({ children }) => (
    {children}
    ), hr: () =>
    , code: ({ children, className }) => { const rawCode = String(children); const codeText = rawCode.replace(/\n$/, ''); const blockCode = rawCode.endsWith('\n') || Boolean(className?.startsWith('language-')); const inlineUrl = blockCode ? null : safeConversationExternalUrl(codeText); const localWebPath = blockCode ? null : openableConversationLocalWebPath(codeText); const resolvedWebPath = blockCode || localWebPath ? null : resolveConversationMarkdownWebPath(codeText, text); if (inlineUrl) { return ( { event.preventDefault(); openExternal(inlineUrl); }} onContextMenu={(event) => { event.preventDefault(); showLinkContextMenu({ kind: 'external', target: inlineUrl }); }} > {codeText} ); } if (resolvedWebPath) { return ( ); } if (localWebPath) { return ( ); } return ( {children} ); }, pre: ({ children }) => (
                  {children}
                
    ), table: ({ children }) => (
    {children}
    ), thead: ({ children }) => {children}, tbody: ({ children }) => {children}, th: ({ children }) => ( {children} ), td: ({ children }) => ( {children} ), img: ({ src, alt }) => typeof src === 'string' ? ( {alt ) : null, }} > {text}
    ); }); const ContentBlock = memo(function ContentBlock({ block, markdown, compactMarkdown = false, muted = false, }: { block: ConversationContentBlock; markdown: boolean; compactMarkdown?: boolean; muted?: boolean; }) { if (block.kind === 'image') { return ; } if (!markdown) { return (

    {block.text} {block.status === 'streaming' && ( )}

    ); } return (
    {block.status === 'streaming' && ( )}
    ); }); function answerBlocks(node: ConversationMessageNode): ConversationContentBlock[] { return node.blocks.filter((block) => block.kind !== 'thinking'); } function hasVisibleAnswer(node: ConversationMessageNode): boolean { return answerBlocks(node).some((block) => ( block.kind === 'image' || block.text.trim().length > 0 || block.status === 'streaming' )); } const MessageNode = memo(function MessageNode({ node, onFork, assistantName, }: { node: ConversationMessageNode; onFork?(sourceEntryId: string): void; assistantName?: string; }) { const user = node.role === 'user'; const blocks = user ? node.blocks : answerBlocks(node); const answerText = blocks .filter((block): block is TextualBlock => block.kind === 'text') .map((block) => block.text) .join('\n\n') .trim(); const [copied, setCopied] = useState(false); const copyAnswer = useCallback(() => { if (!answerText || !navigator.clipboard) return; void navigator.clipboard.writeText(answerText).then(() => { setCopied(true); window.setTimeout(() => setCopied(false), 1_600); }).catch(() => undefined); }, [answerText]); return (
    {blocks.map((block) => ( ))} {node.status === 'optimistic' &&

    正在提交…

    } {node.status === 'error' && (

    {user ? '本次提交未被接受,内容已保留在输入框。' : '本次回复失败。'}

    )} {node.status === 'aborted' &&

    本次处理已中止。

    } {user && node.sourceEntryId && onFork && node.status !== 'optimistic' && ( )}
    {!user && answerText && ( )}
    ); }); function subagentModeLabel(mode: SubagentDetailsV1['mode']): string { if (mode === 'parallel') return '并行子任务'; if (mode === 'chain') return '串行子任务'; return '单个子任务'; } function subagentStatus(details: SubagentDetailsV1): 'running' | 'error' | 'complete' { if (details.tasks.some((task) => task.status === 'queued' || task.status === 'running')) return 'running'; if (details.tasks.some((task) => task.status === 'error')) return 'error'; return 'complete'; } const SubagentGraph = memo(function SubagentGraph({ details }: { details: SubagentDetailsV1 }) { return (
    {subagentModeLabel(details.mode)} {details.dispatchId}
    {details.tasks.map((task) => (

    {task.agentId}

    {statusLabel(task.status)}

    {task.toolProfile === 'coding' ? '可写代码' : '只读'}

    {task.summary &&

    {task.summary}

    } {task.errorCode &&

    {task.errorCode}

    }
    ))}
    ); }); const ToolDetails = memo(function ToolDetails({ details }: { details: KnownToolDetails }) { if (details.schema === 'changed-file.v1') { return (

    本轮变更文件

    {details.paths.map((path) =>

    {path}

    )}
    ); } if (details.schema === 'task-state.v1') { return (

    {details.tasks.map((task) => (

    {task.title}{statusLabel(task.status)}

    ))}
    ); } if (details.schema === 'write-lease.v1') { return

    ; } if (details.schema === 'agent-browser.v1') { return (

    {details.attachmentId && details.mime && ( )}
    ); } if (details.schema === 'game-assets.v1') { return (

    素材确认 · {details.status === 'pending' ? '等待处理' : '已完成'}

    待确认 {details.pendingAssetIds.length} · 已采用 {details.approvedAssetIds.length} · 已丢弃 {details.discardedAssetIds.length}

    ); } if (details.schema === 'runtime-context.v1') { return (

    技能

    {details.skills.map((skill) =>

    {skill.selected ? '已启用 · ' : ''}{skill.name}

    )}

    命令

    {details.commands.map((command) =>

    /{command.name}

    )}
    ); } return ; }); function toolStatusIsActive(status: ConversationToolNode['status']): boolean { return status === 'declared' || status === 'waiting' || status === 'running'; } function toolStatusIcon(node: ConversationToolNode, active: boolean) { if (active) { return