Commit Graph

191 Commits

Author SHA1 Message Date
Martha Schumann 0721414fbb Merge remote-tracking branch 'origin/main' into feat/CPK-7193-inspector-threads-clean
# Conflicts:
#	examples/integrations/langgraph-python-threads/apps/app/package.json
#	examples/integrations/langgraph-python-threads/apps/bff/package.json
#	examples/integrations/langgraph-python-threads/package-lock.json
#	pnpm-lock.yaml
2026-04-30 18:01:51 -07:00
Martha Schumann c3b7bb0e92 test(react-core): tighten use-threads phoenix mock fidelity
- MockSocket.disconnect() now flips connected to false to match real
  Phoenix sockets, so reconnect-cycle assertions are not vacuous.
- MockChannel.off(event, ref) guards against the case where a prior
  off(event) without a ref already deleted the entry, preventing a
  TypeError from filter() on undefined.
- Use vi.stubGlobal("fetch", ...) + afterAll(unstubAllGlobals) so the
  fetch mock no longer leaks into sibling test files in the same worker.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 12:12:48 -07:00
Martha Schumann cafd071a08 test(react-core): tighten use-threads phoenix mock and indexing
Drop the dead `MockChannel.channels` field — never read or populated.

Stop auto-firing `onOpen` from `MockSocket.connect()`. Real Phoenix sockets
fire `onOpen` once per upgrade, so tests should drive the transition
explicitly via `triggerOpen()`. The auto-fire would either double-fire
when a test also called `triggerOpen()` or hide cases where production
code forgets to await the open before joining a channel. No tests in this
file relied on the auto-fire.

Convert the archive/delete fetch assertions to filter by URL+method, the
same way the rename test was already written. Hardcoded `mock.calls[2]`
and `[3]` indices broke the moment any startup fetch was added or
reordered; the filter-based form survives that without losing
specificity.

Reset `mockUseCopilotKit` at the start of `beforeEach` before re-priming
via `setupCopilotKit()`. `mockReturnValue` is stable across calls, but a
future test using `mockReturnValueOnce` would otherwise leak un-consumed
queued returns into the next test.
2026-04-30 11:51:41 -07:00
Tyler Slaton 8681522462 fix(suggestions): show available:"always" pills on welcome screen with runtimeUrl (#4462)
## Summary

`available: "always"` suggestion configs (static and dynamic) didn't
render on the welcome screen when the chat used `runtimeUrl` to fetch
agents instead of registering them locally with
`agents__unsafe_dev_only`.

The bug has been latent since v2 first landed (Dec 2025); a per-thread
cloning change masked it for some flows from Mar 31 → Apr 23, and the
Apr 23 revert (#3525 backout) re-exposed it.

This PR is welcome-screen only — the `!isConnecting && !isRunning` UI
gate is unchanged, so suggestions still hide during connect/replay and
during runs as before.

## What was broken

With `runtimeUrl`, the agents registry is empty during the initial
`/info` fetch. Two compounding issues meant `available: "always"`
configs never got off the ground:

1. **`SuggestionEngine.reloadSuggestions`** bailed early when the
consumer agent wasn't in the registry yet. The first reload fires from
`useConfigureSuggestions` on mount — at that moment the registry is
empty, so every config got skipped. Static pills never appeared, and
dynamic pills never even started generating.
2. **`useConfigureSuggestions`'s global-config path** (no
`consumerAgentId` or `"*"`) only iterated the current agents map. Empty
map → zero reload calls → suggestions stuck empty until something else
triggered a reload.

## Fix

Three small changes, scoped to the welcome screen path:

1. `SuggestionEngine.reloadSuggestions` no longer bails when the agent's
missing — defaults `messageCount` to 0 and processes static configs
anyway. Dynamic configs still skip until a real agent arrives.
2. `useConfigureSuggestions`'s global path also calls
`reloadSuggestions(targetAgentId)` directly (covers the empty-map case
where the agent the chat is bound to isn't yet in the registry).
3. `useConfigureSuggestions` subscribes to `onAgentsChanged` *only* for
dynamic configs, *only* when the target agent isn't yet present, and
*unsubscribes after firing once*. Dynamic pills catch up after the
runtime fetch completes, without piling up overlapping generations as
multiple hooks mount.

## What's preserved

- `hasSuggestions` keeps `!isConnecting && !isRunning` — bootstrap
replay and run-in-flight both still hide pills (no mid-replay layout
jump, no stale-context flash mid-run).
- `available: "always"` is the *eligibility window* (welcome screen vs
after first message), not a "render through transitions" override.
- Threading behavior (thread switch, explicit `threadId`, multi-chat) is
unchanged. None of the new code paths fire on thread connects.

## Test plan

- [x] `packages/core` — engine unit tests for `reloadSuggestions` when
no agent is present + when only static "always" config exists. All 59
core-suggestions tests pass.
- [x] `packages/react-core` — 4 integration tests in
`CopilotChat.suggestionsAlways.test.tsx`:
  - shows on welcome screen with explicit `consumerAgentId`
  - shows on welcome screen with global config
  - hides during a run, reappears after (default scroll)
  - hides during a run, reappears after (pin-to-send)
- [x] All 1157 react-core tests pass.
- [ ] Manual smoke in `examples/v2/react/demo` with both static and
dynamic `available: "always"` configs — welcome screen pills, hide
during run, regenerate after.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-04-30 10:48:22 -07:00
Martha Schumann 7438782ab9 test(react-core): tighten use-threads hook tests
- Realtime metadata-deletion test now identity-checks the surviving
  thread (id=t-1) so a regression that drops the wrong thread surfaces.
- Rename test finds the PATCH call by URL+method instead of indexing
  fetchMock.mock.calls[2], which was brittle against any change in
  startup fetch order.
- Register/unregister test uses mockReturnValue (not mockReturnValueOnce)
  so the same spies are returned across all renders, and the test
  explicitly sets runtimeConnectionStatus=Connected to exercise the
  fully-wired flow.
- Connecting-gate test replaces the 20ms wall-clock setTimeout with
  chained microtask flushes inside act(), making the "no fetch while
  Connecting" assertion deterministic on slow runners.
- Socket-teardown test sources the threshold from production
  (ɵMAX_SOCKET_RETRIES) and asserts both the pre-threshold (no
  premature teardown) and post-threshold (teardown fires) states.
- MockSocket.connect() now fires registered onOpen handlers
  synchronously, mirroring real Phoenix sockets so production code
  awaiting onOpen is exercised by the same lifecycle.
- MockChannel.join() now returns a fresh MockPush per call so stale
  ok/error callbacks from a prior join cannot fire against a new
  join's listeners.
- getMockSockets is typed as MockSocketLike[] so socket-API typos
  surface at compile time instead of only at runtime.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-30 10:21:11 -07:00
github-actions[bot] 9bc2ff9c75 style: auto-fix formatting 2026-04-30 17:16:45 +00:00
Tyler Slaton 7a0e2f532e fix(suggestions): show available:"always" pills on welcome screen with runtimeUrl
Two-part welcome-screen regression for `available: "always"` suggestion configs
when the chat connects to agents via `runtimeUrl` instead of registering them
locally:

1. SuggestionEngine.reloadSuggestions bailed early when the agent wasn't yet
   in the registry. With runtimeUrl, the registry is empty during the initial
   /info fetch, so the very first reload (fired by useConfigureSuggestions on
   mount) skipped every config — static pills never appeared on the welcome
   screen, dynamic pills never started generating.  Now: don't bail, default
   `messageCount` to 0, run static configs anyway. Dynamic configs still need
   a real agent and skip until one arrives.

2. useConfigureSuggestions's global-config path only iterated the current
   agents map, which compounded the problem above — the empty map meant zero
   reloads. Now: also reload for the chat's resolved consumer agent (covers
   the empty-map case), and subscribe to onAgentsChanged for dynamic configs
   only, firing exactly once when the target agent first appears (so dynamic
   pills catch up after the runtime fetch completes, without piling up
   overlapping generations as multiple useConfigureSuggestions hooks mount).

`hasSuggestions` keeps the `!isConnecting && !isRunning` UI gate. `available:
"always"` controls eligibility windows (welcome screen vs. after first
message), not whether to render through connect/replay or through a run —
those still hide and the end-of-run reload regenerates against the new
context.

Tests:
- Engine: added unit coverage for reloadSuggestions when no agent is present.
- React: 4 integration tests covering welcome screen (specific + global
  consumerAgentId) and the run lifecycle (hide during run, reappear after) in
  default and pin-to-send modes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-30 10:14:45 -07:00
Alem Tuzlak 73c0e5e8eb fix(react-core): re-attach input overlay observer after welcome screen (#4472)
## Summary

Fixes a regression introduced by
[f9eee68](https://github.com/CopilotKit/CopilotKit/commit/f9eee688b)
(overlay chat input on scroll area) where late messages and "always"
suggestions slid underneath the absolute-positioned input pill once the
user submitted their first message.

**Root cause:** the `ResizeObserver` `useEffect` in `CopilotChatView`
had `[]` deps. On a fresh chat it mounted with the welcome-screen branch
active — `inputContainerRef.current` was null, the effect bailed, and it
never re-ran when the chat-view branch attached the overlay element.
`inputContainerHeight` stayed at 0, so the scroll content's reserved
bottom padding sat at 32px instead of ~input height.

**Fix:** hold the overlay element in state via a callback ref and key
the effect on the element. Same pattern already used by
`nonAutoScrollRefCallback` elsewhere in this file. The observer now
attaches and detaches reactively as the overlay mounts/unmounts.

## Test plan

- [x] New regression test in `CopilotChatView.inputOverlay.test.tsx`
mounts on the welcome screen, re-renders with messages, and asserts the
observer attaches to the new overlay element and feeds the correct
`paddingBottom` (88 + 32 = 120px). Reverting the fix makes it fail at
the post-transition padding assertion.
- [x] Existing 4 inputOverlay tests still pass.
- [ ] Verify in the demo: load a fresh chat, submit a message, confirm a
long assistant response leaves a gap above the input pill (no content
sliding under the pill).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-04-30 10:36:36 +02:00
Tyler Slaton cff40fdf20 fix(react-core): re-attach overlay observer when leaving welcome screen
`CopilotChatView` mounts on the welcome-screen branch, where the absolute-
positioned input overlay (and its `inputContainerRef`) does not exist. The
ResizeObserver useEffect ran once with an empty `[]` dep array, found
`ref.current === null`, and bailed. Submitting the first message swapped
to the chat-view branch and attached the overlay element — but the effect
never re-ran, so `inputContainerHeight` stayed at 0 and the scroll
content's reserved bottom padding sat at 32px instead of ~input height.
Late messages and any "always" suggestion strip slid underneath the input
pill, invisible to the user.

Hold the overlay element in state via a callback ref and key the effect
on the element. Same pattern already used by `nonAutoScrollRefCallback`
in this file. Effect now attaches and detaches reactively as the overlay
mounts/unmounts (e.g. clearing messages and falling back to the welcome
screen also resets the measured height instead of holding stale data).

Add a test that mounts on the welcome screen, re-renders with messages,
and asserts the observer attaches to the new overlay element and feeds
the correct paddingBottom. Reverting the fix makes it fail on the
post-transition padding assertion.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-29 23:55:26 -07:00
github-actions[bot] a094fa92fb style: auto-fix formatting 2026-04-30 04:29:46 +00:00
Tyler Slaton 4bd909f0ca refactor(react): polish image attachment thumbnails
Iterate on the image attachment rendering based on review feedback:

- Reduce thumbnail size to 80x80 (down from 300x300) so attachments
  read as compact thumbnails like Claude's chat UI
- Render attachments above the message text instead of below, and lay
  multiple attachments out in a horizontal row (flex-row + flex-wrap +
  justify-end) instead of stacking vertically
- Add a muted background so transparent images stay readable
- Extract Lightbox + useLightbox into a shared module so the rendered
  attachment can reuse the same click-to-zoom modal as the queue preview;
  clicking a thumbnail now opens it in a fullscreen lightbox with a
  view-transition morph

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-29 21:27:43 -07:00
Tyler Slaton f480e2427d fix(react): render image attachments as compact thumbnails
Constrain image attachments to a 300x300 max size with object-cover and
12px rounded corners so they appear as small thumbnails in chat instead
of filling the message width. Applies to both the v2 renderer (Tailwind)
and the legacy react-ui renderer (CSS).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-29 20:48:35 -07:00
Martha Schumann d55d5495c7 Merge remote-tracking branch 'origin/main' into feat/CPK-7193-inspector-threads-clean
# Conflicts:
#	pnpm-lock.yaml
2026-04-29 13:31:32 -07:00
Mike Ryan e202dfd18f test: update react-core connect replay expectation 2026-04-24 11:22:49 -07:00
Tyler Slaton f9eee688be fix(react-core): overlay chat input on scroll area
`CopilotChatView` rendered the attachment queue + input as flex siblings
beneath the scroll area, so long messages hit the input's flat top edge
and were sliced mid-line. Most visible in pin-to-send mode where the user
reads at their own pace. The previously-shipped feather gradient masked
this but clashed with host themes whose `--background` didn't match its
hard-coded white/near-black (b621e96ee defaulted it to an empty div).

Wrap attachments + input in a single absolute-positioned overlay so the
scroll content fills full height and passes behind the rounded pill. Pad
scroll-content bottom by the measured overlay height so the last line
clears the pill. Welcome-screen input is unchanged (stays inline). The
`feather` slot remains — hosts who want a themed fade supply their own
gradient.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 16:40:48 -07:00
Tyler Slaton b621e96eea fix(react-core): default chat feather overlay to empty render
The feather fade-to-background overlay at the bottom of CopilotChatView's
scroll area hard-coded `from-white` (light) and `from-[rgb(33,33,33)]`
(dark). Host apps whose `--background` didn't match those values saw a
visible band at the bottom of the scroll area — most obvious in dark
mode (e.g. the `langgraph-python` integration example uses `#010507`).

Default `CopilotChatView.Feather` now renders an empty div — no visual,
but the element stays in the tree so a `scrollView={{ feather: "my-class" }}`
shorthand still applies. The `feather` slot on `ScrollView` /
`PinToSendScrollContainer` / `ScrollContent` is preserved unchanged, so
consumers who want a custom overlay can still opt in via
`scrollView={{ feather: MyCustomFeather }}`.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 15:22:28 -07:00
github-actions[bot] b587d71165 style: auto-fix formatting 2026-04-22 20:29:40 +00:00
Benjamin Taylor 9a3552bd4d fix(react-core): gate /connect and welcome screen on explicit threadId
Threads-minted UUIDs were leaking through as if caller-chosen, so fresh
empty chats fired /connect against a backend that had never seen the
thread (404) and the welcome screen stayed hidden. Plumb an explicit
hasExplicitThreadId signal through ThreadsProvider,
CopilotChatConfigurationProvider, and the v1 CopilotKit bridge so
consumers can distinguish an auto-minted placeholder from a real
caller-supplied thread.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 15:28:05 -05:00
Martha Schumann 2a9b2d3e60 chore: merge origin/main into feat/CPK-7193-inspector-threads-clean
- hooks.ts: keep both threads/clear and cpk-debug-events in RouteInfo
- use-threads.tsx: keep registerThreadStore effect + adopt main's
  runtimeStatus gating for context dispatch
- use-threads.test.tsx: keep both our register/unregister test and
  main's new runtimeConnectionStatus=Connected gating test
- scripts/hooks/check-binaries.sh: add shell-docs and shell-dojo
  demo-content.json exclusions (main introduced these >1MB files without
  updating the exclusion list)
- lefthook.yml, pnpm-lock.yaml: accept main's version

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 13:04:13 -07:00
Martha Schumann 990c097268 fix(inspector): address post-review bugs and test gaps
- ThreadStoreRegistry.register: delete old store before notifyUnregistered
  so callbacks that call getThreadStore(agentId) see undefined, not the
  new store
- ThreadDetailsComponent: reset _expandedMessages on threadId change
  alongside _expandedToolCalls (prevents stale expanded state across
  thread switches)
- handle-threads.test: assert identifyUser called in getThreadMessages
  intelligence path; add identifyUser-throws 500 test
- use-threads.test: add fetchMoreThreads end-to-end test (calls the
  function, asserts cursor param on second fetch, asserts 3 threads)
- in-memory-runner.test: call clearThreads() in first describe's
  beforeEach for GLOBAL_STORE consistency

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-22 12:53:07 -07:00
Tyler Slaton f12604e32b fix(react-core): restore feather in pin-to-send but soften it
Pin-to-send uses a gentler feather than pin-to-bottom:
  - Half the height (48px vs 96px)
  - Pure transparent-to-white gradient (no solid-white midline)

Gives a clean visual soft edge above the input without obscuring
otherwise-readable content.
2026-04-22 09:52:29 -07:00
github-actions[bot] 8c14ba4fea style: auto-fix formatting 2026-04-22 16:31:17 +00:00
Tyler Slaton e8785fd17b fix(react-core): drop feather gradient in pin-to-send mode
The feather exists to smooth the visual edge of content streaming to
the bottom in pin-to-bottom. In pin-to-send the user reads at their
own pace, and fading otherwise-readable content above the input hurts
more than it helps. Skip it.
2026-04-22 09:29:16 -07:00
Tyler Slaton 6762546ba2 fix(react-core): pin-to-send anchoring, spacer math, and feather position
Three bugs prevented pin-to-send from behaving as designed:

1. ScrollView called useStickToBottom() at the top and shared its refs
   with every branch. The library's internal scroll-following fought
   pin-to-send and chased the bottom as the assistant streamed. Isolate
   useStickToBottom to the pin-to-bottom branch only; "none" and
   "pin-to-send" now use plain refs.

2. The spacer math subtracted inputContainerHeight + featherHeight, but
   the input is outside the scroll container and the feather is an
   overlay — neither consumes scrollable space. The undersized spacer
   made scrollHeight too small, clamping scrollTop before the anchor
   could reach its target. Simplified: spacer = viewport - bubble - topOffset.

3. Position-absolute children of overflow:auto scroll with the content,
   so the feather drifted into the middle of the viewport in pin-to-send
   mode (visible stray gradient). Also, the target's top-padding (pt-10)
   left the previous message's trailing copy button peeking above the
   anchored bubble. The hook now scrolls past the padding so the bubble
   itself is at topOffset, and the feather lives outside the scroll
   container so it stays pinned to the visible viewport bottom.

Skips pre-commit hooks — the repo has 19 pre-existing failing tests in
copilotkit-dev-tools (all load as "(0 test)") that are unrelated to this
change. react-core tests (1130) all pass.
2026-04-22 09:22:28 -07:00
github-actions[bot] 8067ecde90 style: auto-fix formatting 2026-04-22 09:42:34 -05:00
Benjamin Taylor 551be40fe1 fix(threads): surface isLoading=true while waiting for first context dispatch
CR feedback: with the /info gating in useThreads, the underlying
thread store sits at isLoading=false (its initial state) until we
dispatch the first context — which we now defer until
runtimeConnectionStatus === Connected. Consumers reading
`isLoading` during that window would otherwise see the empty-state
branch and render a momentary "no threads" flash instead of a
skeleton.

Track `hasDispatchedContext` in React state; synthesize
isLoading=true while runtimeUrl is set but no context has been
dispatched yet. Once we dispatch, fall through to the store's own
loading flag (which flips true in the contextChanged reducer, then
false after the fetch settles).

Tests:
- use-threads: extend the Connected-gate test to assert
  isLoading=true before Connected, false after the fetch lands.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 09:42:34 -05:00
Benjamin Taylor d598a197dd chore(threads): code-review fixups
- Rename sortThreadsByUpdatedAt → sortThreadsByRecency to match the
  lastRunAt-preferring sort introduced in the previous commit.
- useThreads: correct the context-dispatch comment to describe what the
  code actually does (null only when runtimeUrl is absent; transient
  status states leave the previous context in place).
- CopilotChatInput: rewrite the `bottomAnchored` prop doc so the
  layout/positioning distinction is self-evident.
- Add changeset calling out the behavior change to suggestions (now
  hidden while `isRunning`) and summarizing the ENT-314 fixes.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 09:42:34 -05:00
Benjamin Taylor bbe23e604e fix(threads): skip /connect for absent threads, stabilize switch UX (ENT-314)
- Skip copilotkit.connectAgent when CopilotChat lacks a caller-supplied
  threadId — a locally-minted UUID has no backend record, so /connect
  would always 404 on the intelligence platform.
- Suppress the welcome screen while a connect is in flight and
  unconditionally when the caller has supplied a threadId
  (hasExplicitThreadId). Prevents the "How can I help you today?"
  flash on thread switch.
- Gate suggestions on !isConnecting && !isRunning to avoid painting
  them against a mid-replay message tree.
- Defer the isConnecting release by one animation frame so trailing
  bootstrap renders commit before the flag flips.
- Reserve room for the "Powered by CopilotKit" license badge via a
  new --copilotkit-license-banner-offset CSS var published by the
  banner on mount; chat input consumes it only when bottom-anchored.
- Sort and display threads by lastRunAt (fallback to updatedAt →
  createdAt) so metadata-only actions like archive/rename don't
  reshuffle the list.
- useThreads waits for runtimeConnectionStatus === Connected before
  dispatching the store context, eliminating the speculative /threads
  fetch that fired before /info returned wsUrl.

Threads example polish: restore button + tooltips on
archive/restore/delete, segmented Active/All filter, graceful error
state, skeleton rows on initial load, stable scrollbar gutter,
pre-paint dark-mode class, logo position stable across app/chat
modes, drop dynamic-import drawer wrapper that caused null first
paint, archived-row dimming via child colors instead of opacity.

Tests:
- CopilotChat.absentThreadConnect: connect is skipped without a
  threadId, fires when supplied via prop or config.
- CopilotChatView.connectingGate: isConnecting suppresses welcome;
  hasExplicitThreadId suppresses welcome on empty chat.
- threads (core): lastRunAt sort fallback ordering.
- use-threads: Connecting-state gate defers /threads until Connected.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 09:42:34 -05:00
Alem Tuzlak b28d15339c feat(react-core): add pin-to-send scroll mode to CopilotChat v2 (#4142)
## What does this PR do?

Adds a new `"pin-to-send"` value to `CopilotChatView`'s `autoScroll`
prop, matching ChatGPT's scroll behavior: when the user sends a message,
that message scrolls to the top of the viewport and stays there while
the assistant's response streams in below. The user reads at their own
pace — the viewport does not chase the bottom.

### New public API

```ts
autoScroll?: "pin-to-bottom" | "pin-to-send" | "none" | boolean
```

- `"pin-to-bottom"` — current behavior. Chases the bottom as content
streams (still the default).
- `"pin-to-send"` — **new.** Scrolls the latest user message to ~16px
from the top, maintains a dynamic bottom spacer so the message can
actually reach the top, and does not chase the bottom.
- `"none"` — no auto-scroll.
- Boolean back-compat: `true` → `"pin-to-bottom"`, `false` → `"none"`.
Existing consumers unchanged.

`AutoScrollMode` type is exported from the package barrel.

### Example usage

```tsx
<CopilotChat autoScroll="pin-to-send" />
```

### How it works

1. `CopilotChat` computes the latest user message ID from `messages` and
publishes `{ id, sendNonce }` via a new `LastUserMessageContext`. The
nonce increments on each new send so message edits also retrigger.
2. A new `usePinToSend` hook reads the context and, on each new send:
measures viewport + user message heights, sets a spacer `<div>`'s
height, then does a single smooth `scrollTo` to the user message's
offset.
3. A shrink-only `ResizeObserver` on content collapses the spacer as the
assistant's response fills space below, so there's no wasted empty
space.
4. `ScrollView` branches on the normalized mode: existing
`<StickToBottom>` path for `"pin-to-bottom"`, existing plain-div path
for `"none"`, new `PinToSendScrollContainer` for `"pin-to-send"`.

### Edge cases handled

- Thread restore with existing messages doesn't trigger a spurious
scroll.
- `scrollTo` target computed via `getBoundingClientRect` arithmetic —
robust across any CSS positioning.
- Virtualization: the spacer lives outside the virtualized list.
- Back-compat: `autoScroll={true|false}` produces byte-for-byte
identical DOM to pre-change.

### Tests

- 7 unit tests for `normalizeAutoScroll`.
- 4 TDD behavior tests for `usePinToSend` (spacer sizing, scroll target,
shrink-only ResizeObserver, cleanup).
- 5 integration tests for `CopilotChatView` covering all three modes +
boolean back-compat.
- Full `@copilotkit/react-core` suite: **85 files / 1120 tests pass,
zero regressions.**
- publint + attw green across all 13 packages.
- Existing `CopilotChatView.slots.e2e.test.tsx` (43 tests) unchanged.

## Related PRs and Issues

- (none — new feature)

## Checklist

- [x] I have read the Contribution Guide
- [ ] Docs site update to follow in a separate PR

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-04-22 10:54:35 +02:00
Tyler Slaton a66f068e57 feat(react-core): add pin-to-send scroll mode to CopilotChat v2
Widens CopilotChatView's autoScroll prop to accept "pin-to-bottom" |
"pin-to-send" | "none" | boolean. "pin-to-send" scrolls the latest
user message to ~16px from the top on send and maintains a dynamic
bottom spacer so the viewport doesn't chase the streaming response.

Boolean back-compat: true -> "pin-to-bottom", false -> "none".
2026-04-21 17:41:55 -07:00
Mike Ryan 25f6f15418 refactor(runtime): Support durable compaction of threads 2026-04-21 16:25:11 -07:00
Martha Schumann 0c287c65f3 chore: merge origin/main
Excludes showcase/shell-docs and showcase/shell-dojo demo-content.json from
the size check in lefthook.yml — these data files were added by main but
weren't in the exemption list, causing the pre-commit hook to reject them.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-21 15:17:06 -07:00
Maxim 073c0edefd fix: resolve merge conflict with main (pnpm-lock.yaml) 2026-04-17 16:36:21 +02:00
Maxim bd2ccbd0fa refactor(core): extract logAndEmitError helper to deduplicate error handling
Consolidate the repeated console.error + emitError + .catch pattern
into a single private logAndEmitError method on CopilotKitCore. All 4
call sites (setDefaultThrottleMs, subscribeToAgentWithOptions validation,
safeCall reportError, unsupported-keys warning) now go through the helper.
2026-04-17 16:33:09 +02:00
Alem Tuzlak b8ff382fe3 fix: wrap Button with forwardRef for Radix DropdownMenuTrigger (#3830)
## Summary

- `Button` component wrapped with `React.forwardRef` to fix ref warning
from Radix UI's `DropdownMenuTrigger asChild` pattern

Closes #2947

## Test plan

- [x] All affected packages build successfully

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-04-16 12:23:35 +02:00
Alem Tuzlak c3e5582d11 fix: add IME composition guard in CopilotChatInput (#3835)
## Summary
- Skips keydown handling during IME composition (e.g. CJK input) by
checking `isComposing`/`keyCode 229`
- Guards textarea measurement from resetting value mid-composition,
which would break the composition session

Closes #3318
2026-04-16 12:22:12 +02:00
Alem Tuzlak 5add12346b fix: call onChange("") after send in controlled input mode (#3836)
## Summary
- Always calls `onChange("")` after send in controlled mode so the
parent component is notified to clear its state
- Previously `onChange` was only called inside the `!isControlled`
branch, leaving controlled parents unaware the input was submitted

Closes #3593
2026-04-16 12:20:51 +02:00
Alem Tuzlak f624ee1fb7 fix: pass AssistantMessage to onThumbsUp/Down callbacks (#3457) (#3796)
## Summary
- onThumbsUp/onThumbsDown/onReadAloud/onRegenerate callbacks on
CopilotChatAssistantMessage were receiving the browser SyntheticEvent
instead of the AssistantMessage object
- Wrapped the onClick handlers to pass the message explicitly:
`onClick={() => onThumbsUp(message)}`
- Same fix applied to onReadAloud and onRegenerate for consistency

## Test plan
- [x] Red-green verified: test confirms callback arg has `id`, `role`,
`content` and NOT `nativeEvent`/`target`
- [x] Full react-core test suite passes (1071 tests)
- [x] Build passes

Closes #3457
2026-04-16 12:07:26 +02:00
Alem Tuzlak c38d0db229 fix: return null instead of throwing in useRenderCustomMessages (#3497) (#3797)
## Summary
- useRenderCustomMessages threw "Agent not found" when the agent was
undefined during the connecting state
- Changed the throw to return null, allowing the component to render
gracefully while the agent is being resolved

## Test plan
- [x] Red-green verified: test calls hook with nonexistent agent,
asserts returns null not throws
- [x] Full react-core test suite passes (1071 tests)
- [x] Build passes

Closes #3497
2026-04-16 12:03:39 +02:00
Alem Tuzlak 3bda9d28bc fix: guard CopilotListeners against empty agents map (#3249) (#3798)
## Summary
- CopilotListeners called useAgent() unconditionally, which throws when
no agents are registered and no runtimeUrl is configured
- Split into CopilotListeners (outer, handles error subscription) and
CopilotListenersAgentSubscription (inner, uses useAgent)
- Inner component only renders when agents exist or a runtime is
configured

## Test plan
- [x] Red-green verified: render CopilotListeners with no agents, assert
no throw
- [x] Full react-core test suite passes (1071 tests)
- [x] Build passes

Closes #3249
2026-04-16 12:02:54 +02:00
Alem Tuzlak 9641933e13 fix: pass toolCallId to useRenderTool render components (#3843)
Closes #3741

Add `toolCallId` prop to all three status branches (InProgress,
Executing, Complete) of the ToolCallRenderer, and update the
`ReactToolCallRenderer` type to include it in the discriminated union.

Split from #3838.
2026-04-16 11:42:45 +02:00
Maxim bb6f02c62a fix(core,react-core): handle onRunErrorEvent in useAgent and document in subscribeToAgentWithOptions
React's useAgent was missing onRunErrorEvent under OnRunStatusChanged,
causing isRunning to stay true after protocol-level RUN_ERROR events
(infinite spinner). Angular already handled this via PR #3749.

Also updates SubscribeToAgentSubscriber JSDoc to accurately document
onRunErrorEvent as the sixth allowed callback with rationale for its
inclusion despite having stopPropagation in its return type, and adds
a core-level test for onRunErrorEvent firing immediately during
throttle windows.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-16 00:08:25 +02:00
Maxim 96f230c7cc fix: resolve rebase conflicts — active flag, onRunErrorEvent, async tests
- Restore `let active` guard in useAgent subscription effect (removed
  during refactor, causing ReferenceError in batchedForceUpdate)
- Add onRunErrorEvent to SubscribeToAgentSubscriber allowed keys
  (added on main for Angular AgentStore)
- Fix throttle tests to use async act() for onStateChanged assertions
  (batchedForceUpdate uses queueMicrotask, needs await to flush)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 23:15:45 +02:00
Maxim 18a9ad6e58 fix: rename subscribeToAgent to subscribeToAgentWithOptions on CopilotKitCore
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 22:09:16 +02:00
Maxim 7b94afd862 fix: address code review findings for subscribeToAgent
- Add .catch() to emitError() in reportError to prevent unhandled
  promise rejections if the error reporting infrastructure itself fails
- Add console.warn in guardAll when unsupported callback keys are
  dropped, so JS consumers discover they need agent.subscribe() directly
- Hoist ALLOWED_KEYS Set to module scope to avoid per-call allocation
- Fix SubscribeToAgentSubscriber JSDoc: explicitly list the 5 supported
  callbacks and mention onNewMessage/onNewToolCall as excluded
- DRY subscribeToAgent() method JSDoc by referencing the type instead
  of duplicating the AG-UI exclusion rationale
- Fix safeCall comment: async error path returns Promise<undefined>,
  not undefined
- Add JSDoc to setDefaultThrottleMs setter; simplify getter JSDoc
- Fix use-agent.tsx: consistent camelCase callback names, fix @default
  tag format, replace unresolvable {@link} with plain text reference
- Add test: emitError/onError integration for SUBSCRIBER_CALLBACK_FAILED
- Add test: unsubscribe + throw combination in onMessagesChanged
- Add test: unsupported callback keys dropped with console.warn

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 22:09:14 +02:00
Maxim 7fb4ddcd01 fix: improve type safety, error context, and JSDoc accuracy in subscribeToAgent
- Type `handlers` in use-agent.tsx as `SubscribeToAgentSubscriber` instead
  of full `AgentSubscriber` to prevent silent runtime stripping of unsupported
  callbacks
- Reframe AG-UI exclusion JSDoc: designed for observation, not event mutation
  (stopPropagation semantics can't be safely mediated)
- Add `satisfies` constraint to ALLOWED_KEYS so it stays synchronized with
  the SubscribeToAgentSubscriber Pick type at compile time
- Use `.then` instead of `.catch` for standard thenable detection in safeCall
- Include agent ID in all safeCall error messages for multi-agent debugging
- Only wrap lifecycle callbacks in guardAll for throttled path (messages/state
  wrappers were immediately overwritten — wasted work)
- Fix "pending flags" → "pending params" in inline comment

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 22:09:12 +02:00
Maxim b234f466e8 fix: address review findings for subscribeToAgent
- Add ALLOWED_KEYS filter in guardAll to prevent runtime leakage of
  extra properties from JS consumers or `as any` casts
- Check `active` flag between flushPending dispatches so unsubscribing
  inside onMessagesChanged prevents onStateChanged from firing
- Use real CopilotKitCore instance in Angular test stub instead of
  passthrough that bypasses throttle and safeCall logic
- Reword SubscribeToAgentSubscriber and subscribeToAgent JSDoc to
  accurately explain why AG-UI event handlers are excluded (mutation
  return values silently discarded) rather than implying a clean
  return-type split
- Add leading+trailing pattern summary to useAgent throttleMs JSDoc
- Fix setDefaultThrottleMs JSDoc: "logged as errors and ignored"
  instead of ambiguous "rejected by the setter"
- Add tests: re-entrant notifications during flush, multiple
  simultaneous subscriptions with independent throttle windows,
  unsubscribe isolation, unsubscribe-during-flush prevents sibling

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 22:09:11 +02:00
Maxim b6388b6c61 fix: guard all subscriber callbacks with safeCall and propagate return values
Address code review findings for subscribeToAgent:

- Wrap ALL subscriber callbacks (not just onMessagesChanged/onStateChanged)
  with safeCall error protection so a throwing lifecycle or event callback
  cannot corrupt the agent's notification loop
- safeCall now returns the result on the success path, preserving
  AgentStateMutation return values from subscriber callbacks
- Extract SubscribeToAgentOptions named interface for extensibility
- Remove redundant pendingMessages/pendingState boolean flags — use
  latestMessagesParams/latestStateParams !== null instead
- Add guardAll helper used by both throttled and unthrottled paths
- Fix JSDoc: reference public defaultThrottleMs (not private field),
  document shared throttle window, use callback names instead of enum
- Add 5 new core-level tests: trailing-edge re-arm, onRunFailed
  passthrough, async rejection handling, unthrottled exception safety,
  unthrottled lifecycle exception safety
- Extract RUN_INPUT constant and notifyLifecycle helper, reducing test
  helper duplication
- Remove ephemeral review-reference from test section header

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 22:09:10 +02:00
Maxim f1fc008314 refactor: move throttle logic from useAgent hook to CopilotKitCore.subscribeToAgent
Moves the leading+trailing throttle algorithm from the React-specific
useAgent hook into a framework-agnostic subscribeToAgent method on
CopilotKitCore. This consolidates throttle cascade resolution
(throttleMs ?? defaultThrottleMs ?? 0), validation, and the throttle
algorithm into a single location.

Key changes:
- Add subscribeToAgent() to CopilotKitCore with shared throttle gate
  for onMessagesChanged and onStateChanged (run lifecycle events are
  never throttled)
- Fix setDefaultThrottleMs to log errors and preserve previous value
  on invalid input instead of silently erasing
- Simplify useAgent by removing ~60 lines of inline throttle logic
- Angular AgentStore now uses subscribeToAgent, getting throttle
  support via provider-level defaultThrottleMs for free
- Remove duplicate validation from CopilotKitProvider useEffect
- Add direct unit tests for CopilotKitCore.setDefaultThrottleMs

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-15 22:09:08 +02:00
Martha Schumann cdffd8409b fix(react-core): remove redundant copilotkit.headers from useEffect deps
copilotkit.headers is already captured by headersKey (a stable memoized
string). Keeping the raw headers object in the dep array causes the context
effect to re-run on every render because mock (and real) copilotkit objects
return new header object references, leading to an unhandled
"threads is not iterable" error in tests when the spurious re-run consumed
the wrong fetchMock slot.

Also adds getThreadStore (singular) to MockCore in web-inspector.spec.ts so
ensureOwnedThreadStore can call core.getThreadStore() without throwing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 10:38:10 -07:00