{ "$schema": "https://ui.shadcn.com/schema/registry-item.json", "name": "agent-message", "title": "Agent Message", "description": "AI SDK UIMessage presenter for streaming text, provider reasoning, multimodal files, sources, steps, tools, actions, metadata, and usage", "dependencies": ["ai@7.0.31", "daisyui", "lucide-react", "streamdown@2.5.0"], "registryDependencies": ["https://ui-components.wener.me/r/message.json"], "files": [ { "path": "registry/default/ui/agent-message/agent-message-types.ts", "content": "import type { UIMessage } from 'ai';\nimport type { ComponentType, ReactNode } from 'react';\n\nexport type AgentMessagePart = UIMessage['parts'][number];\n\nexport type AgentMessageMessages = {\n\tassistantRole: string;\n\taudioAttachment: string;\n\tdocumentSource: string;\n\tfileAttachment: string;\n\timageAttachment: string;\n\tmalformedPart: string;\n\topenSource: string;\n\treasoning: string;\n\treasoningStreaming: string;\n\tstep: (step: number) => string;\n\tsystemRole: string;\n\ttoolApprovalReason: string;\n\ttoolInput: string;\n\ttoolOutput: string;\n\ttoolState: (state: string, approved?: boolean) => string;\n\tunknownPart: (type: string) => string;\n\tunnamedAttachment: string;\n\tunnamedSource: string;\n\tunnamedTool: string;\n\tuserRole: string;\n};\n\nexport const defaultAgentMessageMessages: AgentMessageMessages = {\n\tassistantRole: '助手',\n\taudioAttachment: '音频附件',\n\tdocumentSource: '文档来源',\n\tfileAttachment: '文件附件',\n\timageAttachment: '图片附件',\n\tmalformedPart: '无法显示的消息内容',\n\topenSource: '打开来源',\n\treasoning: '提供方推理摘要',\n\treasoningStreaming: '提供方正在生成推理摘要',\n\tstep: (step) => `步骤 ${step}`,\n\tsystemRole: '系统',\n\ttoolApprovalReason: '审批说明',\n\ttoolInput: '输入',\n\ttoolOutput: '输出',\n\ttoolState: (state, approved) => {\n\t\tswitch (state) {\n\t\t\tcase 'input-streaming':\n\t\t\t\treturn '正在接收参数';\n\t\t\tcase 'input-available':\n\t\t\t\treturn '等待执行';\n\t\t\tcase 'approval-requested':\n\t\t\t\treturn '等待批准';\n\t\t\tcase 'approval-responded':\n\t\t\t\treturn approved === false ? '已拒绝' : '已批准';\n\t\t\tcase 'output-available':\n\t\t\t\treturn '已完成';\n\t\t\tcase 'output-error':\n\t\t\t\treturn '执行失败';\n\t\t\tcase 'output-denied':\n\t\t\t\treturn '已拒绝';\n\t\t\tdefault:\n\t\t\t\treturn '未知状态';\n\t\t}\n\t},\n\tunknownPart: (type) => `暂不支持的消息内容:${type}`,\n\tunnamedAttachment: '附件',\n\tunnamedSource: '来源',\n\tunnamedTool: '工具',\n\tuserRole: '你',\n};\n\nexport function resolveAgentMessageMessages(messages?: Partial): AgentMessageMessages {\n\treturn { ...defaultAgentMessageMessages, ...messages };\n}\n\nexport type AgentMessageTextRendererProps = {\n\tmessage: UIMessage;\n\tpart: AgentMessagePart;\n\tstreaming: boolean;\n\ttext: string;\n};\n\nexport type AgentMessageTextRenderer = ComponentType;\n\nexport type AgentMessagePartRenderContext = {\n\tindex: number;\n\tmessage: UIMessage;\n\tpart: AgentMessagePart;\n\tstep: number;\n};\n\nexport type AgentMessagePartRenderer = (context: AgentMessagePartRenderContext) => ReactNode | undefined;\n\nexport type AgentMessageReasoningLabel = (context: {\n\tmessage: UIMessage;\n\tpart: AgentMessagePart;\n\tstreaming: boolean;\n}) => ReactNode;\n", "type": "registry:lib", "target": "@components/agent-message/agent-message-types.ts" }, { "path": "registry/default/ui/agent-message/agent-message-value.ts", "content": "export type AgentMessageValueRecord = Record;\n\nexport type AgentMessageValueProjectionLimits = {\n\tmaxArrayItems?: number;\n\tmaxDepth?: number;\n\tmaxNodes?: number;\n\tmaxObjectKeys?: number;\n\tmaxStringLength?: number;\n};\n\ntype ResolvedProjectionLimits = Required;\ntype ProjectionState = {\n\tancestors: WeakSet;\n\tlimits: ResolvedProjectionLimits;\n\tnodes: number;\n};\n\nconst DEFAULT_PROJECTION_LIMITS: ResolvedProjectionLimits = {\n\tmaxArrayItems: 32,\n\tmaxDepth: 6,\n\tmaxNodes: 256,\n\tmaxObjectKeys: 32,\n\tmaxStringLength: 1_024,\n};\nconst TRUNCATED = '[已截断]';\nconst UNREADABLE = '[无法读取]';\n\nexport function isAgentMessageRecord(value: unknown): value is AgentMessageValueRecord {\n\treturn typeof value === 'object' && value !== null;\n}\n\nexport function readAgentMessageValue(value: unknown, key: string): unknown {\n\tif (!isAgentMessageRecord(value)) return undefined;\n\ttry {\n\t\treturn value[key];\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nexport function readAgentMessageString(value: unknown, key: string): string | undefined {\n\tconst field = readAgentMessageValue(value, key);\n\treturn typeof field === 'string' ? field : undefined;\n}\n\nexport function projectAgentMessageValue(value: unknown, limits: AgentMessageValueProjectionLimits = {}): unknown {\n\tconst resolved = resolveProjectionLimits(limits);\n\treturn projectValue(value, 0, { ancestors: new WeakSet(), limits: resolved, nodes: 0 });\n}\n\nexport function formatAgentMessageValue(value: unknown, maxLength = 4_096): string {\n\tconst outputLimit = boundedLimit(maxLength, 4_096);\n\tconst projection = projectAgentMessageValue(value, {\n\t\tmaxStringLength: Math.min(DEFAULT_PROJECTION_LIMITS.maxStringLength, Math.max(0, outputLimit)),\n\t});\n\ttry {\n\t\tconst formatted = typeof projection === 'string' ? projection : JSON.stringify(projection, null, 2);\n\t\treturn truncate(formatted ?? UNREADABLE, outputLimit);\n\t} catch {\n\t\treturn '[无法格式化]';\n\t}\n}\n\nexport function resolveAgentMessageHref(value: unknown, kind: 'file' | 'source'): string | undefined {\n\tconst maxLength = kind === 'file' ? 32 * 1024 * 1024 : 8 * 1024;\n\tif (typeof value !== 'string' || value.length > maxLength) return undefined;\n\ttry {\n\t\tconst url = new URL(value);\n\t\tif (url.protocol === 'https:' || url.protocol === 'http:') return value;\n\t\tif (kind === 'file' && (url.protocol === 'blob:' || url.protocol === 'data:')) return value;\n\t} catch {\n\t\treturn undefined;\n\t}\n\treturn undefined;\n}\n\nfunction projectValue(value: unknown, depth: number, state: ProjectionState): unknown {\n\tif (state.nodes >= state.limits.maxNodes) return TRUNCATED;\n\tstate.nodes += 1;\n\tif (typeof value === 'string') return truncate(value, state.limits.maxStringLength);\n\tif (value === undefined) return '[undefined]';\n\tif (value === null || typeof value === 'number' || typeof value === 'boolean') return value;\n\tif (typeof value === 'bigint') return `${value.toString()}n`;\n\tif (typeof value === 'symbol') return `[Symbol${value.description ? `: ${truncate(value.description, 64)}` : ''}]`;\n\tif (typeof value === 'function') return '[Function]';\n\tif (typeof value !== 'object') return UNREADABLE;\n\tif (depth >= state.limits.maxDepth) return TRUNCATED;\n\tif (state.ancestors.has(value)) return '[Circular]';\n\tstate.ancestors.add(value);\n\ttry {\n\t\treturn Array.isArray(value) ? projectArray(value, depth, state) : projectObject(value, depth, state);\n\t} catch {\n\t\treturn UNREADABLE;\n\t} finally {\n\t\tstate.ancestors.delete(value);\n\t}\n}\n\nfunction projectArray(value: unknown[], depth: number, state: ProjectionState): unknown[] {\n\tconst lengthDescriptor = safeOwnDescriptor(value, 'length');\n\tif (!lengthDescriptor || !('value' in lengthDescriptor) || typeof lengthDescriptor.value !== 'number') {\n\t\treturn [UNREADABLE];\n\t}\n\tconst length = Math.max(0, Math.floor(lengthDescriptor.value));\n\tconst count = Math.min(length, state.limits.maxArrayItems);\n\tconst output: unknown[] = [];\n\tfor (let index = 0; index < count && state.nodes < state.limits.maxNodes; index += 1) {\n\t\tconst descriptor = safeOwnDescriptor(value, String(index));\n\t\toutput.push(projectDescriptor(descriptor, depth + 1, state));\n\t}\n\tif (length > count) output.push(`${TRUNCATED} ${length - count} 项`);\n\treturn output;\n}\n\nfunction projectObject(value: object, depth: number, state: ProjectionState): AgentMessageValueRecord {\n\tconst output = Object.create(null) as AgentMessageValueRecord;\n\tlet inspected = 0;\n\tlet projected = 0;\n\tlet truncated = false;\n\ttry {\n\t\tfor (const key in value) {\n\t\t\tinspected += 1;\n\t\t\tif (inspected > state.limits.maxObjectKeys || state.nodes >= state.limits.maxNodes) {\n\t\t\t\ttruncated = true;\n\t\t\t\tbreak;\n\t\t\t}\n\t\t\tlet own = false;\n\t\t\ttry {\n\t\t\t\town = Object.hasOwn(value, key);\n\t\t\t} catch {\n\t\t\t\tdefineProjection(output, truncate(key, 256), UNREADABLE);\n\t\t\t\tprojected += 1;\n\t\t\t\tcontinue;\n\t\t\t}\n\t\t\tif (!own) continue;\n\t\t\tconst safeKey = truncate(key, 256);\n\t\t\tdefineProjection(output, safeKey, projectDescriptor(safeOwnDescriptor(value, key), depth + 1, state));\n\t\t\tprojected += 1;\n\t\t}\n\t} catch {\n\t\tdefineProjection(output, '[读取错误]', UNREADABLE);\n\t}\n\tif (truncated || projected >= state.limits.maxObjectKeys) defineProjection(output, '[更多字段]', TRUNCATED);\n\treturn output;\n}\n\nfunction projectDescriptor(descriptor: PropertyDescriptor | undefined, depth: number, state: ProjectionState): unknown {\n\tif (!descriptor) return '[缺失]';\n\tif (!('value' in descriptor)) return '[Accessor]';\n\treturn projectValue(descriptor.value, depth, state);\n}\n\nfunction safeOwnDescriptor(value: object, key: PropertyKey): PropertyDescriptor | undefined {\n\ttry {\n\t\treturn Object.getOwnPropertyDescriptor(value, key);\n\t} catch {\n\t\treturn undefined;\n\t}\n}\n\nfunction defineProjection(target: AgentMessageValueRecord, key: string, value: unknown) {\n\tObject.defineProperty(target, key, { configurable: true, enumerable: true, value, writable: true });\n}\n\nfunction resolveProjectionLimits(limits: AgentMessageValueProjectionLimits): ResolvedProjectionLimits {\n\treturn {\n\t\tmaxArrayItems: boundedLimit(limits.maxArrayItems, DEFAULT_PROJECTION_LIMITS.maxArrayItems),\n\t\tmaxDepth: boundedLimit(limits.maxDepth, DEFAULT_PROJECTION_LIMITS.maxDepth),\n\t\tmaxNodes: boundedLimit(limits.maxNodes, DEFAULT_PROJECTION_LIMITS.maxNodes),\n\t\tmaxObjectKeys: boundedLimit(limits.maxObjectKeys, DEFAULT_PROJECTION_LIMITS.maxObjectKeys),\n\t\tmaxStringLength: boundedLimit(limits.maxStringLength, DEFAULT_PROJECTION_LIMITS.maxStringLength),\n\t};\n}\n\nfunction boundedLimit(value: number | undefined, fallback: number): number {\n\treturn value === undefined || !Number.isFinite(value) ? fallback : Math.max(0, Math.floor(value));\n}\n\nfunction truncate(value: string, maxLength: number): string {\n\tconst limit = boundedLimit(maxLength, 4_096);\n\treturn value.length <= limit ? value : `${value.slice(0, Math.max(0, limit - 1))}…`;\n}\n", "type": "registry:lib", "target": "@components/agent-message/agent-message-value.ts" }, { "path": "registry/default/ui/agent-message/agent-markdown.tsx", "content": "'use client';\n\nimport type { ComponentPropsWithRef } from 'react';\nimport { Streamdown } from 'streamdown';\nimport { cn } from '@/lib/utils';\nimport type { AgentMessageTextRendererProps } from './agent-message-types';\n\nexport type AgentMarkdownProps = Omit, 'children'> & {\n\tchildren: string;\n\tstreaming?: boolean;\n};\n\nexport function AgentMarkdown({ children, className, streaming = false, ...props }: AgentMarkdownProps) {\n\treturn (\n\t\t\n\t\t\t{children}\n\t\t\n\t);\n}\n\nexport function AgentMessageDefaultTextRenderer({ text, streaming }: AgentMessageTextRendererProps) {\n\treturn {text};\n}\n", "type": "registry:component", "target": "@components/agent-message/agent-markdown.tsx" }, { "path": "registry/default/ui/agent-message/agent-message-parts.tsx", "content": "'use client';\n\nimport { ExternalLink, FileText, Image as ImageIcon, Music2, Wrench } from 'lucide-react';\nimport type { ComponentPropsWithRef, ReactNode } from 'react';\nimport { cn } from '@/lib/utils';\nimport { AgentMessageDefaultTextRenderer } from './agent-markdown';\nimport type {\n\tAgentMessageMessages,\n\tAgentMessagePart,\n\tAgentMessageReasoningLabel,\n\tAgentMessageTextRenderer,\n} from './agent-message-types';\nimport {\n\tformatAgentMessageValue,\n\tisAgentMessageRecord,\n\treadAgentMessageString,\n\treadAgentMessageValue,\n\tresolveAgentMessageHref,\n} from './agent-message-value';\n\nexport type AgentMessagePartPresenterProps = Omit, 'part'> & {\n\tindex: number;\n\tmessage: import('ai').UIMessage;\n\tmessages: AgentMessageMessages;\n\tpart: unknown;\n\treasoningLabel?: AgentMessageReasoningLabel;\n\trole: import('ai').UIMessage['role'];\n\tstep: number;\n\ttextRenderer?: AgentMessageTextRenderer;\n};\n\nexport function AgentMessagePartPresenter({\n\tclassName,\n\tindex,\n\tmessage,\n\tmessages,\n\tpart,\n\treasoningLabel,\n\trole,\n\tstep,\n\ttextRenderer: TextRenderer = AgentMessageDefaultTextRenderer,\n\t...props\n}: AgentMessagePartPresenterProps) {\n\tconst type = readAgentMessageString(part, 'type');\n\tlet content: ReactNode;\n\n\tif (!type) {\n\t\tcontent = ;\n\t} else if (type === 'text') {\n\t\tconst text = readAgentMessageString(part, 'text');\n\t\tcontent =\n\t\t\ttext === undefined ? (\n\t\t\t\t\n\t\t\t) : (\n\t\t\t\t\n\t\t\t);\n\t} else if (type === 'reasoning') {\n\t\tcontent = renderReasoning({ message, messages, part, reasoningLabel, TextRenderer });\n\t} else if (type === 'file' || type === 'reasoning-file') {\n\t\tcontent = ;\n\t} else if (type === 'source-url' || type === 'source-document') {\n\t\tcontent = ;\n\t} else if (type === 'step-start') {\n\t\tcontent = ;\n\t} else if (type === 'dynamic-tool' || type.startsWith('tool-')) {\n\t\tcontent = ;\n\t} else {\n\t\tcontent = ;\n\t}\n\n\treturn (\n\t\t\n\t\t\t{content}\n\t\t\n\t);\n}\n\nfunction renderReasoning({\n\tmessage,\n\tmessages,\n\tpart,\n\treasoningLabel,\n\tTextRenderer,\n}: {\n\tmessage: import('ai').UIMessage;\n\tmessages: AgentMessageMessages;\n\tpart: unknown;\n\treasoningLabel?: AgentMessageReasoningLabel;\n\tTextRenderer: AgentMessageTextRenderer;\n}) {\n\tconst text = readAgentMessageString(part, 'text');\n\tif (text === undefined) return ;\n\tconst streaming = readAgentMessageString(part, 'state') === 'streaming';\n\tconst label =\n\t\treasoningLabel?.({ message, part: part as AgentMessagePart, streaming }) ??\n\t\t(streaming ? messages.reasoningStreaming : messages.reasoning);\n\treturn (\n\t\t\n\t\t\t{label}\n\t\t\t
\n\t\t\t\t\n\t\t\t
\n\t\t\n\t);\n}\n\nfunction AgentMessageFile({ messages, part }: { messages: AgentMessageMessages; part: unknown }) {\n\tconst mediaType = readAgentMessageString(part, 'mediaType') ?? '';\n\tconst filename = readAgentMessageString(part, 'filename') ?? messages.unnamedAttachment;\n\tconst url = resolveAgentMessageHref(readAgentMessageValue(part, 'url'), 'file');\n\tif (!url) return ;\n\n\tif (mediaType === 'image' || mediaType.startsWith('image/')) {\n\t\treturn (\n\t\t\t
\n\t\t\t\t\n\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t
\n\t\t);\n\t}\n\tif (mediaType === 'audio' || mediaType.startsWith('audio/')) {\n\t\treturn (\n\t\t\t
\n\t\t\t\t
\n\t\t\t\t\t
\n\t\t\t\t{/* biome-ignore lint/a11y/useMediaCaption: Provider attachments do not include a transcript track. */}\n\t\t\t\t\n\t\t\t
\n\t\t);\n\t}\n\treturn (\n\t\t\n\t\t\t