@@ -12,6 +12,7 @@ import {
1212import {
1313 type ComponentProps ,
1414 isValidElement ,
15+ memo ,
1516 type ReactNode ,
1617 useEffect ,
1718 useMemo ,
@@ -283,134 +284,143 @@ function formatBytes(bytes: number): string {
283284 return `${ mb . toFixed ( 1 ) } MB`
284285}
285286
286- function ToolPayload ( {
287- payload,
288- downloadName,
289- } : {
290- payload : unknown
291- downloadName : string
292- } ) {
293- const normalizedPayload = useMemo (
294- ( ) => extractMcpTextContent ( payload ) ,
295- [ payload ]
296- )
297- const serialized = useMemo (
298- ( ) => serializeToolPayload ( normalizedPayload ) ,
299- [ normalizedPayload ]
300- )
301- const isLargePayload = serialized . text . length > MAX_INLINE_PAYLOAD_CHARS
302- const codeLanguage = serialized . extension === "json" ? "json" : "console"
303- const byteCount = useMemo (
304- ( ) =>
305- isLargePayload ? new TextEncoder ( ) . encode ( serialized . text ) . length : 0 ,
306- [ isLargePayload , serialized . text ]
307- )
308- const [ downloadHref , setDownloadHref ] = useState < string | null > ( null )
287+ const ToolPayload = memo (
288+ function ToolPayload ( {
289+ payload,
290+ downloadName,
291+ } : {
292+ payload : unknown
293+ downloadName : string
294+ } ) {
295+ const normalizedPayload = useMemo (
296+ ( ) => extractMcpTextContent ( payload ) ,
297+ [ payload ]
298+ )
299+ const serialized = useMemo (
300+ ( ) => serializeToolPayload ( normalizedPayload ) ,
301+ [ normalizedPayload ]
302+ )
303+ const isLargePayload = serialized . text . length > MAX_INLINE_PAYLOAD_CHARS
304+ const codeLanguage = serialized . extension === "json" ? "json" : "console"
305+ const byteCount = useMemo (
306+ ( ) =>
307+ isLargePayload ? new TextEncoder ( ) . encode ( serialized . text ) . length : 0 ,
308+ [ isLargePayload , serialized . text ]
309+ )
310+ const [ downloadHref , setDownloadHref ] = useState < string | null > ( null )
309311
310- useEffect ( ( ) => {
311- if ( ! isLargePayload || ! serialized . text ) {
312- setDownloadHref ( null )
313- return
314- }
312+ useEffect ( ( ) => {
313+ if ( ! isLargePayload || ! serialized . text ) {
314+ setDownloadHref ( null )
315+ return
316+ }
315317
316- const blob = new Blob ( [ serialized . text ] , {
317- type : serialized . extension === "json" ? "application/json" : "text/plain" ,
318- } )
319- const href = URL . createObjectURL ( blob )
320- setDownloadHref ( href )
321- return ( ) => URL . revokeObjectURL ( href )
322- } , [ isLargePayload , serialized . extension , serialized . text ] )
318+ const blob = new Blob ( [ serialized . text ] , {
319+ type :
320+ serialized . extension === "json" ? "application/json" : "text/plain" ,
321+ } )
322+ const href = URL . createObjectURL ( blob )
323+ setDownloadHref ( href )
324+ return ( ) => URL . revokeObjectURL ( href )
325+ } , [ isLargePayload , serialized . extension , serialized . text ] )
326+
327+ if ( ! serialized . text ) {
328+ return null
329+ }
323330
324- if ( ! serialized . text ) {
325- return null
326- }
331+ if ( isLargePayload ) {
332+ return (
333+ < div className = "space-y-2" >
334+ < div className = "flex items-center justify-between gap-2 rounded-md border border-dashed bg-background/70 px-2.5 py-2 text-xs text-muted-foreground" >
335+ < span >
336+ Large payload ({ formatBytes ( byteCount ) } ). Preview hidden.
337+ </ span >
338+ { downloadHref && (
339+ < Button
340+ asChild
341+ variant = "outline"
342+ size = "sm"
343+ className = "h-6 px-2 text-xs"
344+ >
345+ < a
346+ href = { downloadHref }
347+ download = { `${ downloadName } .${ serialized . extension } ` }
348+ >
349+ < DownloadIcon className = "mr-1.5 size-3" />
350+ Download file
351+ </ a >
352+ </ Button >
353+ ) }
354+ </ div >
355+ </ div >
356+ )
357+ }
327358
328- if ( isLargePayload ) {
329359 return (
330360 < div className = "space-y-2" >
331- < div className = "flex items-center justify-between gap-2 rounded-md border border-dashed bg-background/70 px-2.5 py-2 text-xs text-muted-foreground" >
332- < span > Large payload ({ formatBytes ( byteCount ) } ). Preview hidden.</ span >
333- { downloadHref && (
334- < Button
335- asChild
336- variant = "outline"
337- size = "sm"
338- className = "h-6 px-2 text-xs"
339- >
340- < a
341- href = { downloadHref }
342- download = { `${ downloadName } .${ serialized . extension } ` }
343- >
344- < DownloadIcon className = "mr-1.5 size-3" />
345- Download file
346- </ a >
347- </ Button >
348- ) }
349- </ div >
361+ < CodeBlock
362+ code = { serialized . text }
363+ language = { codeLanguage }
364+ className = "[&_code]:text-[11px] [&_pre]:px-3 [&_pre]:py-2.5 [&_pre]:text-[11px]"
365+ >
366+ < CodeBlockCopyButton className = "absolute right-1.5 top-1.5 z-10 size-5 [&_svg]:size-3" />
367+ </ CodeBlock >
350368 </ div >
351369 )
352- }
353-
354- return (
355- < div className = "space-y-2" >
356- < CodeBlock
357- code = { serialized . text }
358- language = { codeLanguage }
359- className = "[&_code]:text-[11px] [&_pre]:px-3 [&_pre]:py-2.5 [&_pre]:text-[11px]"
360- >
361- < CodeBlockCopyButton className = "absolute right-1.5 top-1.5 z-10 size-5 [&_svg]:size-3" />
362- </ CodeBlock >
363- </ div >
364- )
365- }
370+ } ,
371+ ( prev , next ) =>
372+ prev . payload === next . payload && prev . downloadName === next . downloadName
373+ )
374+ ToolPayload . displayName = "ToolPayload"
366375
367376export type ToolInputProps = ComponentProps < "div" > & {
368377 input : ToolPart [ "input" ]
369378}
370379
371- export const ToolInput = ( { className, input, ...props } : ToolInputProps ) => (
372- < div className = { cn ( "space-y-2 overflow-hidden" , className ) } { ...props } >
373- < h4 className = "font-medium text-[11px] text-muted-foreground tracking-wide" >
374- PARAMETERS
375- </ h4 >
376- < ToolPayload payload = { input } downloadName = "tool-parameters" />
377- </ div >
380+ export const ToolInput = memo (
381+ ( { className, input, ...props } : ToolInputProps ) => (
382+ < div className = { cn ( "space-y-2 overflow-hidden" , className ) } { ...props } >
383+ < h4 className = "font-medium text-[11px] text-muted-foreground tracking-wide" >
384+ PARAMETERS
385+ </ h4 >
386+ < ToolPayload payload = { input } downloadName = "tool-parameters" />
387+ </ div >
388+ )
378389)
390+ ToolInput . displayName = "ToolInput"
379391
380392export type ToolOutputProps = ComponentProps < "div" > & {
381393 output : ToolPart [ "output" ]
382394 errorText : ToolPart [ "errorText" ]
383395}
384396
385- export const ToolOutput = ( {
386- className,
387- output,
388- errorText,
389- ...props
390- } : ToolOutputProps ) => {
391- const hasOutput = output !== undefined && output !== null
392- if ( ! hasOutput && ! errorText ) {
393- return null
394- }
397+ export const ToolOutput = memo (
398+ ( { className, output, errorText, ...props } : ToolOutputProps ) => {
399+ const hasOutput = output !== undefined && output !== null
400+ if ( ! hasOutput && ! errorText ) {
401+ return null
402+ }
395403
396- return (
397- < div className = { cn ( "space-y-2" , className ) } { ...props } >
398- < h4 className = "font-medium text-[11px] text-muted-foreground tracking-wide" >
399- { errorText ? "Error" : "RESULT" }
400- </ h4 >
401- { errorText && (
402- < div className = "rounded-md bg-destructive/10 px-2.5 py-1.5 text-[11px] text-destructive" >
403- { errorText }
404- </ div >
405- ) }
406- { hasOutput &&
407- ( isValidElement ( output ) ? (
408- < div className = "rounded-md bg-muted/50 p-1.5 text-[11px] text-foreground" >
409- { output }
404+ return (
405+ < div className = { cn ( "space-y-2" , className ) } { ...props } >
406+ < h4 className = "font-medium text-[11px] text-muted-foreground tracking-wide" >
407+ { errorText ? "Error" : "RESULT" }
408+ </ h4 >
409+ { errorText && (
410+ < div className = "rounded-md bg-destructive/10 px-2.5 py-1.5 text-[11px] text-destructive" >
411+ { errorText }
410412 </ div >
411- ) : (
412- < ToolPayload payload = { output } downloadName = "tool-result" />
413- ) ) }
414- </ div >
415- )
416- }
413+ ) }
414+ { hasOutput &&
415+ ( isValidElement ( output ) ? (
416+ < div className = "rounded-md bg-muted/50 p-1.5 text-[11px] text-foreground" >
417+ { output }
418+ </ div >
419+ ) : (
420+ < ToolPayload payload = { output } downloadName = "tool-result" />
421+ ) ) }
422+ </ div >
423+ )
424+ }
425+ )
426+ ToolOutput . displayName = "ToolOutput"
0 commit comments