Skip to content

Commit e8192a1

Browse files
committed
fix(react-core): address CR-loop findings on IntelligenceIndicator
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.
1 parent 678d143 commit e8192a1

3 files changed

Lines changed: 39 additions & 22 deletions

File tree

packages/react-core/src/v2/components/chat/CopilotChatMessageView.tsx

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -330,8 +330,8 @@ const MemoizedCustomMessage = React.memo(
330330
// before runAgent fires) won't observe the false→true transition for
331331
// the first run. In practice this is rare; the canonical pattern is
332332
// to gate on the slot's own runId membership (see
333-
// CopilotKitProvider.intelligenceIndicator.e2e.test.tsx for the
334-
// recommended renderer shape).
333+
// intelligence-indicator/__tests__/IntelligenceIndicator.e2e.test.tsx
334+
// for the recommended renderer shape).
335335
if (
336336
nextProps.isInLatestRun &&
337337
prevProps.isRunning !== nextProps.isRunning
@@ -699,12 +699,17 @@ export function CopilotChatMessageView({
699699
);
700700
}
701701

702-
// Auto-mount the IntelligenceIndicator for every message slot when
703-
// the runtime is in intelligence mode. The component self-gates so
704-
// only the canonical slot (last message of latest in-flight run
705-
// containing a matching tool call) actually renders a pill — every
706-
// other invocation returns null.
707-
if (copilotkit.intelligence !== undefined) {
702+
// Auto-mount the IntelligenceIndicator on assistant message slots
703+
// when the runtime is in intelligence mode. The component self-gates
704+
// further (last in run, latest run, matching tool call) so only one
705+
// pill renders at a time — but mounting it only for assistant
706+
// messages avoids spinning up a `useAgent` subscription, a 200 ms
707+
// poll interval, and four effects on every user/reasoning/activity
708+
// slot just to have it return null at the role gate.
709+
if (
710+
copilotkit.intelligence !== undefined &&
711+
message.role === "assistant"
712+
) {
708713
elements.push(
709714
<IntelligenceIndicator
710715
key={`${message.id}-intelligence`}

packages/react-core/src/v2/components/intelligence-indicator/IntelligenceIndicator.tsx

Lines changed: 24 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,19 @@ const CHECK_HOLD_MS = 800;
2323
const FADE_OUT_ANIMATION_MS = 480;
2424

2525
/**
26-
* Polling interval for `agent.isRunning`. Background: AG-UI's
27-
* `runAgent` snapshots `[...this.subscribers]` at invocation time, so a
28-
* subscriber added AFTER the run starts — which is always the case here
29-
* since the renderer mounts when the matching message appears INSIDE
30-
* the run — is missing from that snapshot and never sees the falling
31-
* edge. `onMessagesChanged` works because `addMessage` iterates
32-
* subscribers live, but it stops firing once streaming ends, leaving
33-
* the renderer stuck on its last view of `isRunning=true`. A 200 ms
34-
* poll closes the gap.
26+
* Polling interval for `agent.isRunning`. Background: AG-UI's `runAgent`
27+
* snapshots `[...this.subscribers]` at invocation time and threads that
28+
* snapshot through the entire run pipeline (including the `finalize`
29+
* block that fires `onRunFinalized` and flips `isRunning` off). A
30+
* subscriber added AFTER `runAgent` starts — which is always the case
31+
* here, because the renderer mounts when the matching message first
32+
* appears INSIDE the run — is missing from that snapshot and never
33+
* receives the falling edge.
34+
*
35+
* Re-renders driven by parent state still keep the pill alive while
36+
* messages stream, but `agent.isRunning` only flips off via the
37+
* snapshotted set. A 200 ms poll reads the live property and forces a
38+
* re-render when it changes, closing the gap.
3539
*/
3640
const ISRUNNING_POLL_MS = 200;
3741

@@ -167,10 +171,17 @@ export function IntelligenceIndicator(
167171

168172
// Must be an assistant message with at least one matching tool call.
169173
if (message.role !== "assistant") return null;
170-
const toolCalls = message.toolCalls ?? [];
171-
const hasMatch = toolCalls.some((tc) =>
172-
DEFAULT_TOOL_PATTERNS.some((p) => p.test(tc.function.name)),
173-
);
174+
// Defensive: a malformed `toolCalls` (non-array, missing nested
175+
// `function.name`) would otherwise throw inside `.some(...)` and take
176+
// down the chat tree. Treat as "no match" instead.
177+
const toolCalls = Array.isArray(message.toolCalls) ? message.toolCalls : [];
178+
const hasMatch = toolCalls.some((tc) => {
179+
const name = tc?.function?.name;
180+
return (
181+
typeof name === "string" &&
182+
DEFAULT_TOOL_PATTERNS.some((p) => p.test(name))
183+
);
184+
});
174185
if (!hasMatch) return null;
175186

176187
// The message must belong to the latest run on the thread, AND must

packages/react-core/src/v2/styles/globals.css

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,7 +233,8 @@
233233
* IntelligenceIndicator pill — visual styles ported from
234234
* CopilotKit/Intelligence #155.
235235
*
236-
* Color palette: violet/indigo glassmorphism (#BEC2FF / #9599E0 / #EEE6FE).
236+
* Violet/indigo glassmorphism — text #5B21B6, icon #7C3AED, border
237+
* #9599E0, gradient stop #EEE6FE, soft shadow #5E64AD.
237238
*/
238239
.cpk-intelligence-pill {
239240
display: inline-flex;

0 commit comments

Comments
 (0)