import { OPEN_WORK_PROMPT } from '../../../shared/coding-work-preview'; import { CONTINUE_CODING_PROMPT, type CodingRecoveryAction } from '../../../shared/coding-recovery'; import { CodingRecoveryCard } from './CodingRecoveryCard'; import { memo, type ReactNode, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, } from 'react'; import ReactMarkdown from 'react-markdown'; import remarkGfm from 'remark-gfm'; import { 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'; import { CodingWelcomeHero } from './CodingWelcomeHero'; import { GameAudioFiles } from './GameAudioFiles'; 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 NODE_STATUS_LABELS: Record = { declared: '已声明', waiting: '等待中', running: '执行中', complete: '已完成', error: '失败', aborted: '已中止', queued: '排队中', skipped: '已跳过', }; type TextualBlock = Exclude; type ThinkingBlock = Extract; type CapabilityToolDetails = Extract; function capabilityBillingLabel(billing: CapabilityToolDetails['billing']): string { switch (billing.status) { case 'not_started': return '尚未开始计费'; case 'included': return '已包含'; case 'external': return '由外部账户结算'; case 'reserved': return `已预留 ${billing.reserved_points} Token Point`; case 'dispatched': return `处理中 · 已预留 ${billing.reserved_points} Token Point`; case 'released': return '预留已释放,未收费'; case 'pending_review': return '账单待审核'; case 'expired': return '预留已过期'; case 'settled': return `已结算 ${billing.actual_points} Token Point`; case 'refunded': return `已退款 ${billing.actual_points} Token Point`; case 'receipt_unavailable': return '收费状态未同步,请勿重复发起'; } } function gameResourceProgressLabel(details: CapabilityToolDetails): string | null { if (!['makelore.game-resource', 'makelore.game-audio'].includes(details.plugin_id) || !['generate', 'preview'].includes(details.operation) || !details.data || typeof details.data !== 'object' || Array.isArray(details.data)) return null; const data = details.data as Record; const label = details.plugin_id === 'makelore.game-audio' ? '游戏音频' : '游戏资源'; const phase = typeof data.phase === 'string' ? data.phase : null; const deliveryStatus = typeof data.deliveryStatus === 'string' ? data.deliveryStatus : null; const providerStatus = typeof data.providerStatus === 'string' ? data.providerStatus : null; const outputCount = Number.isSafeInteger(data.outputCount) ? data.outputCount as number : 0; if (phase === 'saved' || deliveryStatus === 'saved') { return `${label} · 已保存 ${outputCount} 个文件`; } if (phase === 'saving' || deliveryStatus === 'saving') return label + ' · 正在保存到项目'; if (deliveryStatus === 'delivery_failed') return label + ' · 自动保存失败,等待恢复'; if (deliveryStatus === 'needs_reconciliation') return label + ' · 等待原任务状态恢复,请勿重复生成'; if (phase === 'submitted') return label + ' · 已提交'; if (phase === 'generating' || ['reserved', 'accepted', 'running'].includes(providerStatus ?? '')) { return label + ' · 正在生成'; } if (providerStatus === 'pending_review') return label + ' · 账单待审核'; if (providerStatus === 'submission_unknown') return label + ' · 提交状态待确认'; if (providerStatus === 'cancelled') return label + ' · 已取消'; if (providerStatus === 'failed') return label + ' · 生成失败'; return null; } 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]); if (user && (answerText === OPEN_WORK_PROMPT || answerText === CONTINUE_CODING_PROMPT)) { return null; } 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}

    )}
    ); } if (details.schema === 'makelore-capability.v1') { const gameResourceProgress = gameResourceProgressLabel(details); return (

    {gameResourceProgress ?? `插件能力 · ${details.operation}`}

    {details.success ? '成功' : details.error ?? '请求失败'} · HTTP {details.status}

    {capabilityBillingLabel(details.billing)}

    {details.plugin_id === 'makelore.game-audio' && }
    ); } if (details.schema === 'makelore-model-tool.v1') { return (

    模型联网搜索 · {details.modelId}

    {details.status === 'succeeded' ? (

    {details.sources.length > 0 ? `已返回 ${details.sources.length} 个来源` : '提供方未返回结构化来源'}

    ) : (

    {details.error.code} · {details.error.message}

    )}
    ); } if (details.schema === 'makelore-device-package.v1') { const subject = details.preview?.displayName ?? details.index?.packages.at(-1)?.displayName ?? '本机包'; return (

    本机插件 · {subject}

    {details.operation === 'prepare' ? `等待确认${details.preview?.includesExecutableCode ? ' · 包含可执行扩展' : ''}` : `操作完成 · 共 ${details.index?.packages.length ?? 0} 个本机包`}

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