- Kill useJsonStable in CopilotChatConfigurationProvider — useShallowStableRef
is now the single stabilization primitive for both messageView props and labels
- Harden useShallowStableRef with isPlainObject guard so arrays, Dates, and
class instances are never shallow-compared (reference-only for non-plain objects)
- Remove redundant stableParentLabels — parentConfig?.labels is already stabilized
by the parent provider's own useShallowStableRef
- Uninstall ts-deepmerge from react-core dependencies (no longer imported anywhere)
- Add unit tests for useShallowStableRef in slots.test.ts (5 tests)
- Add changeset (patch) describing the FOR-75 performance fix
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Extract useShallowStableRef<T> into slots.tsx alongside shallowEqual —
replaces the 20-line inline stabilization block in CopilotChat with two
readable call sites, and covers suggestionView for free
- Apply useShallowStableRef to both messageView and suggestionView in CopilotChat
- Use post-hoc assignment for messageView to avoid an empty object allocation
on every render when messageView is undefined
- Rewrite Test 2 (labels) to use LabelConsumerMessage which calls
useCopilotChatConfiguration() directly — context consumers re-render when
their context changes regardless of parent memo boundaries, making this a
genuine regression guard for the labels fix (previous version tracked
assistantRenderCount which was already protected by messageView stabilization)
- Replace `as any` on messageView with scoped cast to preserve structural
type checking on the surrounding object
- Fix text/testid collision: CountingAssistantMessage now renders data-testid
instead of a hardcoded string that matched the agent reply text
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the manual JSON.stringify dep-key pattern (which required three
eslint-disable-next-line comments) with a useJsonStable utility that
stabilizes object references using JSON comparison. The dep array for
mergedLabels is now honest — it references the exact variables used inside
the useMemo callback.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Inline `messageView` and `labels` objects passed to CopilotChat caused all
completed assistant messages to re-render on every keystroke because:
1. `ts-deepmerge.merge()` deep-clones its inputs, producing a new `messageView`
reference on every render even when content is identical — defeating
`MemoizedSlotWrapper`'s shallow equality check.
2. An inline `labels` object (new reference each render) invalidated the
`mergedLabels` useMemo in `CopilotChatConfigurationProvider`, causing
every `useCopilotChatConfiguration()` consumer to re-render on every keystroke.
3. The inline `onAddFile` arrow function was a new reference each render.
Fixes:
- Replace `merge()` with shallow spread; stabilize `messageView` via a ref +
`shallowEqual` guard so the same object reference is returned when props
are shallowly equal across renders.
- Use `useCallback` for `handleAddFile`.
- Add JSON.stringify-based dep keys in `CopilotChatConfigurationProvider` so
`mergedLabels` is only recomputed when label values actually change.
Adds deterministic render-count regression tests (FOR-75) that fail on the
unfixed code and pass after the fix.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1. consumeAttachments: read from a ref mirror instead of side-effecting
out of a setState updater, avoiding reliance on synchronous updater
execution under React concurrent mode.
2. Codemod: skip declaration positions (variable, function, class, type,
interface) and non-reference positions (object keys, member accesses).
When a local declaration shadows the import name, only rename
unambiguous type-position references to avoid corrupting unrelated code.
Reverts the copy-dts.mjs approach from #3612 in favor of typesVersions
which is the standard Node/TS mechanism for resolving subpath types
under legacy moduleResolution: "node".
feat(runtime): add defaults and max caps for thread lock TTL and heartbeat
lockTtlSeconds defaults to 20s (max 3600s / 1 hour) and
lockHeartbeatIntervalSeconds defaults to 15s (max 3000s / 50 minutes).
Both values are always set, so the heartbeat timer now starts
unconditionally and the paired-option validation is removed.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The CopyButton setTimeout fires after the jsdom test environment is
torn down, causing an unhandled ReferenceError on Node 20. Track the
timer in a ref and clear it on cleanup.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace `ReactActivityMessageRenderer<unknown>` + `(content as any).resourceUri`
with `ReactActivityMessageRenderer<z.infer<typeof MCPAppsActivityContentSchema>>`
so the renderer prop is properly typed and the `as any` cast is eliminated.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After the useRenderActivityMessage fix, MCPAppsActivityRenderer receives the
per-thread clone instead of the registry agent. The mock's clone() did not share
isRunning state, causing waitForAgentIdle(clone) to never resolve (emit() only
updated registry.isRunning). Also, tests that monkey-patch runAgent before
renderWithCopilotKit need the clone to delegate proxied MCP requests to the
registry (to pick up the monkey-patch) while running user-message flows on the
clone itself (so clone.messages is updated and rendered by CopilotKit).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fingerprint all tool-call args on last message (not just last tool call)
so parallel/non-last tool-call streaming is detected and triggers re-render
- Add dev warning when scroll container has clientHeight=0 so virtualization
silently disabled (e.g. chat inside display:none tab) is surfaced early
- Use callback ref for non-autoScroll path in ScrollView to eliminate
useLayoutEffect + eslint-disable; autoScroll path keeps useLayoutEffect
since StickToBottom manages that ref internally
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a jsdom-compatible test that confirms the virtual code path activates
above VIRTUALIZE_THRESHOLD without requiring a real browser viewport. Mocks
clientHeight and getBoundingClientRect on the scroll element to pass both
CopilotChatMessageView's guard and TanStack Virtual's observeElementRect.
Asserts the virtual container div exists and rendered count < TOTAL.
Drains pending rAF callbacks before unmount to prevent spurious uncaught
exceptions after jsdom teardown.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds a minimal demo at /a2ui-demo that uses the demo-button agent to
reproduce and validate the A2UI thread-clone bug fix. The agent renders
an A2UI surface with a Confirm button on first run; clicking it fires a
second run that emits a text confirmation message.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
scrollToIndex was firing on every streaming chunk (deduplicatedMessages.length
dep), forcibly yanking users to the bottom even when scrolled up to read
history. Removed the dep — use-stick-to-bottom handles streaming auto-scroll
via content height growth on the virtualizer's total-size div, same as the
flat path. scrollToIndex now only fires on virtual mode activation and thread
switches.
Also removed a stale comment referencing toolResultMap (removed two commits
ago) and replaced it with an accurate description of the linear scan.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- All callbacks (processFiles, handleFileUpload, handleDragOver, handleDragLeave,
handleDrop, removeAttachment, consumeAttachments) are now wrapped in useCallback
with empty deps — referentially stable across renders
- Config values read from configRef to avoid dep array changes
- consumeAttachments no-ops on empty queue (returns same state reference)
- 9 new tests: referential stability across re-renders, re-render counting
(consumeAttachments on empty queue triggers zero re-renders), initial state,
consumeAttachments behavior, removeAttachment no-op
useRenderActivityMessage was calling copilotkit.getAgent() directly,
always returning the registry agent. When a user clicked an A2UI button,
handleAction → runAgent executed on the registry agent — messages
accumulated there while CopilotChat displayed from the per-thread clone
(created by useAgent), so responses appeared to silently vanish.
Apply the same getThreadClone(registryAgent, threadId) ?? registryAgent
pattern already used in useRenderCustomMessages.
Adds a regression test that asserts the renderer receives the clone,
and confirms the test catches the bug when the fix is reverted.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- New docs/(root)/migration-guides/ section with migrate-to-v2, migrate-attachments,
migrate-to-1.10.X, migrate-to-1.8.2
- Troubleshooting now only contains error-debugging, observability-connectors, common-issues
- Update all @deprecated JSDoc URLs from /troubleshooting/ to /migration-guides/
- Update doc cross-links (multimodal-attachments page, snippet import)
- Move snippet to snippets/shared/migration-guides/
- Set minimum version to v1.56.0 (was vX.Y.Z placeholder)
- Restore console.error as safety net in processFiles catch blocks (errors no longer
silently vanish when onUploadFailed callback is not provided)
- Add try/catch to useBlobUrl atob() — gracefully handles malformed base64 instead of
crashing the component tree
- Fix DocumentLightboxContent: use blobUrl instead of undefined src variable
- Add .catch() to View Transition API transition.finished promise
- Codemod: preserve actual imageUploadsEnabled value (false, dynamic expressions)
instead of hardcoding true — adds 3 new test cases (18 total)
- Replace all placeholder @deprecated versions (v1.x.0) with @since 1.56.0
- Add missing @since to ImageRenderer, ImageRendererProps, AIMessage.image tags
Attachment queue & previews:
- Image lightbox with View Transition API morph animation
- Video lightbox with native controls and play button overlay
- Document lightbox (PDF via blob URL, text inline, info card fallback)
- Drop zone overlay with upload icon
- Filename preservation via InputContent metadata
- Proper video thumbnail sizing and play/pause indicator
- Fix attachment queue positioning (max-w-3xl constraint)
- Padding between X button and content for audio/document cards
- Document filenames wrap instead of truncating
Attachments config:
- onUploadFailed callback for validation/upload errors (file-too-large, invalid-type, upload-failed)
- onUpload accepts sync or async returns
- AttachmentUploadResult discriminated union with explicit interfaces
- Metadata field on Attachment and onUpload return type
AG-UI version bump:
- Bump @ag-ui/client, @ag-ui/core, @ag-ui/encoder, @ag-ui/proto to 0.0.51
- Remove process.env Vite workaround (fixed upstream in 0.0.51)
Deprecation lifecycle:
- @deprecated JSDoc on all legacy image upload APIs
- ImageRenderer, ImageRendererProps, ImageUpload type, imageUploadsEnabled prop,
inputFileAccept prop, ImageRenderer prop, AIMessage.image, ImageData
- Codemod at codemods/migrate-attachments.ts (15 tests)
- Migration guide updated with codemod instructions and new type shapes
Docs:
- New guide: docs/(root)/multimodal-attachments.mdx
- Updated migration guide with onUpload return type, metadata, codemod section
- Cross-links from prebuilt-components and migration guide
- Label change: "Add photos or files" → "Add attachments"
Tests:
- CopilotChat.attachments.test.tsx — 5 tests for onUploadFailed
- migrate-attachments codemod — 15 tests
Wire multimodal attachment support into the v2 CopilotChat component:
- Add AttachmentsConfig prop to CopilotChat for enabling file attachments
- Implement processFiles with accept filter, size validation, placeholder/ready lifecycle
- Support custom onUpload handlers and default base64 encoding
- Build InputContent[] when attachments accompany a message
- Add drag-and-drop handlers with visual feedback (dashed outline)
- Add clipboard paste handler for file items
- Render CopilotChatAttachmentQueue between scroll view and input
- Forward onAddFile through CopilotChatView to CopilotChatInput's AddMenuButton
- Omit internal attachment state props from CopilotChatProps public API