Commit Graph

1601 Commits

Author SHA1 Message Date
Martha Schumann e05c5ef1b7 perf(inspector): keep tab DOM mounted, cache panel templates, defer off-screen events
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>
2026-04-30 19:29:47 -07:00
Martha Schumann ce00492070 fix(inspector): treat tool calls with parsed args as DONE
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>
2026-04-30 19:29:47 -07:00
github-actions[bot] 56524a7282 style: auto-fix formatting 2026-05-01 01:08:19 +00:00
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 762eb79d74 fix(inspector): lazy-load events/state + spinner so tab clicks feel instant
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>
2026-04-30 15:43:29 -07:00
Martha Schumann c9e1bd8e34 feat(runtime): wire intelligence /_inspect/threads/:id/{events,state} endpoints
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>
2026-04-30 14:33:02 -07:00
Martha Schumann 099700721c fix(inspector): silent re-fetch for live conversation updates
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>
2026-04-30 14:27:57 -07:00
Martha Schumann 94305c0927 fix(inspector): re-fetch conversation when active agent emits new messages
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>
2026-04-30 13:51:27 -07:00
Martha Schumann 2e498341ad test(runtime): pin per-thread routes, GET-only enforcement, and agentId tagging
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>
2026-04-30 12:52:35 -07:00
Martha Schumann cf51841d18 test(core): cover onAgentsChanged auto-unregister + seed previousAgentIds from constructor
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>
2026-04-30 12:52:02 -07:00
Martha Schumann 3d6166611e refactor(runtime,inspector): tighten types and align JSDoc with reality
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>
2026-04-30 12:45:07 -07:00
Martha Schumann bd9fe0169a fix(core): guard thread-store auto-unregister against initial empty agents
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>
2026-04-30 12:44:13 -07:00
Martha Schumann daf52a2068 fix(inspector): plug data-staleness, error-swallowing, and parse silent-fail
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>
2026-04-30 12:43:50 -07:00
Tyler Slaton 52f8030f82 Merge branch 'main' into release/publish/monorepo/v1.56.5 2026-04-30 12:42:10 -07:00
Martha Schumann 7dcaa15063 test(core,runtime): strengthen error-log assertions and stub identity
- 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>
2026-04-30 12:13:43 -07:00
Martha Schumann 7d525d62aa test(runtime): align MessagePopulatingTestAgent with TestAgent contract
- 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>
2026-04-30 12:13:22 -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 d5e5da6dce test(runtime): tighten thread-handler and in-memory-runner assertions
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.
2026-04-30 11:54:14 -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
Martha Schumann 9b7c0517b6 fix(core): forward prevStore on unregister and freeze getAll snapshot
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.
2026-04-30 11:49:13 -07:00
杨兴隆 417ff8986b fix(react-ui): add stable test ids to chat input 2026-05-01 02:29:50 +08:00
Tyler Slaton 0bdc798854 Merge branch 'main' into tyler/kind-mendeleev-636369 2026-04-30 10:48:28 -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 ed4be71076 test(core): narrow invocationCallOrder access for tsc strict mode
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>
2026-04-30 10:25:50 -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
Martha Schumann 9fdfe23b39 refactor(core): export ɵMAX_SOCKET_RETRIES for thread-store tests
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>
2026-04-30 10:20:25 -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
Martha Schumann 7421bf12b1 test(runtime): tighten thread-handler and in-memory-runner tests
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>
2026-04-30 10:14:09 -07:00
Martha Schumann f5787e0b5d fix(core): tighten ThreadStoreRegistry notify ordering and snapshot isolation
- 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>
2026-04-30 10:06:16 -07:00
Ran Shemtov e8061b707b Merge branch 'main' into release/publish/monorepo/v1.56.5 2026-04-30 17:59:09 +02:00
Alem Tuzlak 917c54a8d1 Merge branch 'main' into tyler/kind-mendeleev-636369 2026-04-30 11:03:19 +02: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
Alem Tuzlak 672070946f fix(react): render image attachments as compact thumbnails (#4459)
## 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
2026-04-30 09:53:20 +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
Tyler Slaton 5fe7ec55ca Merge branch 'main' into tyler/kind-mendeleev-636369 2026-04-29 22:46:51 -07:00
Jordan Ritter b355acd56c fix(runtime): stop converting TanStack stream after RUN_FINISHED
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.
2026-04-29 22:41:58 -07:00
Tyler Slaton bd68aeda32 fix(deps): bump ag-ui packages to 0.0.53
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.
2026-04-29 22:41:28 -07:00
Tyler Slaton 644a4a2d5f fix(a2ui-renderer): remove redundant prepack script causing canary publish failures
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>
2026-04-29 22:09:34 -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
Jordan Ritter 497b205d1e fix: auto-format 16 files with pre-existing oxfmt violations
These files accumulated formatting drift across recent PRs. Fixes the
format CI check on main.
2026-04-29 19:12:22 -07:00
Jordan Ritter f2bc4a8dcb fix: default streamSubgraphs to true in LangGraph agent wrapper (#4446)
## 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
2026-04-29 16:57:58 -07:00
Jordan Ritter 1baeb9940a fix: use RegExp routes in Express adapter for Express 4/5 compatibility
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.
2026-04-29 16:50:19 -07:00
Jordan Ritter c0c97b4bf1 fix: default streamSubgraphs to true in LangGraph agent wrapper
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.
2026-04-29 15:59:08 -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
ranst91 5686889567 chore: release monorepo v1.56.5 2026-04-29 11:57:11 +00:00
Ran Shem Tov e504254792 chore: use latest ag-ui-langgraph 2026-04-29 11:30:43 +02:00
github-actions[bot] 5eaa1a2f4a style: auto-fix formatting 2026-04-29 11:29:04 +02:00