Extract shared helpers (DEFAULT_LAYOUT_OPTIONS, getLayoutGrid,
renderTypeAndExpectLayout) to eliminate duplication, add explanatory
comments for unavoidable `as any` casts, and add missing test for
canvas.getContext returning null.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Split ResizeObserver into container vs textarea observers so textarea
height changes (typing, manual resize) don't invalidate the dimension
cache unnecessarily — only grid/button resizes invalidate it
- Invalidate cache on self-triggered resizes (compact↔expanded toggle)
even when skipping re-evaluation, preventing stale dimensions
- Validate fontSize and fontFamily before constructing font fallback
string, avoiding malformed CSS font values in Safari
- Add dev-mode console.warn when font resolution or canvas context
fails, making silent measurement path failures diagnosable
- Update test ResizeObserver mock to support multiple observers
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Test canvas text measurement triggers expansion for long single-line text
- Test compact layout preserved when text fits within cached width
- Test cache invalidation on resize produces correct re-measurement
- Test empty font string is not cached (guard prevents invalid canvas state)
- Mock ResizeObserver to exercise cache invalidation path in jsdom
- Extend mockLayoutMetrics with getComputedStyle mocks for grid and textarea
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Store compactWidth (pre-padding) in cache, compute compactInnerWidth
at read time from live measurementsRef to avoid stale padding values
- Move cache invalidation after ignoreResize check so programmatic
layout transitions don't needlessly destroy the cache
- Guard against empty/invalid font strings before writing to cache
- Fix comment inaccuracies: 2x (not 3x) getComputedStyle, lazily
populated (not on mount), invalidated (not updated) on resize
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
evaluateLayout runs on every keystroke via useLayoutEffect. Previously it
performed 3x getComputedStyle + 2x getBoundingClientRect on every call to
measure grid/button dimensions and the textarea font — none of which change
between keystrokes.
Cache these measurements in a ref (containerCacheRef), populated lazily on
first evaluateLayout call and invalidated when the ResizeObserver fires.
Per-keystroke reflows drop from 3-4 to 1 (the unavoidable adjustTextareaHeight).
Also removes a stale console.log("FOOBAR") debug line.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The { ...existing, ...message } spread meant a later streaming chunk with
toolCalls: undefined would silently wipe accumulated tool calls, contradicting
the comment's claim that "latest toolCalls wins". Apply the same ?? recovery
logic to toolCalls as content already uses for the || fallback.
Add a test for the flip-side edge case: first occurrence has toolCalls, second
has non-empty content but undefined toolCalls — toolCalls must survive.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- 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".
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>
- Extract dedup logic into exported deduplicateMessages() pure function so
it can be unit-tested directly
- Wrap with useMemo([messages]) to avoid allocating a new Map on every frame
- Collapse three-way branch to two-way (the two arms were identical)
- Use { ...existing, ...message, content } for a true merge so fields present
only in an earlier occurrence are not silently dropped
- Expand comment to explain why || treats empty string as falsy
- Add deduplicateMessages unit tests: toolCalls assertion, reverse scenario,
non-assistant keep-last behavior
- Rename "keeping the last occurrence" test — no longer accurate for assistant
messages which now merge rather than keep-last
Co-Authored-By: Claude Sonnet 4.6 <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>
- 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
When users explicitly set autoFocus={true}, the textarea now focuses
with { preventScroll: true } so the page doesn't jump. Also applies
to the modal-open focus path. Added tests verifying default no-focus
behavior and that opt-in focus uses preventScroll.
https://claude.ai/code/session_01LPiJ8GcUNtWdS2W6MYSE4b
During streaming, the same message ID can arrive multiple times as tool calls are appended.
The previous "keep last" dedup lost any text content that was streamed before the first tool call,
because later entries carry empty content. Replace with a merge strategy: for assistant messages,
recover non-empty content from earlier occurrences while keeping the latest toolCalls accumulation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>