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>
The v2 CopilotChatInput had a useEffect that called scrollIntoView()
whenever the textarea received focus. Combined with autoFocus defaulting
to true, this caused the entire page to scroll to the chat component on
load - the same bug that was previously fixed in v1.
https://claude.ai/code/session_01LPiJ8GcUNtWdS2W6MYSE4b
- Change ScrollElementContext to carry HTMLElement | null state (not a ref)
so CopilotChatMessageView re-renders reactively when the scroll container
first mounts. Removes the one-shot useLayoutEffect that silently kept
virtualization disabled in the default autoScroll path (hasMounted gate
in ScrollView deferred the Provider past the one-shot read window).
- Lift resolveSlotComponent calls out of renderMessageBlock into useMemo.
When a slot was passed as a CSS class string, resolveSlotComponent
returned a new { className } object on every call, defeating
MemoizedAssistantMessage's slotProps reference-equality check and
causing all completed messages to re-render on every parent update.
- Add deduplicatedMessages.length to scrollToIndex deps so the virtualizer
stays at the bottom as new messages arrive during streaming.
- Replace four as-any ToolMessage casts in the memoized comparator with
typed inline type predicates using the ToolMessage import from @ag-ui/core.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The /perf demo page and .bench.tsx file were useful during development but
add noise to the PR. Removing them keeps the diff focused on the library
changes. Also removes the __perfScrollEl / __perfMsgCount window globals
that only existed to serve the perf page's polling mechanism.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The pre-computed toolResultMap in MemoizedAssistantMessage's comparator caused
HITL tests to fail: after responding to one tool call, the second tool call's
status would not transition from 'inProgress' to 'complete'.
Root cause: when executingToolCallIds context updates (removing the completed
tool call from the set), CopilotChatToolCallsView re-renders via useCopilotKit()
context subscription. At that point it holds stale `messages` props (MemoizedAssistantMessage
hasn't re-rendered yet), so messages.find(tc.id) returns undefined. The timing
window between the context update and the messages re-render caused the status
to briefly show inProgress. In the old code the same O(n) messages.filter() scan
was used, which is proven correct. Reverting to that approach.
Keeps all other perf improvements: deduplicatedMessages useMemo, virtualization,
resolveSlotComponent helper, NODE_ENV-gated dev globals.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Addresses review findings from PR #3611:
- toolResultMap: pre-compute id→content Map via useMemo so MemoizedAssistantMessage
comparator does O(k) lookups instead of O(n) linear scans
- resolveSlotComponent<T>() helper eliminates repeated slot-resolution boilerplate
- NODE_ENV gate on __perfMsgCount / __perfScrollEl (no production window pollution)
- hasScrolledToBottomRef → dep-based [shouldVirtualize, firstMessageId] in useLayoutEffect
- Merged duplicate virtual/flat render return paths; removed dead null-guards in comparator
- AssistantMessage type import + cast in fingerprint (CopilotChat.tsx)
- perf page: private → protected, lastScrollTop=NaN, STABLE_POLLS_REQUIRED constant,
agent as any with version-mismatch comment (ag-ui 0.0.46 vs 0.0.48)
- snapshot test: act+tick flush instead of sentinel message (Observable subscription
ends after RUN_FINISHED so new events are dropped)
- .bench.tsx extension comment: not picked up by vitest CI glob
Pre-existing test failures in use-human-in-the-loop.e2e.test.tsx (2 tests) and
@copilotkit/sqlite-runner + @copilotkit/runtime are on main; unrelated to this PR.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copies .d.cts → .d.ts after each tsdown build so consumers on
moduleResolution "node" (e.g. DocuSign) can resolve types without
changing their tsconfig. Also points the "types" field back to .d.ts.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add @tanstack/react-virtual to react-core deps; activate useVirtualizer
in CopilotChatMessageView when message count exceeds 50 (VIRTUALIZE_THRESHOLD).
Only visible items are mounted; total scrollable height is maintained via
an absolutely-positioned container div.
- Add scroll-element-context.ts to pass the scroll container ref from
CopilotChatView down to CopilotChatMessageView without circular imports.
- Expose window.__perfScrollEl (scroll container) and window.__perfMsgCount
(committed message count) for programmatic perf measurement.
- Update /perf demo page with two-phase timing: phase 1 measures event emit
time, phase 2 waits for all messages committed + scroll position stable
(animation settled). PerfPanel now shows emit / render+anim / total.
Benchmark results (MacBook, dev build):
Load 50 [anim]: emit 305ms render+anim 1433ms total 1739ms
Load 100 [anim]: emit 752ms render+anim 1578ms total 2331ms
Load 500 [anim]: emit 14.2s render+anim 1783ms total 16.0s
Load 50 [noAnim]: emit 299ms render+anim 182ms total 481ms
Load 100 [noAnim]: emit 771ms render+anim 161ms total 931ms
Load 500 [noAnim]: emit 13.9s render+anim 199ms total 14.1s
Key findings:
- React render cost is now O(1) in message count (161-199ms flat).
Virtualization fully decouples DOM work from history size.
- Animation (StickToBottom spring scroll-to-bottom) adds ~1.4-1.8s
regardless of message count; this is a fixed cost on every history load.
- The remaining bottleneck for large histories is event emit time (~5.6ms
per TEXT_MESSAGE_CHUNK through AbstractAgent). This is upstream of React
and tracked separately.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix fetchRuntimeInfoAutoDetect in agent.ts to use 2xx-only check
(was checking only 404/405, now aligns with agent-registry.ts)
- Remove duplicate JSDoc block on fetchRuntimeInfoAutoDetect
- Add 7 edge case tests: 500/403/405/network-error/both-fail scenarios
- Add 4 tests for useSingleEndpoint->runtimeTransport ternary mapping
- Update Angular test stubs to use "auto" default (was "rest")
Co-Authored-By: Claude Sonnet 4.6 (1M context) <noreply@anthropic.com>
Instead of requiring users to explicitly set transport: "single", the
client now auto-detects the transport mode by trying REST (GET /info)
first and falling back to single-endpoint (POST with { method: "info" })
if the REST probe returns 404/405 or fails. The explicit flag is kept
for backward compatibility.
https://claude.ai/code/session_016xtYN15TY2BaBviryTcVyY
Replace full JSON.stringify(agent.messages) memo dep with a lightweight
fingerprint (length + last-id + last-content + last tool-call args) so only
the in-flight message is serialized on each render instead of the whole array.
Wrap deduplicatedMessages in useMemo([messages]) so the Map is not
reconstructed on every render cycle.
Add generateMessages(n) factory to test-helpers for perf-focused tests, a
vitest bench for the dedup map at 100/1000 messages, and three regression
tests that assert completed messages do not re-render when new messages or
state snapshots arrive.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All @copilotkit/* packages were missing type declarations in their exports
map. This meant TypeScript couldn't resolve types for subpath imports like
@copilotkit/react-core/v2 without manual tsconfig paths workarounds
pointing to dist .d.ts files (which no longer exist after the tsup to
tsdown migration).
Uses nested conditional exports to map .d.mts for ESM and .d.cts for CJS
consumers, satisfying both attw and TypeScript module resolution under
nodenext and bundler modes.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace eslint and prettier with oxlint and oxfmt for faster linting
and formatting across the monorepo. Remove all eslint and prettier
configs, dependencies, and related packages. Add .oxlintrc.json and
.oxfmtrc.json for the new tooling. Update CI workflows and lefthook
hooks accordingly. Reformat codebase with oxfmt.
https://claude.ai/code/session_01GMkSf29p78HuMR1mbXn8He
- Introduced message factory functions to eliminate `as any` type assertions for message literals.
- Updated test cases to utilize the new message factories for improved clarity and type safety.
- Added helper functions for subscriber notifications to streamline test setup.
This refactor improves code readability and maintainability in the test suite for the useAgent hook.
- Replace Date.now()-based throttle with flag-based leading+trailing
pattern that is immune to wall-clock jumps
- Wrap throttleMs validation in useMemo to warn once per value change
instead of on every render
- Remove phantom AbstractAgent.notificationThrottleMs JSDoc reference
- Reorder cleanup to clear timer before unsubscribing for safety
- Add tests for trailing-edge data freshness, timer precision, and
dynamic throttleMs prop changes
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Fix JSDoc to accurately describe behavior for negative/non-finite values
- Remove NODE_ENV guard so console.warn fires in all environments
- Use derived effectiveThrottleMs instead of mutating destructured param
- Simplify handler wrappers: direct references instead of arrow wrappers
- Remove stale comment about content stripping
- Add parameterized tests for invalid throttleMs (NaN, Infinity, -1, -Infinity)
- Fix misleading test comment
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Addresses code review findings:
- Merge two separate useEffect hooks into one to eliminate dual-subscription
race window and simplify cleanup
- Add active boolean guard to prevent forceUpdate after unmount/re-run
- Validate throttleMs input (reject NaN, Infinity, negatives with dev warning)
- Expand JSDoc with scope, constraints, and notificationThrottleMs interaction
- Replace JSON.stringify(updateFlags) with direct ref in effect deps
- Strengthen unmount test with render counter instead of not.toThrow()
- Add 5 new test cases: explicit zero, rapid burst, new cycle after trailing,
updates excluding OnMessagesChanged, subscription cleanup verification
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Adds optional `throttleMs` parameter to `useAgent()` that coalesces
rapid onMessagesChanged notifications (per-token TEXT_MESSAGE_CHUNK
events) into fewer React re-renders using a leading+trailing throttle.
- throttleMs=0 (default): exact current behavior, zero overhead
- throttleMs>0: only onMessagesChanged is throttled; onStateChanged
and onRunStatusChanged always fire immediately
- Leading+trailing: first call immediate, last call guaranteed
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>