Commit Graph

135 Commits

Author SHA1 Message Date
Alem Tuzlak 1bae2fc899 Merge branch 'main' into chore/oxlint-new-rules 2026-04-10 19:43:20 +02:00
Alem Tuzlak ff2093102c chore(lint): add new oxlint rules and auto-fix violations
Enable stricter oxlint rules for better code health:
- typescript/consistent-type-imports: enforce `import type` for type-only imports
- typescript/no-import-type-side-effects: prefer top-level type imports
- import/consistent-type-specifier-style: consistent type specifier placement
- typescript/no-unnecessary-type-assertion: bump to error
- react/self-closing-comp: enforce self-closing JSX components
- unicorn/prefer-optional-catch-binding: drop unused catch params
- eslint/no-useless-computed-key: simplify object keys
- unicorn/prefer-string-slice: prefer .slice() over .substring()
- unicorn/prefer-array-flat-map: prefer .flatMap() over .map().flat()

All existing violations auto-fixed via oxlint --fix.
2026-04-10 19:29:32 +02:00
Jordan Ritter 01466e5629 fix: handle RunErrorEvent in proxy agent, state manager, angular agent, and node name hook
When the backend emits RunErrorEvent via the AG-UI protocol, several
components did not handle it:

- ProxiedCopilotRuntimeAgent: isRunning stayed true, causing
  data-copilot-running to never transition to false (infinite spinner)
- StateManager: activeRun entries were never cleaned up, runFinished
  flag never set (stale state on subsequent runs)
- Angular CopilotKitAgent: same isRunning bug as the proxy agent
- useAgentNodeName: node name stuck at last step instead of "end"

onRunErrorEvent is distinct from onRunFailed — the former handles
protocol-level RUN_ERROR events from the backend, the latter handles
local exceptions (network errors, deserialization failures).
2026-04-10 10:25:35 -07:00
Martha Kelly Schumann fdad548be7 Merge branch 'main' into fix/CPK-7154-agent-text-wiped-multiple-tool-calls 2026-04-09 15:38:55 -07:00
Jordan Ritter 1b01725d40 test: add 18 MCP integration tests with aimock MCPMock
Phase 1 — BasicAgent mcpServers (8 tests):
- HTTP transport tool fetch, SSE error handling
- Tool call round-trip, client cleanup, unreachable server error
- Multiple servers merge tools, error-path cleanup, tool descriptions

Phase 2 — MCPAppsActivityRenderer proxy (5 tests):
- tools/call proxy round-trip, error handling
- ui/open-link handler + missing URL error
- Multiple independent activities

Phase 3 — MCPAppsMiddleware edge cases (5 tests):
- Middleware creation, tools/call proxy, resources/read proxy
- Non-proxied request delegation, wrong serverHash error

All tests use real HTTP connections to aimock MCPMock.
2026-04-09 14:58:32 -07:00
Martha Kelly Schumann 8e050a2ab7 Merge branch 'main' into fix/CPK-7154-agent-text-wiped-multiple-tool-calls 2026-04-09 14:51:04 -07:00
Jordan Ritter 0096a91717 fix(react-core): invoke agent after MCP ui/message events
The ui/message handler added messages to the chat but never called
runAgent(), so the agent never processed MCP-sent messages.

- Use copilotkit.runAgent({ agent }) through RunHandler for frontend
  tools, context, tool execution, and abort support
- Send JSON-RPC response immediately after addMessage, before agent run
- Agent run is fire-and-forget with error logging
- followUp parameter: true=always, false=skip, default=user messages only

Fixes #3216
2026-04-09 13:50:39 -07:00
Martha Kelly Schumann 14d752c6e0 Merge branch 'main' into fix/CPK-7154-agent-text-wiped-multiple-tool-calls 2026-04-09 12:59:36 -07:00
github-actions[bot] 1bc4786759 chore: version packages (next) 2026-04-09 18:09:30 +00:00
Martha Kelly Schumann f65b841885 Merge branch 'main' into fix/CPK-7154-agent-text-wiped-multiple-tool-calls 2026-04-09 08:03:40 -07:00
Martha Schumann 22a7f98bcf fix(react-core): address review feedback on deduplicateMessages
- Fix JSDoc: "while keeping the latest toolCalls" was wrong for the ?? case;
  now says "recovers toolCalls from earlier occurrences if the latest is
  undefined" and notes that [] is treated as intentional
- Add @internal annotation to signal export is for testing only
- Remove redundant AssistantMessage casts inside the role-narrowed branch
- Add missing content assertion to "uses latest content" render test
- Add test: [] toolCalls from later chunk is kept (not fallen back from)
- Add test: undefined content on both sides is handled without error
- Add changeset for @copilotkit/react-core patch

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-09 07:58:52 -07:00
github-actions[bot] 0093bdbaa4 chore: version packages 2026-04-09 02:26:42 +00:00
github-actions[bot] c2837a3001 chore: version packages (next) 2026-04-09 01:53:20 +00:00
github-actions[bot] 00cace6abf chore: version packages 2026-04-08 23:54:54 +00:00
Maxim 9ee8e92766 chore: resolve merge conflicts with origin/main
Keep both defaultThrottleMs (our branch) and inspectorDefaultAnchor +
design-skill context registration (main) in CopilotKitProvider.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 22:58:45 +02:00
Maxim 7a99322e77 fix: harden throttleMs cascade — setter validation, timing fix, JSDoc accuracy, tests
- Core setter rejects invalid values (NaN, Infinity, negative) instead of
  storing them, preventing garbage from reaching downstream consumers.
- Provider initializes defaultThrottleMs synchronously during instance
  creation so child hooks see the correct value on their first render.
- Remove phantom notificationThrottle JSDoc reference (API does not exist).
- Remove dead "default" branch in error source detection.
- Fix misleading @default comments re: 0-vs-undefined semantics and
  cascade direction.
- Replace React-specific "re-renders" language in framework-agnostic core.
- Add test for dynamic provider defaultThrottleMs changes.
- Add tests for core setter validation behavior.
- Add defaultThrottleMs to renderWithCopilotKit test helper.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 22:05:22 +02:00
Markus Ecker 05ddcfc12a feat(react-core): Open Generative UI — sandboxed HTML/CSS/JS rendering via iframe
AI-generated UI streamed into chat via websandbox iframe (allow-scripts only,
no allow-same-origin), progressive HTML preview with throttled updates,
one-shot auto-resize, sandboxFunctions for host-to-iframe calls, designSkill
styling guidelines, A2UI catalog context injection.
2026-04-08 12:49:38 -07:00
Maxim cb7bfae188 refactor: move defaultThrottleMs from React context onto CopilotKitCore instance
Addresses Tyler's review comment on #3657 — defaultThrottleMs is configuration
that belongs on the copilotkit instance, not as a sibling field in the React
context value.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 21:44:47 +02:00
Maxim a8e05401e7 fix: improve throttleMs validation, JSDoc accuracy, and test coverage
- Add source attribution to error messages (hook-level vs provider-level)
- Add eager validation of defaultThrottleMs in CopilotKitProvider
- Add tests for invalid provider defaultThrottleMs (NaN, Infinity, -1)
- Remove redundant resolved !== 0 guard in validation
- Fix JSDoc: "streaming" → "message change notifications", "passed
  directly" → "forwarded", align @default terminology
- Extract shared mock helpers to reduce test duplication

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 21:31:28 +02:00
Maxim c8db6476a8 test: verify CopilotSidebar/CopilotPopup inherit throttleMs type
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 21:14:51 +02:00
Maxim a7c57cef9c feat: add throttleMs prop to CopilotChat, forwarded to internal useAgent
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 21:11:46 +02:00
Maxim e30d27b46b test: guard throttleMs:0 override of provider defaultThrottleMs
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 21:07:57 +02:00
Maxim af98091bda feat: add defaultThrottleMs to CopilotKitProvider, read as fallback in useAgent
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 20:49:57 +02:00
Maxim ad4271c266 test(react-core): extract shared helpers and document casts in cache tests
Extract renderAndWarmCache() and installBoundingRectSpy() helpers to
eliminate duplicated setup across 4 tests. Add justifying comments on
all necessary type casts (MockResizeObserver, CSSStyleDeclaration,
TextMetrics, canvas getContext overloads).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 19:22:33 +02:00
Maxim 35e35bb5bc fix(react-core): preserve cache across layout toggles and reject zero-width entries
Self-inflicted resizes (compact↔expanded) were clearing the cache before
the ignoreResizeRef guard, forcing a full re-measurement on the very next
keystroke. Move the guard ahead of invalidation so layout toggles keep the
cache warm — container dimensions don't change during these transitions.

Also reject compactWidth <= 0 from being cached, preventing a zero-width
entry from silently disabling text-width expansion until the next resize.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 18:54:25 +02:00
Maxim 84d531c1e3 fix: address review feedback — single observer, fresh font read, test coverage
- Merge two ResizeObserver instances into one; inspect entry.target to
  decide cache invalidation.  Eliminates the ignoreResizeRef race where
  two observers could consume the guard independently.
- Move textarea font read out of the cache into evaluateLayout so CSS/
  theme changes are picked up without a container resize.
- Return { compactWidth } | null from updateContainerCache; callers use
  the value directly instead of the two-step "call then check ref" pattern.
- Hoist cache invalidation above the ignoreResizeRef guard to remove
  duplicated containerCacheRef.current = null.
- Add zero-width guard in updateContainerCache to avoid caching
  compactWidth: 0 when the container is hidden.
- Replace vi.fn() as any ResizeObserver mock with a class-based
  MockResizeObserver that tracks observed targets, enabling per-target
  resize triggers in tests.
- Fix empty-font and null-canvas tests to assert data-layout="compact"
  (verifying the fallback path) instead of just non-null.
- Add tests: warm-cache keystroke perf (getBoundingClientRect not called),
  textarea-only vs container resize invalidation, ignoreResizeRef + cache
  invalidation during layout toggle.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 18:39:22 +02:00
Maxim f35df00b16 test(react-core): improve test hygiene in container dimension cache tests
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>
2026-04-07 23:49:24 +02:00
Maxim 8da887c14c fix(react-core): harden container dimension cache in CopilotChatInput
- 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>
2026-04-07 23:36:47 +02:00
Maxim 27bde85b6f test(react-core): add tests for container dimension cache in CopilotChatInput
- 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>
2026-04-07 23:10:10 +02:00
Maxim 8986a47220 fix(react-core): harden container dimension cache in CopilotChatInput
- 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>
2026-04-07 22:56:12 +02:00
Maxim d9125fc076 perf(react-core): cache container dimensions in CopilotChatInput to reduce per-keystroke reflows
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>
2026-04-07 22:24:17 +02:00
Martha Schumann 191fdc4999 fix(react-core): recover toolCalls when later chunk sends undefined; add test
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>
2026-04-07 12:21:56 -07:00
Martha Schumann 53b7fa206d chore: merge main into fix/CPK-7154-agent-text-wiped-multiple-tool-calls
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 12:10:59 -07:00
Martha Schumann 1a61be49dc refactor(FOR-75): replace useJsonStable with useShallowStableRef; add unit tests and changeset
- 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>
2026-04-07 11:00:19 -07:00
Martha Schumann 56cc194782 refactor(react-core): extract useShallowStableRef; fix Test 2 to guard labels regression
- 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>
2026-04-07 10:41:46 -07:00
Martha Schumann 35481074bd refactor(react-core): replace eslint-disable comments with useJsonStable hook
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>
2026-04-07 10:30:26 -07:00
Martha Schumann cdbc3457cf fix(react-core): stabilize messageView/labels props to prevent message re-renders on keystroke
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>
2026-04-07 09:55:34 -07:00
Martha Kelly Schumann 30b81395f6 Merge branch 'main' into feat/CPK-7190-chat-performance 2026-04-07 08:51:33 -07:00
Martha Schumann 82d58fae9b chore: merge main into feat/CPK-7190-chat-performance
Resolves conflict in CopilotChat.tsx — takes main's attachment-aware
messagesMemoKey fingerprint (scans all messages, avoids JSON.stringify
on base64 data) which subsumes our all-tool-calls fix.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 08:35:29 -07:00
Alem Tuzlak 39b5f671c0 Merge branch 'main' into claude/remove-endpoint-flag-xd3G1 2026-04-07 14:47:09 +02:00
Alem Tuzlak 4b11b995a2 feat: multimodal attachments — images, audio, video, documents in CopilotChat (#3604)
## Summary

Adds full multimodal attachment support to CopilotKit's v2 chat
components. Users can send images, audio, video, and documents alongside
text messages to any AG-UI-compatible agent.

### Frontend
- **`attachments` prop** on `CopilotChat` — `{ enabled, accept, maxSize,
onUpload, onUploadFailed }`
- **`useAttachments` hook** — reusable, referentially stable hook for
custom chat UIs (all callbacks memoized, zero unnecessary re-renders)
- **Attachment queue** with lightbox previews (View Transition API),
video player, PDF/text document viewer
- **Drag-and-drop**, **clipboard paste**, and **file picker** upload
flows
- **Drop zone overlay** with visual indicator
- **Filename preservation** via `InputContent` metadata
- **`onUploadFailed` callback** with typed error reasons
(`file-too-large`, `invalid-type`, `upload-failed`)
- **`AttachmentUploadResult` discriminated union** for type-safe
`onUpload` returns
- Memoized attachment renderer components (v1 and v2)

### Backend
- `BuiltInAgent` properly converts multimodal `InputContent[]` to Vercel
AI SDK format
- Handles image, audio, video, document, and legacy binary content parts

### Deprecation lifecycle
- `@deprecated` JSDoc on all legacy image upload APIs (9 symbols) with
`@since 1.56.0`
- **Codemod** at `codemods/migrate-attachments.ts` — transforms props +
imports (18 tests)
- Migration guide moved to new **Migration Guides** docs section

### Docs
- New guide: `docs/(root)/multimodal-attachments.mdx`
- Updated migration guide with codemod instructions, new `onUpload`
type, metadata
- Cross-links from prebuilt-components page
- New **Migration Guides** section (moved from troubleshooting)

### AG-UI
- Bumped `@ag-ui/client`, `@ag-ui/core`, `@ag-ui/encoder`,
`@ag-ui/proto` to 0.0.51

### Example
- React Router example with dual agent setup (BuiltInAgent + TanStack
AI)
- Multimodal content properly forwarded to TanStack AI `chat()`

## Test plan

- [x] `useAttachments` hook stability tests — 9 tests (referential
stability, re-render counting, state defaults)
- [x] `CopilotChat.attachments` tests — 5 tests (onUploadFailed for
invalid-type, file-too-large, upload-failed, multiple rejections, valid
files)
- [x] Codemod tests — 18 tests (prop transforms, import renames,
idempotency, edge cases)
- [x] Full react-core suite — 966 tests passing
- [x] Tool call streaming tests pass (messagesMemoKey fingerprint fix)
2026-04-07 14:26:03 +02:00
Ran Shemtov 0a262b3ad6 Merge branch 'main' into claude/remove-endpoint-flag-xd3G1 2026-04-07 12:55:33 +02:00
Alem Tuzlak 773a4a6415 fix: format package.json files 2026-04-07 12:29:29 +02:00
Alem Tuzlak 0aca4e88bc fix: address review feedback — concurrent-safe consumeAttachments & scope-aware codemod
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.
2026-04-07 12:14:14 +02:00
Alem Tuzlak 1317e51302 fix: revert copy-dts script, use typesVersions for legacy TS consumers
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".
2026-04-07 11:39:15 +02:00
Alem Tuzlak 136bfc1182 Merge branch 'main' into feat/multimodal-builtin-agent 2026-04-07 09:51:46 +02:00
Alem Tuzlak 9ae7a97823 fix: address PR review feedback from Martha
Bugs fixed:
- v1 Chat.tsx: onUpload now reads result.type/result.value (was result.data/result.url)
- v1 Chat.tsx: metadata captured from onUpload and forwarded in InputContent parts
- v1 Chat.tsx: onUploadFailed callback now invoked at all three error paths
- useAttachments: consumeAttachments preserves in-flight uploads (was dropping them)

Should-fix addressed:
- Lightbox: vtCounter replaced with React.useId() (concurrent mode + SSR safe)
- Lightbox: SSR guard before createPortal(document.body)
- useBlobUrl: useEffect deps changed to scalar values (was object reference)
- Codemod: dynamic inputFileAccept expressions preserved (was silently dropped)
- Codemod: scope-awareness limitation documented in comment

Tests: 966/966 react-core, 19/19 codemod
2026-04-07 09:42:28 +02:00
Benjamin Taylor 3dc6963956 fix(threads): Improve useThreads DX with clearer names and direct type hints 2026-04-06 16:50:46 -07:00
Benjamin Taylor 9413015aa6 fix(react-core): clear copy button timeout on unmount
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>
2026-04-06 16:19:52 -07:00
Martha Schumann ccfff2d84b refactor(react-core): extract deduplicateMessages, add useMemo, expand test coverage
- 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>
2026-04-06 11:46:46 -07:00