- Close TOCTOU window: set abortController synchronously before Observable
creation in classic run(), matching factory run() pattern (M3)
- Add concurrent run guard to classic run() (C1)
- Add threadId/runId to RUN_ERROR events in classic mode (I2)
- Check both 'output' and 'result' property names in classic tool-result (C2/M2)
- Add try/catch around JSON.stringify in classic tool-result (I4/M2)
- Add undefined guards before emitting state snapshot/delta events (I3)
- Guard assignToolsToAgents against factory-mode agent configs (M1)
- Improve error handling in classic error case with proper fallback (C3)
- Improve TanStack converter TODO comment with known gap details (M5)
- Document @ts-expect-error on clone() middlewares access (S1)
- Add JSDoc to AgentFactoryContext.abortController (M4)
- Fix duplicate import and clean up react-router example
- Update docs AgentFactoryContext reference
- Add converter imports (convertAISDKStream, convertTanStackStream) to agent/index.ts
- Update canOverride() to return false for factory configs
- Add factory-path early return in run() that delegates to runFactory()
- Add private runFactory() method that handles aisdk/tanstack/custom factory configs
with full lifecycle management (RUN_STARTED, RUN_FINISHED, RUN_ERROR, abort support)
- Fix copilot-runtime.ts: change BuiltInAgentConfiguration["tools"] indexed access
types to BuiltInAgentClassicConfig["tools"] since tools only exist on classic configs
Source fixes:
- Fix TOCTOU race in concurrent run guard — set abortController
synchronously before Observable creation, closing the window where
two run() calls could both pass the guard
- Add JSON.stringify try/catch in TanStack TOOL_CALL_RESULT handler
(matching existing aisdk.ts protection against circular refs)
- Use role allowlist (user/assistant/tool) instead of denylist in
convertInputToTanStackAI to exclude activity/reasoning roles
- Change error+abort handler from break to return in aisdk converter
to stop processing after error during abort
- Fix version-pinned comments (remove "AI SDK 5.0" reference)
Test fixes:
- Add timedOut flag to collectEventsIncludingErrors to distinguish
hung observables from normal completion
- Add clearTimeout on success/error to prevent timer leaks
- Remove timing-dependent setTimeout in concurrent run guard test
Docs fixes:
- Split imports: copilotRuntimeNextJSAppRouterEndpoint from
@copilotkit/runtime, Agent/converters from @copilotkit/runtime/v2
- Fix forwardedProps example: use CopilotKit properties prop
- Fix useCopilotChat → useCoAgent for state management reference
- Fix model syntax colon → slash (openai/gpt-4o)
Critical fixes:
- Guard concurrent run() calls — throw if agent is already running
- Check both "output" and "result" properties on AI SDK tool-result events
for compatibility with older AI SDK versions
- Handle undefined/missing error in AI SDK error events with proper
Error wrapping instead of throwing undefined
Important fixes:
- Add TOOL_CALL_RESULT handling to TanStack converter (was silently dropped)
- Include threadId and runId in RUN_ERROR events for run correlation
- Guard state tool events (STATE_SNAPSHOT/STATE_DELTA) against undefined
snapshot/delta fields before emitting
- Wrap JSON.stringify in tool-result with try/catch to handle circular refs
- Fix collectEventsIncludingErrors to distinguish error vs normal completion
Test coverage:
- Add tests for convertInputToTanStackAI (message filtering, tool call
mapping, context injection, state serialization)
- Add tests for state management tool results in AI SDK converter
- Add tests for async factory (Promise-returning) paths
- Add tests for tool-result property compatibility (output vs result)
- Add tests for error event handling edge cases
- Add tests for concurrent run guard and RUN_ERROR correlation fields
- Replace flaky setTimeout-based abort test with signal-based sync
Other:
- Add exhaustiveness check (default: never) in config type switch
- Document clone() @ts-expect-error coupling with AbstractAgent.middlewares
- Fix docs: remove incorrect "v6" version reference, fix wrong hook name
Add Agent class extending AbstractAgent that accepts aisdk, tanstack,
or custom configurations via a discriminated union. Routes to the
appropriate converter, handles RUN_STARTED/FINISHED/ERROR lifecycle,
and supports clone/abortRun. Re-export Agent and converters from the
agent barrel.
Add convertTanStackStream async generator that converts TanStack AI
stream chunks (TEXT_MESSAGE_CONTENT, TOOL_CALL_START/ARGS/END) into
AG-UI events. Create converters/index.ts barrel that re-exports both
AI SDK and TanStack converters.
Extract the AI SDK fullStream event-mapping loop into a standalone
async generator (convertAISDKStream) that yields AG-UI BaseEvent
objects. This is a pure converter with no lifecycle events — the
caller handles RUN_STARTED/FINISHED/ERROR.
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".
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>
- 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>