feat(react-core): auto-mounted IntelligenceIndicator + MemoizedCustomMessage memo gating signals#4632
Merged
BenTaylorDev merged 12 commits intoMay 6, 2026
Conversation
Contributor
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
📣 Social Copy GeneratorGenerate social media copies (Twitter/X, LinkedIn, Blog Post) for this PR using Claude.
|
@copilotkit/a2ui-renderer
@copilotkit/agentcore-runner
@copilotkitnext/angular
@copilotkit/core
@copilotkit/react-core
@copilotkit/react-textarea
@copilotkit/react-ui
@copilotkit/runtime
@copilotkit/runtime-client-gql
@copilotkit/sdk-js
@copilotkit/shared
@copilotkit/sqlite-runner
@copilotkit/voice
@copilotkit/web-inspector
commit: |
…sage gating signals Adds three new memo gating signals to MemoizedCustomMessage so custom message renderers stay reactive across the structural events that affect "is this slot still authoritative?" decisions: - numberOfMessagesInRun — invalidates when peers stream into the same run, so renderers gating on "last message of the run" stay correct. - isInLatestRun — invalidates when a newer run starts, so renderers gating on "is this the latest activity?" can drop their badges on completed runs. - isRunning (gated on isInLatestRun) — invalidates exactly twice per run on the latest run's slots (start, end), preserving the perf-test guarantee that completed runs' messages skip re-renders during streaming. CopilotChatMessageView computes these per slot via getRunIdForMessage and passes them down. Helper getNumberOfMessagesInRun lives next to the call site for clarity. New e2e test at CopilotKitProvider.intelligenceIndicator.e2e.test.tsx exercises an "Using CopilotKit Intelligence" renderer that gates on: position === "after", last-in-run, agent.isRunning, and run-is-latest. The walkthrough scenario drives Run A then Run B with multiple messages each, verifying the indicator only appears on the canonical slot at each phase. Four condition-focused tests pin each gate individually. Includes IsRunningAccurateMockAgent — a local subclass that makes run() return a per-run observable terminating on RUN_FINISHED/RUN_ERROR. The shared MockStepwiseAgent.run() returns the un-terminating subject for backward compatibility, so emit(RUN_FINISHED) on it doesn't trigger AbstractAgent's finalize → onRunFinalized → useAgent re-render path. The subclass scopes the fix to this file without disturbing other tests.
… is configured Adds an official "Using CopilotKit Intelligence" pill, ported from the visuals of CopilotKit/Intelligence#155. Mounts automatically — the caller never adds the renderer themselves. Behavior: - `CopilotChatMessageView` mounts an `<IntelligenceIndicator>` for every message slot whenever `copilotkit.intelligence !== undefined`. When intelligence is not configured, no indicator instance is mounted at all (no perf cost). - `IntelligenceIndicator` self-gates so only the canonical message renders a pill — last message of the latest in-flight run, with at least one tool call whose name matches a pattern from `DEFAULT_TOOL_PATTERNS` (currently `[/^bash$/]`, the Intelligence MCP server's canonical tool). - The "exactly one pill at any moment" guarantee is structural: only one message ever satisfies (last in run) + (run is latest) + (matching tool call), so each renderer invocation decides independently and the result is one pill in the DOM. Phase machine (per-instance, all timers local): - `spinner` while `agent.isRunning` - → `check` after `agent.isRunning` falls (debounced 500 ms to absorb step-boundary `RUN_FINISHED → RUN_STARTED` blips inside one user turn) - → `fading` after `CHECK_HOLD_MS` (800 ms) - → `hidden` after `FADE_OUT_ANIMATION_MS` (480 ms) A 200 ms `agent.isRunning` poll closes the AG-UI snapshot-subscriber gap (subscribers added INSIDE a run never see that run's `onRunFinalized`). Public surface (via `@copilotkit/react-core/v2`): - `IntelligenceIndicator` — the pill component, exposed for tests and inspection. Most callers don't import it directly; the auto-mount in `CopilotChatMessageView` does the work. There is no factory and no provider — auto-registration eliminates the prior `createIntelligenceIndicatorRenderer` factory and any `IntelligenceIndicatorProvider`/coordination store. Tests (all live next to the component): - 1 walkthrough (Run A → Run B with multiple messages, asserts the pill follows the canonical "last message of latest in-flight run" slot through every phase, with no `renderCustomMessages` prop on the test setup) - 4 condition tests (last-in-run / in-flight / latest-run / tool-match), each pinning one gate - 1 intelligence-gate test (no pill when `copilotkit.intelligence` is undefined) - 1 explicit auto-registration assertion (no `renderCustomMessages` prop is required for the pill to render) 7 tests, react-core 1157 → 1158.
ff0862d to
678d143
Compare
Round-1 review on the indicator branch surfaced perf and defensive hardening items. Tests: react-core 1158 — all green. - CopilotChatMessageView auto-mount now gates on `message.role === "assistant"` in addition to `copilotkit.intelligence !== undefined`. Eliminates wasted `useAgent` subscriptions, 200 ms polling intervals, and four `useEffect`s on every user / activity / reasoning slot — the indicator's own role gate would short-circuit anyway, but only after a subscribe + interval-set + cleanup cycle on every render. - IntelligenceIndicator's `toolCalls` access is now defensive: `Array.isArray(...)` guard and `tc?.function?.name` chain. A malformed agent payload no longer crashes the chat tree at `.some(...)`. Comment fixes: - CopilotChatMessageView: stale `CopilotKitProvider.intelligenceIndicator.e2e.test.tsx` reference updated to the actual path `intelligence-indicator/__tests__/IntelligenceIndicator.e2e.test.tsx`. - IntelligenceIndicator `ISRUNNING_POLL_MS` JSDoc rewritten — the prior version claimed `addMessage` iterates subscribers live during streaming. In fact AG-UI's `runAgent` snapshots subscribers and threads them through the entire pipeline (including `processApplyEvents` for streaming events), so a late-mounted subscriber misses both `onMessagesChanged` AND `onRunFinalized` from the run's pipeline. The poll fallback is the only thing that catches the falling edge. - `globals.css` pill-styles port comment listed `#BEC2FF` as part of the palette but that hex doesn't appear anywhere in the rules. Updated to the actual swatches: text #5B21B6, icon #7C3AED, border #9599E0, gradient #EEE6FE, shadow #5E64AD.
…rleaving The pill's gate "the message must be the last message of its run" was suppressed every time a `role: "tool"` result arrived between successive assistant-with-tool-call messages. Real MCP recall flows always interleave tool results between assistant tool-call messages, so the assistant message holding the matching tool call lost its "last in run" claim immediately, the indicator returned `null`, and the pill flashed off. By the time the run finished, the final prose-only assistant message was the last in the run and the pill on the bash-bearing assistant stayed suppressed. Net result: the user saw no pill at all during a real recall. Change the gate to "the latest assistant-with-matching-tool-call message in the run". Tool result messages (`role: "tool"`) and prose-only assistant messages now skip through the walk without invalidating an earlier matching-assistant's claim on the slot, so the pill stays continuously through a multi-step tool chain and transitions to checkmark on `isRunning` falling (debounced 500 ms, unchanged) as before. The existing test suite did not cover this case — none of the walkthrough scenarios emit `role: "tool"` between successive assistant messages. A regression test that interleaves a tool result will land alongside this fix.
…orMessage Two SDK gaps surface in real MCP recall flows that the previous gate revision still tripped on: - The bash-issuing assistant message is consistently missing from `stateManager.messageToRun` even though it is the message the indicator needs to attach to. The first gate `if (!messageRunId) return null;` fired before any of the slot logic ran, so the pill never rendered. - The threadId key in `messageToRun` can drift out of sync with the chat configuration's threadId — same lookup, same null, same gate. Drop the run-id dependency entirely. The indicator only needs `agent.messages` and `message.role` / `message.toolCalls`, both of which the runtime populates correctly in every observed flow. The walk just finds the latest assistant-with-matching-tool-call across `agent.messages`; tool result messages (`role: "tool"`) and prose- only assistants are skipped without invalidating the slot. Cross-run isolation moves to the phase machine: once an indicator reaches `phase === "hidden"` it stays there. A later run on the same chat does not resurrect a faded pill; the new run mounts fresh indicator instances on its own assistant messages. Net behaviour: - Through a multi-step tool chain the pill stays put on the bash- issuing assistant. - When the run finishes, the existing 500 ms debounce -> 800 ms check-hold -> 480 ms fade lifecycle plays out unchanged, then hidden becomes terminal. - Subsequent runs are independent: their first assistant-with-tool- call message becomes the new canonical slot.
The auto-mount in `CopilotChatMessageView` puts the indicator inside a flex column container (`cpk:flex cpk:flex-col`) whose default `align-items: stretch` was overriding the pill's intended `display: inline-flex` shrink-to-content behaviour, leaving the pill stretched to the full chat width — out of proportion with the short label. Add `align-self: flex-start` to opt the pill out of the parent's stretch. Pill renders at content width, anchored to the chat's left edge in line with the assistant message bubble it represents.
Removes the numberOfMessagesInRun, isInLatestRun, and isRunning props on MemoizedCustomMessage along with the per-render runMetadata derivation that fed them. Authored renderers observe run state via useAgent's OnRunStatusChanged / OnMessagesChanged subscriptions, which forceUpdate the renderer independently of the memo's bail-out — the extra invalidation inputs added nothing for that canonical path and only masked staleness for renderers that read run state from closure without subscribing. The IntelligenceIndicator itself uses useAgent and remains correct. Net change: ~95 lines removed; one less O(n) scan through messages per chat re-render. All chat e2e tests (587) pass, including the indicator suite (10).
…→ runtimeAgentId Renames the proxy-config field, the field on the agent instance, and all matching references in tests and the useCopilotKit reference page. "runtime" reads more naturally now that the proxy concept is documented as "a local agent that delegates to a runtime agent" rather than "remote agent" — the latter conflates with `remoteAgents` (the registry of agents fetched from the runtime), which keeps its name. No behavioral change; the field still controls the outbound REST URL used by the proxy.
…e auto-mount Reverts the cosmetic changes that crept in alongside the auto-mount edit — restores `stateSnapshot?` (always passed at runtime, no functional difference), the original concise comments around the memo's comparison function, and the original combined value+type imports. The only remaining framework change in this PR is the new auto-mount block: when `copilotkit.intelligence !== undefined` and the message is an assistant message, push an `<IntelligenceIndicator>` after the message slot. Indicator e2e suite still green (10/10).
The previous comment referenced a "200 ms poll interval" that the indicator no longer uses (polling was removed when we switched to the tool-call pending-grace timer). Updates the rationale to mention the current self-gates (latest matching-assistant slot + pending grace window).
mme
added a commit
that referenced
this pull request
May 6, 2026
…_knowledge_base_shell tool The Intelligence platform's MCP tool was renamed from `bash` to `copilotkit_knowledge_base_shell` (intelligence/mme/integrate-sl 4256c13). Update the indicator's `DEFAULT_TOOL_PATTERNS` to match the new name so the pill keeps rendering on the right assistant slots. Test fixtures that previously used the bare `bash` name follow the rename — both the default in `emitAssistantMessageWithToolCalls` and the explicit `tc_match` entry in the tool-match condition test.
mme
added a commit
that referenced
this pull request
May 6, 2026
BenTaylorDev
approved these changes
May 6, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Stacked on top of #4629. Two commits:
MemoizedCustomMessage memo gating signals (Friday's WIP, cleaned in CR loop). Adds three new memo gating signals to
MemoizedCustomMessageso custom message renderers stay reactive across the structural events that affect "is this slot still authoritative?" decisions:numberOfMessagesInRun— invalidates when peers stream into the same runisInLatestRun— invalidates when a newer run startsisRunning(gated onisInLatestRun) — invalidates exactly twice per run on the latest run's slots, preserving the perf-test guarantee that completed runs' messages skip re-renders during streamingCopilotChatMessageViewconsolidates per-run metadata (run-id-by-message, count-by-run, latest-run) into a singleuseMemothat runs O(n) per render rather than O(n²) per message.IntelligenceIndicator— auto-mounted "Using CopilotKit Intelligence" pill, ported from the visuals of CopilotKit/Intelligence#155.How the indicator works
CopilotChatMessageViewmounts<IntelligenceIndicator>for every message slot whenevercopilotkit.intelligence !== undefined. No factory, no provider, norenderCustomMessagesregistration — the caller doesn't add anything.function.namematches one ofDEFAULT_TOOL_PATTERNS(currently[/^bash$/], the Intelligence MCP server's canonical tool).Phase machine (per-instance, all timers local)
spinnerwhileagent.isRunningcheckafteragent.isRunningfalls (debounced 500 ms to absorb step-boundaryRUN_FINISHED → RUN_STARTEDblips)fadingafter 800 ms holdhiddenafter 480 ms fade animationA 200 ms
agent.isRunningpoll closes the AG-UI snapshot-subscriber gap (subscribers added INSIDE a run never see that run'sonRunFinalized).API
Nothing new for the user. Configure intelligence on your runtime, and the pill appears.
When
/inforeturnsintelligence: { wsUrl: ... }, the gate flips and the pill auto-mounts on the canonical slot.Configurability
Tool-name matching is currently hardcoded to
[/^bash$/]. If we need per-instance customization later (aCopilotKitProviderprop, or atoolPatternsfield onIntelligenceRuntimeInfoso the runtime can declare its own intelligence tools), the constant becomes the fallback.Test plan
nx run @copilotkit/react-core:test— 1158 passednx run @copilotkit/core:test— 424 passednx run @copilotkit/web-inspector:test— 7 passedreact-core,core,web-inspectorclean. Pill CSS bundled intodist/v2/index.css(10 rules + 3 keyframes).New test coverage
renderCustomMessagesprop to prove auto-mount.copilotkit.intelligenceis undefined.Caveats
CopilotChatConfigurationProviderdependency thatCopilotChatMessageViewuses foragentId/threadId(to query state-manager) is pre-existing and not addressed here — flagged separately.🤖 Generated with Claude Code