Canvas SDK: <Link> click handlers leak across webview mounts — clicking a link opens N tabs after N canvas opens

Where does the bug appear (feature/product)?

Cursor IDE

Describe the Bug

Summary
In a .canvas.tsx using the cursor/canvas SDK, each canvas mount leaves a persistent click listener behind that survives tab close. Every subsequent mount accumulates another listener, so clicking a single eventually opens N tabs with the same URL (N = number of times that canvas file has been mounted in the current window).

Steps to Reproduce

Canvas SDK imports used: Link, Card, CardHeader, CardBody, TextArea, Select, Button, IconButton, useCanvasState, useCanvasAction

  • Ctrl+Shift+P → “Developer: Reload Window” to start with no ghost mounts
  • Open a .canvas.tsx that contains at least one <Link href="https://example.com/test">click</Link> inside any <Card>
  • Click the link → 1 tab opens (expected)
  • Close the canvas tab (not the IDE)
  • Reopen the canvas via the chat button or by opening the file
  • Click the same link → 2 tabs open with the same URL
  • Close + reopen again → click → 3 tabs
  • Each cycle adds exactly one more tab. Linear accumulation.
  • Only Developer: Reload Window resets the count.

The leak is worse (more ghosts per open, or triggers more reliably) when the canvas contains:

  • <TextArea> with controlled value + onChange — especially when rendered inside a repeating .map() loop (e.g. one per item in a list)
  • <Select> — similar pattern
  • Multiple useCanvasState calls — consolidating all state into a single useCanvasState with a compound object drops the leak rate back to ~1 per mount
  • Hot reloads while canvas is open — every file rewrite triggers a remount that also leaves a ghost behind, compounding with normal close/reopen leak

Expected Behavior

Clicking a link just opens 1 window/tab

Operating System

Windows 10/11

Version Information

3.1.15 IDE

Additional Information

Either the cursor/canvas React runtime is adding listeners at the webview/document level without a useEffect cleanup, or the Cursor webview host isn’t tearing down the React root on tab close (keeping the DOM alive with all listeners attached). The SDK’s <Link> component appears to use an explicit click handler (since clicks on an anchor in isolation shouldn’t fire N times via event bubbling). useCanvasState’s subscription appears to be the primary leak source — each hook instance adds to the persistent count.

Does this stop you from using Cursor

No - Cursor works, but with this issue

Hey! Thanks for the incredibly detailed report – the repro steps and analysis are super helpful.

This is a confirmed bug that our team is actively investigating. The listener accumulation pattern you described matches what we’re seeing on our end.

As you noted, Developer: Reload Window resets the count in the meantime. We’ll update this thread when the fix ships.