Description
CopilotChat (v2, packages/react-core/src/v2/components/chat/CopilotChat.tsx) memoizes the messages array it hands to its chatView slot using a fingerprint computed per message:
const contentKey = typeof m.content === "string"
? m.content.length
: Array.isArray(m.content)
? m.content.length
: 0;
For messages whose content is a plain object (e.g. activity/progress messages shaped like { steps: [...] }), contentKey is always the constant 0 regardless of how the object's contents change. The dependency array of the useMemo therefore never changes for these messages, so CopilotChat never recomputes messages when only object-content fields mutate — downstream components (e.g. a custom chatView) receive a stale, frozen message even though the underlying agent state (via onActivitySnapshotEvent / onRunFinalized) is updating correctly.
This is distinct from #4676 (already fixed): that issue was about @ag-ui/client mutating message objects in place and MemoizedActivityMessage's shallow/reference comparator not detecting changes. That underlying fix is present and correct in 0.0.57 — but CopilotChat's own memo sits one layer above it and independently blocks the update via this fingerprinting bug.
Impact
Any custom activity/progress renderer registered via renderActivityMessages appears frozen on its first snapshot; a run's live step progress and completion state (e.g. "Finished in Xs") never render, even though the agent event stream is correct.
Reproduction
- Register a custom activity renderer via
renderActivityMessages that emits ACTIVITY_SNAPSHOT events with replace: true and object content (e.g. { steps: [{ name: 'step1', status: 'running' }] })
- Emit a second
ACTIVITY_SNAPSHOT with the same messageId but different content (e.g. { steps: [{ name: 'step1', status: 'done' }] })
- Observe: the renderer receives the updated content in
agent.messages (verifiable via useEffect(() => { console.log(agent.messages) }, [agent.messages])) but the UI never re-renders
Environment
@copilotkit/react-core@1.64.1
@ag-ui/client@0.0.57
@ag-ui/core@0.0.57
Suggested Fix
Include a stable content-derived key for non-string/array content, e.g. JSON.stringify(m.content).length or a content hash, instead of falling back to a constant 0:
const contentKey =
typeof m.content === "string"
? m.content.length
: Array.isArray(m.content)
? m.content.length
: JSON.stringify(m.content).length;
Or better yet, use the full JSON.stringify(m.content) for activity messages to ensure any content change is detected.
Workaround Used
Subscribe directly to the agent's raw event stream (agent.subscribe(...)) in a separate React Context provider mounted alongside chatView, bypassing CopilotChat's messages memo entirely.
Description
CopilotChat (v2,
packages/react-core/src/v2/components/chat/CopilotChat.tsx) memoizes themessagesarray it hands to itschatViewslot using a fingerprint computed per message:For messages whose
contentis a plain object (e.g. activity/progress messages shaped like{ steps: [...] }),contentKeyis always the constant0regardless of how the object's contents change. The dependency array of theuseMemotherefore never changes for these messages, so CopilotChat never recomputesmessageswhen only object-content fields mutate — downstream components (e.g. a customchatView) receive a stale, frozen message even though the underlying agent state (viaonActivitySnapshotEvent/onRunFinalized) is updating correctly.This is distinct from #4676 (already fixed): that issue was about
@ag-ui/clientmutating message objects in place andMemoizedActivityMessage's shallow/reference comparator not detecting changes. That underlying fix is present and correct in 0.0.57 — but CopilotChat's own memo sits one layer above it and independently blocks the update via this fingerprinting bug.Impact
Any custom activity/progress renderer registered via
renderActivityMessagesappears frozen on its first snapshot; a run's live step progress and completion state (e.g. "Finished in Xs") never render, even though the agent event stream is correct.Reproduction
renderActivityMessagesthat emitsACTIVITY_SNAPSHOTevents withreplace: trueand object content (e.g.{ steps: [{ name: 'step1', status: 'running' }] })ACTIVITY_SNAPSHOTwith the samemessageIdbut different content (e.g.{ steps: [{ name: 'step1', status: 'done' }] })agent.messages(verifiable viauseEffect(() => { console.log(agent.messages) }, [agent.messages])) but the UI never re-rendersEnvironment
@copilotkit/react-core@1.64.1@ag-ui/client@0.0.57@ag-ui/core@0.0.57Suggested Fix
Include a stable content-derived key for non-string/array content, e.g.
JSON.stringify(m.content).lengthor a content hash, instead of falling back to a constant0:Or better yet, use the full
JSON.stringify(m.content)for activity messages to ensure any content change is detected.Workaround Used
Subscribe directly to the agent's raw event stream (
agent.subscribe(...)) in a separate React Context provider mounted alongsidechatView, bypassing CopilotChat'smessagesmemo entirely.