Skip to content

feat(react-core): auto-mounted IntelligenceIndicator + MemoizedCustomMessage memo gating signals#4632

Merged
BenTaylorDev merged 12 commits into
mme/register-proxied-agentfrom
mme/test-intelligence-indicator-renderer
May 6, 2026
Merged

feat(react-core): auto-mounted IntelligenceIndicator + MemoizedCustomMessage memo gating signals#4632
BenTaylorDev merged 12 commits into
mme/register-proxied-agentfrom
mme/test-intelligence-indicator-renderer

Conversation

@mme
Copy link
Copy Markdown
Contributor

@mme mme commented May 4, 2026

Summary

Stacked on top of #4629. Two commits:

  1. MemoizedCustomMessage memo gating signals (Friday's WIP, cleaned in CR loop). 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
    • isInLatestRun — invalidates when a newer run starts
    • isRunning (gated on isInLatestRun) — invalidates exactly twice per run on the latest run's slots, preserving the perf-test guarantee that completed runs' messages skip re-renders during streaming

    CopilotChatMessageView consolidates per-run metadata (run-id-by-message, count-by-run, latest-run) into a single useMemo that runs O(n) per render rather than O(n²) per message.

  2. IntelligenceIndicator — auto-mounted "Using CopilotKit Intelligence" pill, ported from the visuals of CopilotKit/Intelligence#155.

How the indicator works

  • CopilotChatMessageView mounts <IntelligenceIndicator> for every message slot whenever copilotkit.intelligence !== undefined. No factory, no provider, no renderCustomMessages registration — the caller doesn't add anything.
  • The indicator self-gates so only the canonical slot renders a pill: last message of the latest in-flight run, with at least one tool call whose function.name matches one of DEFAULT_TOOL_PATTERNS (currently [/^bash$/], the Intelligence MCP server's canonical tool).
  • "Exactly one pill at any moment" is structural — only one message can ever satisfy the gates, 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)
  • fading after 800 ms hold
  • hidden after 480 ms fade animation

A 200 ms agent.isRunning poll closes the AG-UI snapshot-subscriber gap (subscribers added INSIDE a run never see that run's onRunFinalized).

API

Nothing new for the user. Configure intelligence on your runtime, and the pill appears.

<CopilotKitProvider runtimeUrl="..."><CopilotChat /></CopilotKitProvider>

When /info returns intelligence: { 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 (a CopilotKitProvider prop, or a toolPatterns field on IntelligenceRuntimeInfo so the runtime can declare its own intelligence tools), the constant becomes the fallback.

Test plan

  • nx run @copilotkit/react-core:test1158 passed
  • nx run @copilotkit/core:test424 passed
  • nx run @copilotkit/web-inspector:test7 passed
  • Build: react-core, core, web-inspector clean. Pill CSS bundled into dist/v2/index.css (10 rules + 3 keyframes).

New test coverage

  • 1 walkthrough — Run A → Run B with multiple bash calls; pill follows the canonical slot through every phase. Setup passes no renderCustomMessages prop to prove auto-mount.
  • 4 gate tests: last-in-run / in-flight (with phase-machine fade-out) / latest-run / tool-match.
  • 1 intelligence gate — no pill when copilotkit.intelligence is undefined.
  • 1 explicit auto-registration assertion — pill renders solely because intelligence is set, no user setup.

Caveats

  • Tool-name patterns are hardcoded; not yet configurable by the runtime or the app caller. Easy follow-up if needed.
  • The CopilotChatConfigurationProvider dependency that CopilotChatMessageView uses for agentId / threadId (to query state-manager) is pre-existing and not addressed here — flagged separately.

🤖 Generated with Claude Code

@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented May 4, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
chat-with-your-data Ready Ready Preview, Comment May 6, 2026 3:36pm
docs Ready Ready Preview, Comment May 6, 2026 3:36pm
form-filling Ready Ready Preview, Comment May 6, 2026 3:36pm
research-canvas Ready Ready Preview, Comment May 6, 2026 3:36pm
travel Ready Ready Preview, Comment May 6, 2026 3:36pm

Request Review

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 4, 2026

📣 Social Copy Generator

Generate social media copies (Twitter/X, LinkedIn, Blog Post) for this PR using Claude.

  • Generate social media copies

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 4, 2026

Open in StackBlitz

@copilotkit/a2ui-renderer

pnpm add https://pkg.pr.new/CopilotKit/CopilotKit/@copilotkit/a2ui-renderer@4632

@copilotkit/agentcore-runner

pnpm add https://pkg.pr.new/CopilotKit/CopilotKit/@copilotkit/agentcore-runner@4632

@copilotkitnext/angular

pnpm add https://pkg.pr.new/CopilotKit/CopilotKit/@copilotkitnext/angular@4632

@copilotkit/core

pnpm add https://pkg.pr.new/CopilotKit/CopilotKit/@copilotkit/core@4632

@copilotkit/react-core

pnpm add https://pkg.pr.new/CopilotKit/CopilotKit/@copilotkit/react-core@4632

@copilotkit/react-textarea

pnpm add https://pkg.pr.new/CopilotKit/CopilotKit/@copilotkit/react-textarea@4632

@copilotkit/react-ui

pnpm add https://pkg.pr.new/CopilotKit/CopilotKit/@copilotkit/react-ui@4632

@copilotkit/runtime

pnpm add https://pkg.pr.new/CopilotKit/CopilotKit/@copilotkit/runtime@4632

@copilotkit/runtime-client-gql

pnpm add https://pkg.pr.new/CopilotKit/CopilotKit/@copilotkit/runtime-client-gql@4632

@copilotkit/sdk-js

pnpm add https://pkg.pr.new/CopilotKit/CopilotKit/@copilotkit/sdk-js@4632

@copilotkit/shared

pnpm add https://pkg.pr.new/CopilotKit/CopilotKit/@copilotkit/shared@4632

@copilotkit/sqlite-runner

pnpm add https://pkg.pr.new/CopilotKit/CopilotKit/@copilotkit/sqlite-runner@4632

@copilotkit/voice

pnpm add https://pkg.pr.new/CopilotKit/CopilotKit/@copilotkit/voice@4632

@copilotkit/web-inspector

pnpm add https://pkg.pr.new/CopilotKit/CopilotKit/@copilotkit/web-inspector@4632

commit: a83acc7

mme added 2 commits May 4, 2026 17:26
…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.
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.
mme added 2 commits May 6, 2026 12:16
…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).
…_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.
@BenTaylorDev BenTaylorDev merged commit 905d742 into mme/register-proxied-agent May 6, 2026
10 checks passed
@BenTaylorDev BenTaylorDev deleted the mme/test-intelligence-indicator-renderer branch May 6, 2026 21:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants