Three layered fixes for the tab-switch jank on threads with many AG-UI
events. Symptoms: clicking back to a previously-opened tab took roughly
a second per switch on a thread with several hundred recorded events,
even though the underlying data was already cached.
1. Keep activated tab panels mounted. The render conditional swapped
between renderConversation/renderState/renderEvents based on `_tab`,
so Lit tore down the previous panel's DOM and rebuilt the next one
from scratch on every switch. Now once a tab is activated, its panel
stays mounted and inactive panels are hidden via `display:none`.
Activated set resets on threadId change.
2. Memoize per-panel TemplateResults by data reference. Even with the
panel mounted, render() still re-evaluated the template on every
parent update, allocating fresh nested TemplateResults for every
event row. Each render now returns the cached TemplateResult when
`_conversation` / `_fetchedState` / events array references haven't
changed; Lit then short-circuits the entire diff.
3. Defer layout for off-screen events with `content-visibility: auto`
plus a `contain-intrinsic-size` hint. The cached-data switch back to
the events panel still triggered a full layout pass over every
recorded event, which on a 600-event thread shows up as a seconds-
long freeze when the panel becomes visible. The browser now skips
layout/paint for off-screen rows entirely.
Also adds a WeakMap memo around `highlightedJson` so identical event
payloads don't re-run JSON.stringify + the syntax-highlight regex pass
on every render.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Frontend-rendered generative-UI tools (charts, custom UI) never produce
a `role: tool` result message because they execute client-side, so the
prior `item.result ? DONE : PENDING` rule rendered them as PENDING
forever even after the run finished and the chart was on screen.
The args block being populated is itself the resolution signal for these
tools, so flip the condition: any tool call with parsed arguments shows
DONE. The badge stays PENDING only for the brief window where args have
not yet streamed in.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The threads-detail sub-tabs (Conversation, Agent State, AG-UI Events)
all fetched eagerly on threadId change. Against an Intelligence-backed
runtime, the AG-UI events response can be many MB; the JSON.parse alone
blocks the main thread for several seconds while the user is still on
the conversation tab. Any sub-tab click queued during that window
couldn't fire until the parse finished, so the tab itself appeared
unresponsive — the user perceived 15s of frozen UI before the panel
swapped, and the active-tab highlight didn't paint either.
Two fixes:
1. Defer the events / state fetches to first sub-tab click. Conversation
stays eager because it's the default tab and visible immediately.
When the user clicks an Agent State or AG-UI Events sub-tab the fetch
kicks off then — so the heavy JSON.parse blocks AFTER the click has
registered, not before.
2. Add a `_panelInitializing` flag set on tab change and cleared in the
next animation frame. The render switches to a generic "Loading…"
placeholder while the flag is true, so the active-tab highlight and
spinner paint before the heavy per-tab render runs.
Total wait for events to display is unchanged; perceived responsiveness
of the click is now immediate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the 501 stubs in handleGetThreadEvents and handleGetThreadState
with real delegation to CopilotKitIntelligence. Adds two new client
methods (`getThreadEvents`, `getThreadState`) on intelligence-platform/
client.ts that hit the new `/api/_inspect/threads/:id/{events,state}`
routes shipped in Intelligence PR #144 (CPK-7453). Auth flows through
the existing `resolveIntelligenceUser` path; threadId scoping happens
server-side via the API key's org/project resolution.
Wire shapes match the in-memory branch so the inspector consumes both
runtimes identically:
- events: `{ events }` (platform-internal `decodeErrorRowIds` and
`truncated` flags are stripped at the runtime boundary)
- state: `{ state }` where the platform's discriminated `ThreadStateResult`
flattens to the snapshot value for `kind: "snapshot"` and to `null` for
both `no-snapshot` and `snapshot-decode-error`
Replaces the 501 regression-protection tests with real delegation tests
that mock the platform methods, assert call args, and exercise the
no-snapshot / decode-error / throw paths.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The first reactivity fix triggered a `_loadingMessages` flicker between
streaming chunks because every live re-fetch toggled the loading state
and replaced the conversation array wholesale. The silent flag suppresses
both: live re-fetches keep the loading indicator off and preserve the
last-good conversation on transient fetch errors. Initial threadId-change
fetches still show a real loading state.
Adds the same staleness guard the other tab fetches use (skip applying
results if `threadId` changed mid-flight) so a quick thread switch can't
leave the wrong thread's data on screen.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The conversation view in cpk-thread-details only re-fetched on threadId
change, so live agent output during streaming wasn't visible until the
user switched threads (or unmounted/remounted the element by clicking
out and back in). Restoring the original conversationOverride channel
isn't viable because the parent's agentMessages map is keyed by agentId
(not threadId) and ConversationItem mapping lives in the child — passing
mapped messages would either leak across threads or duplicate the
runtime's AG-UI → ConversationItem conversion in the parent.
Instead, the parent now tracks each agent's currently-running threadId
(from `onAgentRunStarted`) and ticks a per-thread liveMessageVersion
counter every time `syncAgentMessages` fires for that agent. The counter
is passed to cpk-thread-details, which watches it in `updated()` and
re-fetches `/threads/:id/messages` when it changes for the same threadId.
The runtime endpoint is the single source of truth for conversation
shape, so streaming output flows in without any client-side mapping.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three new test areas covering surfaces this PR introduced or relies on:
- fetch-router: matchRoute tests for `/threads/:id/events`,
`/threads/:id/state`, and `/threads/clear` (with and without URL
encoding). Critically pins that "/threads/clear" resolves to
`threads/clear` and does NOT fall through to the more permissive
`threads/update` arm with threadId="clear" — the explicit guard in
the router exists for this reason.
- fetch-handler validation: 405 enforcement for POST/PATCH/DELETE on
the read-only `/threads/:id/events` and `/threads/:id/state`
endpoints, with `Allow: GET` header. Complementary positive case
asserts GET is NOT a 405.
- handle-run: end-to-end test that handle-run.ts:40
(`agent.agentId = agentId`) propagates the registry key onto historic
runs. Without this stamp, InMemoryAgentRunner falls back to "default"
and the agentId filter on `GET /threads?agentId=...` breaks for the
local-dev fallback.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pins three integration cases for the thread-store auto-unregister branch
in CopilotKitCore's internal onAgentsChanged subscriber:
1. agent removed from agents → store IS unregistered; subscriber receives
the previous store via prevStore.
2. FIRST onAgentsChanged({ agents: {} }) on a published-style core where
the store was registered before any agents arrived → store SURVIVES.
Reproduces the race that the "previously had" guard exists to
prevent.
3. add → register → remove cycle → store IS unregistered. Complements (2)
by exercising the same code path's positive branch.
Test (1) surfaced a real bug in the seed: agentRegistry.initialize does
NOT emit onAgentsChanged, so the constructor's internal subscriber never
saw the agents__unsafe_dev_only set, and `previousAgentIds` stayed empty.
A later removeAgent__unsafe_dev_only call would then be guarded into a
no-op because the agentId looked "new". Fixed by seeding
`previousAgentIds = new Set(Object.keys(agents__unsafe_dev_only))` right
before wiring the subscriber.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Type quality:
- intelligence/threads.ts handleGetThreadMessages: switch on the Message
discriminant (role) and read narrowed fields directly. Removes
`as Record<string, unknown>` laundering and chained `as` casts on
toolCalls/function/arguments. AssistantMessage's toolCalls always have
`function: { name, arguments }`, so the prior fallbacks (`tc.name`,
`tc.args`) were dead branches.
- in-memory.ts getThreadState: import StateSnapshotEvent and use it
instead of `(event as { snapshot?: unknown }).snapshot`.
Comment / API alignment:
- intelligence/threads.ts handleClearThreads JSDoc no longer claims the
inspector calls this; the actual caller is the demo button.
- in-memory.ts clearThreads JSDoc updated to match.
- in-memory.ts getThreadEvents JSDoc no longer references a SQLite
runner that does not exist; just describes the compaction logic.
- web-inspector lint fix: rename unused `changed` parameter to
`_changed` per oxc no-unused-vars.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
CopilotKitCore subscribes to onAgentsChanged and unregisters thread
stores for any agentId not in the new agents map. For published cores,
core.agents is asynchronously populated, so the FIRST
onAgentsChanged({ agents: {} }) notification fires BEFORE published
agents are merged in. Without a guard, that empty notification rips out
a thread store that a consumer (e.g. useThreads) just registered.
Track previousAgentIds and only unregister an agentId that was present
in the previous snapshot AND missing from the new one. The first
empty-agents notification (where the agentId was never previously
present) becomes a no-op.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Five behavioural fixes on cpk-thread-details / WebInspectorElement:
- fetchEvents/fetchState: mirror the AbortController pattern fetchMessages
already uses. Without this, switching threads quickly (A→B) can leave
the user looking at thread B with thread A's events/state when A's
request resolves last.
- mapMessages: when JSON.parse fails on tool-call args or tool result
content, log via console.error and attach __parseError + __raw on the
parsed object instead of silently substituting `{}`. The inspector is a
debugging surface; hiding malformed payloads defeats its purpose.
- fetchAnnouncement: keep the captured error and console.warn it. The
prior `catch {}` swallowed Malformed-payload throws, JSON parse
failures, and convertMarkdownToHtml exceptions silently.
- subscribeToThreadStore: also subscribe to ɵselectThreadsError, store
per-agent in `_threadsErrorByAgent`, and surface in renderThreadsView /
cpk-thread-list as an error state branch alongside empty/no-results.
Previously a thread-store load failure (REST list rejection, Phoenix
subscribe failure, retry exhaustion) left the user with stale data and
no indication of failure.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- thread-store-registry: makeStore now attaches a __testId via
intersection so callers can distinguish stubs at a glance during
debugging instead of relying on identity-by-allocation alone.
- thread-store-registry subscriber-isolation test: assert the
diagnostic content ("Subscriber onThreadStoreRegistered error") and
Error argument, not just that some error was logged.
- handle-threads identifyUser-throws test: assert
"Error identifying intelligence user" with an Error argument, since
the throw originates inside resolveIntelligenceUser which logs and
returns 500 before subscribeToThreads is reached.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add the missing protected connect() override returning EMPTY so the
mock matches TestAgent and ThrowingAgent. Without it, a clone() that
ever exercised connect() would fall through to AbstractAgent.connect()
and may try to open a real transport in tests.
- clone() now forwards this.agentId directly instead of coercing
undefined to "". The constructor accepts string | undefined to make
this type-safe — coercion would silently turn "no agent id" into
"empty agent id", a different state per AgentConfig.agentId?: string.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- 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>
handleClearThreads is intentionally synchronous, but neither test
asserted the return type. Add `expect(response).not.toBeInstanceOf(Promise)`
on both branches so a regression that starts awaiting I/O updates the
synchronous call sites.
The handleGetThreadEvents/handleGetThreadState 501 tests asserted the
status code but not that intelligence stayed untouched. Stub spies for
both `listThreads` and a hypothetical `getThreadEvents`/`getThreadState`,
then assert neither was called — so a regression that drops the early
return and falls through to platform calls fails this test even after
the response code changes.
The handleSubscribeToThreads 500 test created an `errorSpy` to silence
console output but never asserted on it. Add `expect(errorSpy).toHaveBeenCalled()`
so a regression that quietly drops the diagnostic log is caught.
The InMemoryAgentRunner getThreadEvents test claimed the synthetic
terminal event was present but only asserted on TEXT_MESSAGE_*. Add an
explicit assertion on `RUN_ERROR` with `code: "INCOMPLETE_STREAM"` so
finalizeRunEvents' contract is locked in — a regression that stops
appending the synthetic event would leave the inspector showing an
in-progress thread forever.
Declare `onNewMessage` on `MessagePopulatingTestAgent.runAgent`'s options
type to match the runner's call site and `TestAgent` above. Without it,
a regression that starts depending on `onNewMessage` here would compile
cleanly even though the mock would silently drop the call.
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.
R1's notify-ordering fix only made the sync subscriber path safe; async
subscribers still race against the next register(). When notifySubscribers
awaits inside Promise.all, control returns to register() which assigns the
new store to the same agentId before the async handler resumes — at which
point registry.get(agentId) returns the new store, not the unregistered one.
Carry the previous store on the onThreadStoreUnregistered payload so
subscribers tearing down state don't need to consult the registry. Same
treatment for unregister(). Doc the "do not call registry.get(agentId) in
this callback" contract on the subscriber type itself.
Also tighten getAll(): cache the snapshot, freeze it (so the Readonly<>
claim is honest at runtime), and return the same reference between
mutations. Stable identity matters for useSyncExternalStore consumers that
compare snapshots to skip re-renders. Tests updated to assert prevStore
delivery, frozen-snapshot mutation throws, and reference stability across
calls. Mock notifySubscribers now uses Promise.all to mirror production
parallel dispatch.
## 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)
vitest types invocationCallOrder as `number[]`, but TS narrows
indexed access to `number | undefined` under noUncheckedIndexedAccess.
Capture the entries first, assert each is defined, then compare —
keeps the ordering check intact while satisfying tsc.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- 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>
Tests in @copilotkit/react-core need the WebSocket retry budget so they
can validate teardown semantics without hardcoding the threshold
separately from production. Exposing it under the ɵ-prefixed internal
namespace keeps it out of the public API surface while letting the
test bench import a single source of truth.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
handle-threads.test.ts:
- handleGetThreadMessages intelligence-path test now asserts the response
body verbatim, so a regression that swaps in a stub body is caught.
- 422-no-intelligence test issues a real DELETE request for the delete
path instead of cloning a POST request.
- handleClearThreads block carries a comment explaining why the handler
is intentionally synchronous (no I/O on either branch).
- The identifyUser-throws test now silences console.error for the
duration of the assertion, matching the pattern already used by the
subscribe-throws test.
in-memory-runner.test.ts:
- ThrowingAgent test asserts RUN_ERROR is the last emitted event and
that no RUN_FINISHED is emitted, locking in terminal-event semantics.
- getThreadEvents test asserts the full TEXT_MESSAGE triple is present
in the persisted event log, and the comment now reflects the real
finalizeRunEvents behaviour (it appends a synthetic terminal event
when the agent does not emit one, so terminal events ARE persisted).
- getThreadState multi-run test gains a cross-thread isolation
assertion: a snapshot on a different thread must not bleed into the
original thread's state.
- Bumped the inter-thread sort delay from 5ms to 20ms to absorb timer
jitter on slow CI runners.
- Removed four redundant `agent.agentId = "test-agent"` reassignments
(the constructor already sets it via super({ agentId })).
- Aligned MessagePopulatingTestAgent.runAgent with TestAgent: `onEvent`
is now required so the runner contract is exercised consistently.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- register() now snapshots and clears the previous slot before queuing the
unregister notification, then assigns the new store before queuing the
register notification. The unregister microtask is queued first so
subscribers tear down stale subscriptions before receiving the
replacement.
- All notifySubscribers calls now attach .catch handlers so notification
failures surface via console.error instead of being silently swallowed
by `void`.
- getAll() returns a shallow copy so callers cannot mutate the registry's
internal state through the returned reference.
- Test mock now mirrors the real notifySubscribers signature (handler +
errorMessage) and wraps each subscriber call in try/catch, exercising
the same error-isolation behaviour as production.
- Replaced the `as unknown as ɵThreadStore` cast with a satisfies-typed
minimal stub matching the real interface.
- Added tests for ordering (unregister before second register), throwing
subscriber isolation, and getAll() snapshot isolation.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
## 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)
## What does this PR do?
Polish image attachments in chat. Previously they rendered at the full
message
width, taking up most of the screen. After this PR they render as
compact 80x80
thumbnails — closer to how Claude shows attached images.
**Layout & sizing**
- Image attachments render as 80x80 thumbnails with `object-cover`, 12px
rounded corners, and a muted background so transparent PNGs stay
readable.
- Multiple attachments lay out in a horizontal row (`flex-row` +
`flex-wrap` +
`justify-end`) instead of stacking vertically.
- Attachments render *above* the message text instead of below.
**Click-to-zoom**
- Clicking a thumbnail opens it in a fullscreen lightbox with a smooth
view-transition morph (same UX as the attachment queue preview).
- The `Lightbox` + `useLightbox` previously private to
`CopilotChatAttachmentQueue` got extracted into a shared module
(`packages/react-core/src/v2/components/chat/Lightbox.tsx`) so the queue
and
the rendered message attachment share one modal implementation.
**Both renderers updated**
- v2 (Tailwind): `CopilotChatAttachmentRenderer.tsx` +
`CopilotChatUserMessage.tsx`
- legacy (CSS): `packages/react-ui/src/css/messages.css`
## Related PRs and Issues
- N/A
## Checklist
- [x] I have read the [Contribution
Guide](https://github.com/copilotkit/copilotkit/blob/master/CONTRIBUTING.md)
- [x] If the PR changes or adds functionality, I have updated the
relevant documentation
- [x] "Allow edits by maintainers" is checked
`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>
TanStack's chat() engine runs a multi-turn agent loop: after the model
returns tool calls, it tries to execute them via processToolCalls().
Frontend-only tools (like render_pie_chart) are unknown to TanStack, so
executeToolCalls() treats them as errors and buildToolResultChunks()
re-emits TOOL_CALL_END without a preceding TOOL_CALL_START. The ag-ui
verify middleware rejects this duplicate.
Fix: track a runFinished flag in convertTanStackStream and discard all
events after the first RUN_FINISHED, which marks the boundary between
the streaming pass and TanStack's internal tool execution loop.
Also adds built-in-agent to docker-compose.local.yml and local-ports.json.
Picks up ag-ui-protocol/ag-ui#1578 — `import * as jsonpatch from
"fast-json-patch"` produced an empty namespace under Node native ESM
because fast-json-patch@3.x populates exports via Object.assign, which
the CJS→ESM named-export detector cannot see. Result: every STATE_DELTA
and ACTIVITY_DELTA event threw "applyPatch is not a function", and
LangGraph generative UI streams floods the console with the failure on
each patch.
0.0.53 switches to a default import so the emitted bundle works under
both ESM and CJS consumers. Bumped @ag-ui/core and @ag-ui/encoder in
lockstep since they share the release.
The prepack script ran `pnpm run build` during `pnpm publish`, duplicating
the build that prerelease.ts/publish-release.ts already do across the
workspace. With tsdown's `exports: true` regenerating package.json's
exports field on every build, the second rewrite happened mid-publish
and pnpm's pack step failed with ENOENT on the staging tarball.
No other monorepo package has prepack — bringing a2ui-renderer in line.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>
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>
## Summary
- Explicitly defaults `streamSubgraphs: true` in `LangGraphAgent.run()`
forwardedProps so subagent streaming works with `@ag-ui/langgraph`
0.0.31+, which changed the default from `true` to `undefined`
- Uses nullish coalescing (`??`) so explicit user overrides (including
`false`) are preserved
- Fixes 6 showcase integrations failing E2E probes on the `subagents`
feature
## Why
`@ag-ui/langgraph` 0.0.31 removed the `?? true` fallback on
`streamSubgraphs` in `handleStreamEvents()`. The CopilotKit runtime
never explicitly set this prop, so subgraph event forwarding became
silently disabled. This caused all subagent-dependent demos to stop
working.
A previous v1 fix (commit `21e12afca`) handled this in the now-deleted
`agui-action.ts`, but the logic was lost during the v2 migration.
## Additional context
Investigation identified a second issue: `@ag-ui/langgraph` 0.0.34
includes state snapshot fixes needed for shared-state-read/write
features, but 0.0.34 is not yet published on npm (0.0.31 is latest). The
ag-ui team needs to cut a release. Full analysis: [Notion
write-up](https://app.notion.com/p/3513aa381852812a9ecef5fbf1e71739)
## Test plan
- [x] Runtime tests pass (1412/1412)
- [x] 7-agent CR converged Round 1 (0 findings)
- [ ] Verify showcase E2E probes for subagent features turn green after
merge
Express 4 does not support the {*splat} wildcard syntax introduced in
Express 5. Replace string-based wildcard patterns with RegExp routes
that work across both major versions.
The @ag-ui/langgraph 0.0.31+ changed the default for streamSubgraphs
from true to undefined, breaking subagent streaming. This enriches the
run() input's forwardedProps with streamSubgraphs: true as a default,
while preserving any explicit user override via the nullish coalescing
operator.