- Fix stale closure in paste handler by using processFilesRef pattern
- Wrap processFiles calls in try/catch across paste, file upload, and drop handlers
- Add early return for empty/falsy accept string in matchesAcceptFilter
- Block send while attachments are still uploading
- Add id field to Attachment interface, replace _id hack with randomUUID()
- Add 10s timeout to generateVideoThumbnail to prevent infinite hanging
- Show placeholder div for uploading attachments instead of rendering incomplete source
- Include filename context in upload error messages
Wrap all three new URL() call sites in try/catch so a malformed URL string
skips the part with a console.warn rather than crashing message conversion.
Add a default switch case to warn on unrecognized content part types.
Add a test covering the malformed-URL skip behaviour.
Re-export AG-UI multimodal input types (InputContent, TextInputPart, ImageInputPart,
AudioInputPart, VideoInputPart, DocumentInputPart, InputContentSource, etc.) from
@copilotkit/shared. UserMessage is now a plain pass-through of agui.UserMessage,
removing the CopilotKit-specific image?: ImageData extension. ImageData is kept with
@deprecated for backward compatibility with AIMessage.image.
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
- Python SDK: 18 new tests for LangGraphAGUIAgent (custom event handling,
emit filtering, state merging, copilotkit namespace)
- TypeScript SDK: 25 new tests for copilotkitCustomizeConfig and
convertActionsToDynamicStructuredTools
- TypeScript Runtime: 27 new tests for event-source helpers
(shouldEmitToolCall, getCurrentMessageId, getCurrentContent, etc.)
- TypeScript Runtime: expanded dispatch-event-filtering tests with
custom event dispatch (manually_emit_message/tool_call/state, exit)
and langGraphDefaultMergeState tests
- Dead code annotations: LangGraphAgent class and use_function_call=True
branch annotated with TODO(ran-review) for Ran to verify
https://claude.ai/code/session_01BPMn7zhadhapfyyD8kYeAH
- 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>
- Add package.json exports for v2/{express,hono,node} subpaths
- Add elysia devDependency and tsdown entry points
- Exclude bun integration tests from vitest config
- Update CI workflows to include runtime-servers test job
- Add runtime-server-adapter docs page
- Add changeset for the fetch-based runtime feature
Comprehensive test coverage for the new fetch handler layer:
- Unit tests for fetch-handler, fetch-cors, fetch-router, hooks
- Adapter tests for Express and Hono bridges
- Node.js native HTTP handler tests
- Integration test suites for single and multi-endpoint setups
across Express, Hono, Node, and Bun/Elysia servers
- Edge case and backward compatibility tests
Bridge the framework-agnostic fetch handler to specific frameworks:
- Express adapter via express-fetch-bridge.ts (converts req/res to fetch)
- Hono adapter (thin wrapper, Hono already uses Request/Response)
- Node.js native HTTP adapter via node-fetch-handler.ts
- Re-export entry points at @copilotkit/runtime/v2/{express,hono,node}
Simplifies existing Express and Hono endpoints by delegating to the
shared fetch core instead of duplicating routing and CORS logic.
Introduce the framework-agnostic fetch handler layer:
- fetch-handler.ts: single Request→Response pipeline for all routes
- fetch-cors.ts: CORS preflight and response header handling
- fetch-router.ts: path-based routing for multi-endpoint setups
- hooks.ts: lifecycle hooks (onBeforeRequest, onAfterResponse, onError)
Also migrates handler error responses from Response.json() to
new Response(JSON.stringify(...)) for broader runtime compatibility
(Node.js <20, CF Workers, Deno).
Move middleware-sse-parser.ts, middleware.ts, and runtime.ts into
a new core/ subdirectory to make room for the fetch-based handler
layer. Updates all import paths in handlers and existing tests.