Skip to content

fix(copilot): stop chat panel from wiping on tool invocation (ENT-517)#2

Open
dustywusty wants to merge 1 commit intomainfrom
dustywusty/investigate-tool-refresh-issue
Open

fix(copilot): stop chat panel from wiping on tool invocation (ENT-517)#2
dustywusty wants to merge 1 commit intomainfrom
dustywusty/investigate-tool-refresh-issue

Conversation

@dustywusty
Copy link
Copy Markdown

@dustywusty dustywusty commented May 8, 2026

Summary

The deployed Copilot chat would silently wipe back to the welcome screen any time the model invoked a tool. Three independent bugs all produced the same symptom; this PR fixes the root cause + the worst secondary issues that surfaced once the chat stopped wiping.

Root cause — CopilotKitProvider thrashing

@copilotkit/react-core/v2's CopilotKitProvider declares headers = {}, properties = {}, agents__unsafe_dev_only = {}, selfManagedAgents = {} as default param values. JS evaluates those defaults on every render, producing fresh {} literals each time. Those refs invalidate the internal useMemos for mergedHeaders / mergedAgents. The deps of the giant setup useEffect (setRuntimeUrl + setRuntimeTransport + setHeaders + setProperties + setAgents__unsafe_dev_only) include those memos, so the effect re-fires on every render. Mid-tool-run, this reconnects the runtime and replaces the agent's threadId — the visible "chat reset to welcome" symptom.

Fix: pass module-level constant {} for those four props in CopilotProvider.tsx. Refs stay stable, the effect runs once at mount, no thread thrashing.

Secondary bugs surfaced after the chat stopped wiping

  • Quick-action buttons (Search Contacts, Lead Triage) silently did nothing. CopilotKit clones agents per-thread (getOrCreateThreadClone); useAgent() outside <CopilotChat> returned the BASE agent while the chat rendered a CLONED agent for its own thread. Lifted threadId ownership up to CopilotOverlayPanel and Dashboard, pass it to useAgent({ threadId }), and forward it to <CopilotChat> so both ends share the same threaded clone.
  • Tool throws caused CopilotKit's RunHandler to abort the run and spawn a fresh thread on the next message — same wipe symptom, different cause. Wrapped frontend-tool handlers in useAuditedFrontendTool to catch and return { error, ok: false } instead of re-throwing.
  • res.json() exploded on HTTP errors in useGetTopLeads, useGetContactsByCompany, useSearchContacts (no res.ok check). Added guards.
  • useCreateTask now validates contactId via dataProvider.getOne before creating, and sets sales_id from the logged-in identity (the dashboard's Upcoming Tasks widget filters by sales_id, so chat-created tasks were invisible).
  • useSearchContacts now accepts firstName / lastName parameters (the server already supported them); previously the agent had no way to look up a contact by name.
  • useGetTopLeads now pre-shapes its response to match LeadPriorityList's prop names so the agent doesn't have to translate snake_case → camelCase per row.
  • Wrapped CopilotOverlayPanelGate in its own <ErrorBoundary> so a render error in the panel shows a visible fallback instead of silently unmounting.

Dev-quality changes

  • Vite dev proxy now matches /api/* (was /api/copilotkit) and accepts a COPILOTKIT_PROXY_TARGET env var, so frontend devs can hit the deployed CopilotKit backend without running the local server.
  • README updated with the new dev workflow.

Test plan

  • npm run typecheck clean
  • npm run lint clean (no new warnings on changed files)
  • Click Lead Triage on the dashboard → user message appears in chat, getTopLeads runs, LeadPriorityList renders with names linking to contact pages, scores, and "Lifecycle · Last activity: …" subtitles
  • "create a task to call Shelley Kerluke on 5/10/26 for a CopilotKit interview"searchContacts finds her, createTask succeeds, task appears under Shelley's contact page AND on the dashboard's Upcoming Tasks widget
  • Force a tool failure (e.g. unreachable backend) → chat shows the model's apology, threadId stays stable, panel does not wipe
  • Verify on Render after deploy

@dustywusty dustywusty requested a review from mxmzb May 8, 2026 07:56
Root cause: CopilotKitProvider declares headers/properties/agents as default
`= {}` params, which create fresh object references on every render. Those
refs invalidate the internal useMemos for mergedHeaders/mergedAgents, which
re-fires the giant setRuntimeUrl/setRuntimeTransport effect mid-tool-run
and replaces the agent's threadId. The visible symptom was "chat resets to
welcome screen with no console errors" any time a tool was invoked.

Pass module-level constant {} for those four props so they stay stable
across renders and the runtime reconnect effect only runs once at mount.

Additional fixes surfaced while investigating:

- Lift threadId ownership from CopilotChat into CopilotOverlayPanel and
  Dashboard; pass it explicitly to useAgent({threadId}) so quick-action
  buttons (Search Contacts, Lead Triage) operate on the same threaded
  agent clone CopilotChat renders from. Without this, addMessage went to
  the BASE agent while the chat displayed a different thread's clone, so
  the buttons appeared to do nothing.

- Wrap frontend-tool handlers with try/catch that returns {error, ok:false}
  instead of throwing. A thrown handler caused CopilotKit's RunHandler to
  abort the run and spawn a fresh thread on the next user message — same
  user-visible wipe symptom from a different cause.

- Add res.ok guards in useGetTopLeads, useGetContactsByCompany,
  useSearchContacts so HTTP errors become typed errors instead of
  res.json() parse explosions.

- useCreateTask now validates contactId against the data provider before
  creating, and sets sales_id from the logged-in identity (the dashboard's
  Upcoming Tasks widget filters by sales_id, so omitting it hid chat-
  created tasks).

- useSearchContacts exposes firstName/lastName params (server already
  supported them) so the agent can look up contacts by name rather than
  guessing at company-only filters.

- useGetTopLeads pre-shapes its response to match LeadPriorityList's prop
  names (contactId, name, score, lifecycleStage, lastActivity), so the
  agent doesn't have to translate snake_case → camelCase per row.

- Wrap CopilotOverlayPanelGate in its own ErrorBoundary so an uncaught
  render error in the panel surfaces a visible fallback instead of
  silently unmounting the subtree.

- Broaden the demo vite proxy from /api/copilotkit to /api/* and add a
  COPILOTKIT_PROXY_TARGET env-var override, so dev can run the frontend
  alone against the deployed CopilotKit backend without touching CORS.
  README updated with the workflow.
@dustywusty dustywusty force-pushed the dustywusty/investigate-tool-refresh-issue branch from e6f8661 to 0cd5f6b Compare May 8, 2026 08:01
@dustywusty dustywusty requested a review from MikeRyanDev May 8, 2026 08:01
@dustywusty dustywusty changed the title fix(copilot): stop chat panel from wiping on tool invocation fix(copilot): stop chat panel from wiping on tool invocation (ENT-517) May 8, 2026
@linear
Copy link
Copy Markdown

linear Bot commented May 8, 2026

ENT-517

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.

1 participant