Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
1 Skipped Deployment
|
|
agents-manage-ui/src/components/agent/copilot/message-parts/tool-approval.tsx
Outdated
Show resolved
Hide resolved
agents-manage-ui/src/components/agent/copilot/message-parts/tool-approval.tsx
Outdated
Show resolved
Hide resolved
sarah-inkeep
left a comment
There was a problem hiding this comment.
I'm seeing this error in the console when the tool approval ui shows:
Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.
Here is a loom:
https://www.loom.com/share/922d7d2cb78948d184437fd1149ab593
I checked on the main branch and I don't see the error there.
- Updated IkpTool component to use a functional component syntax for better clarity. - Enhanced parseToolOutputForRelations function to utilize flatMap for cleaner data extraction. - Introduced ApprovalButtons component to streamline approval button rendering in ToolApproval. - Replaced setConversationId with resetPlaygroundConversationId in ChatWidget and Playground components for consistency in state management. - Added playgroundConversationId to agent state for better tracking of conversation IDs.
There was a problem hiding this comment.
PR Review Summary
(2) Total Issues | Risk: High
🔴❗ Critical (1) ❗🔴
Inline Comments:
- 🔴 Critical:
copilot-chat.tsx:208-219Inline component definition causes infinite re-render loop (root cause of the "Maximum update depth exceeded" error)
🟡 Minor (1) 🟡
Inline Comments:
- 🟡 Minor:
tool-approval.tsx:90-115ApprovalButtonsreceives unstableapproveprop (contributing factor)
🕐 Pending Recommendations (2)
Items from prior reviews that remain applicable to unchanged code:
- 💭
copilot-chat.tsx:209-216Component invoked as function instead of JSX (now addressed by the delta, but replaced with another anti-pattern) - 💭
message.tsxFile naming no longer reflects content
🚫 REQUEST CHANGES
Summary: This PR introduces a critical regression reported by @sarah-inkeep: a "Maximum update depth exceeded" React infinite re-render bug that does not occur on the main branch. The root cause is the inline IkpTool wrapper function defined inside the components prop object in copilot-chat.tsx. Every render of CopilotChat creates a new function reference, causing React to unmount and remount the ToolApproval subtree, which triggers its useEffect → setLoading(true) → parent re-renders → infinite loop.
To fix: Memoize the IkpTool wrapper component using useMemo and ensure handleOAuthLogin has a stable reference by wrapping the onFinish callback in useCallback. See the inline comment on copilot-chat.tsx:208-219 for the complete fix.
The overall refactor goal (delegating to the widget package) is good — this just needs the memoization fix to avoid the re-render cascade.
Discarded (1)
| Location | Issue | Reason Discarded |
|---|---|---|
tool-approval.tsx:96 |
approveIcon = <CheckIcon /> creates new element each render |
While technically true, this is a common pattern and not the cause of the infinite loop; the component receives this as a default prop, not in a dependency array |
Reviewers (2)
| Reviewer | Returned | Main Findings | Consider | While You're Here | Inline Comments | Pending Recs | Discarded |
|---|---|---|---|---|---|---|---|
pr-review-frontend |
2 | 0 | 0 | 0 | 2 | 0 | 0 |
pr-review-standards |
1 | 0 | 0 | 0 | 1 | 0 | 1 |
| Total | 3 | 0 | 0 | 0 | 2 | 2 | 1 |
Note: Both reviewers converged on the same root cause (inline component definition). The findings were deduplicated into a single critical inline comment with the comprehensive fix.
| components: { | ||
| ...(IkpMessage | ||
| ? { | ||
| IkpMessage: (props: any) => | ||
| IkpMessage({ | ||
| ...props, | ||
| copilotAgentId: PUBLIC_INKEEP_COPILOT_AGENT_ID, | ||
| copilotProjectId: PUBLIC_INKEEP_COPILOT_PROJECT_ID, | ||
| copilotTenantId: PUBLIC_INKEEP_COPILOT_TENANT_ID, | ||
| apiUrl: PUBLIC_INKEEP_AGENTS_API_URL, | ||
| targetTenantId: tenantId, | ||
| targetProjectId: projectId, | ||
| targetAgentId: agentId, | ||
| onOAuthLogin: handleOAuthLogin, | ||
| refreshAgentGraph: refreshAgentGraph, | ||
| cookieHeader: cookieHeader, | ||
| copilotToken: copilotToken, | ||
| }), | ||
| } | ||
| : {}), | ||
| IkpTool(props) { | ||
| return ( | ||
| <IkpTool | ||
| {...props} | ||
| targetTenantId={tenantId} | ||
| targetProjectId={projectId} | ||
| onOAuthLogin={handleOAuthLogin} | ||
| refreshAgentGraph={refreshAgentGraph} | ||
| /> | ||
| ); | ||
| }, |
There was a problem hiding this comment.
🔴 CRITICAL React infinite re-render: Inline component definition causes unmount/remount loop
Issue: The IkpTool wrapper function is defined inline inside the components prop object. Every render of CopilotChat creates a new function reference, which React interprets as a completely new component type.
Why: When ToolApproval calls setLoading(true) in its useEffect, it can propagate back up, causing CopilotChat to re-render. This creates a new wrapper function reference, causing React to unmount and remount the entire IkpTool subtree, which triggers the useEffect again — creating an infinite loop. This matches the "Maximum update depth exceeded" error reported by @sarah-inkeep.
Fix: Memoize the component wrapper and ensure stable callback references:
// Step 1: At line ~49, wrap onFinish in useCallback to stabilize handleOAuthLogin:
const onOAuthFinish = useCallback(() => {
refreshAgentGraph({ fetchTools: true });
}, [refreshAgentGraph]);
const { handleOAuthLogin } = useOAuthLogin({
tenantId,
projectId,
onFinish: onOAuthFinish,
});
// Step 2: Before the return statement, create a stable component:
const IkpToolComponent = useMemo(
() =>
function IkpToolWrapper(props: React.ComponentProps<typeof IkpTool>) {
return (
<IkpTool
{...props}
targetTenantId={tenantId}
targetProjectId={projectId}
onOAuthLogin={handleOAuthLogin}
refreshAgentGraph={refreshAgentGraph}
/>
);
},
[tenantId, projectId, handleOAuthLogin, refreshAgentGraph]
);
// Step 3: At line 208, use the stable reference:
components: {
IkpTool: IkpToolComponent,
},Also add useMemo and useCallback to the react import at the top of the file.
Refs:
| const ApprovalButtons = ({ | ||
| state, | ||
| approve, | ||
| approveLabel = 'Approve', | ||
| approveVariant = 'default' as 'default' | 'destructive', | ||
| rejectLabel = 'Reject', | ||
| approveIcon = <CheckIcon className="size-3" />, | ||
| }: { | ||
| state: string; | ||
| approve: (approved?: boolean) => Promise<void>; | ||
| approveLabel?: string; | ||
| approveVariant?: 'default' | 'destructive' | 'destructive-outline'; | ||
| rejectLabel?: string; | ||
| approveIcon?: React.ReactNode; | ||
| }) => | ||
| state === 'approval-requested' && ( | ||
| <div className="flex gap-2 justify-end"> | ||
| <Button variant="outline" size="xs" type="button" onClick={() => approve(false)}> | ||
| {rejectLabel} | ||
| </Button> | ||
| <Button variant={approveVariant} size="xs" type="button" onClick={() => approve(true)}> | ||
| {approveIcon} | ||
| {approveLabel} | ||
| </Button> | ||
| </div> | ||
| ); |
There was a problem hiding this comment.
🟡 Minor Consider: ApprovalButtons receives unstable approve prop
Issue: The refactoring moved ApprovalButtons from an inline function (closure over approve) to a module-level component receiving approve as a prop. The approve callback from the parent widget library may not be memoized, causing unnecessary re-renders.
Why: While this is not the primary cause of the infinite re-render (that's in copilot-chat.tsx), it's a contributing factor. When approve is an unstable reference and React compares props between renders, it sees a "change" even when there's no semantic difference.
Fix: After fixing the primary issue in copilot-chat.tsx, if re-renders are still excessive, consider one of these options:
Option A: Move ApprovalButtons back inside ToolApproval (after the early returns):
// Inside ToolApproval, after loading/error checks:
const ApprovalButtons = ({ approveLabel, ... }: { ... }) =>
tool.state === 'approval-requested' && ( ... );Option B: Wrap ApprovalButtons with React.memo():
const ApprovalButtons = React.memo(({ state, approve, ... }) => ...)Refs:
No description provided.