@@ -437,24 +437,36 @@ export function CopilotChat({
437437 ? "processing"
438438 : transcribeMode ;
439439
440- // Memoize messages array - only create new reference when content actually changes
441- // (agent.messages is mutated in place, so we need a new reference for React to detect changes)
442-
443- // Use message id + role + content length as memo key instead of JSON.stringify
444- // to avoid serializing large base64 content on every render while still detecting
445- // content changes during streaming
440+ // Memoize messages array — only create a new reference when content changes.
441+ // We build a lightweight fingerprint instead of JSON.stringify to avoid
442+ // serializing large base64 attachment data on every render. The key captures:
443+ // - message id, role, content length (text streaming)
444+ // - content part count (multimodal additions)
445+ // - tool call ids + argument lengths (tool call streaming)
446446 const messagesMemoKey = agent . messages
447447 . map ( ( m ) => {
448- const contentLen =
448+ const contentKey =
449449 typeof m . content === "string"
450450 ? m . content . length
451451 : Array . isArray ( m . content )
452452 ? m . content . length
453453 : 0 ;
454- return `${ m . id } :${ m . role } :${ contentLen } ` ;
454+ const toolCallsKey =
455+ "toolCalls" in m && Array . isArray ( m . toolCalls )
456+ ? m . toolCalls
457+ . map (
458+ ( tc : any ) => `${ tc . id } :${ tc . function ?. arguments ?. length ?? 0 } ` ,
459+ )
460+ . join ( ";" )
461+ : "" ;
462+ return `${ m . id } :${ m . role } :${ contentKey } :${ toolCallsKey } ` ;
455463 } )
456464 . join ( "," ) ;
457- const messages = useMemo ( ( ) => [ ...agent . messages ] , [ messagesMemoKey ] ) ;
465+ const messages = useMemo (
466+ ( ) => [ ...agent . messages ] ,
467+ // eslint-disable-next-line react-hooks/exhaustive-deps
468+ [ messagesMemoKey ] ,
469+ ) ;
458470
459471 const finalProps = merge ( mergedProps , {
460472 messages,
0 commit comments