Skip to content

Commit 1392a74

Browse files
committed
fix(chat): don't attach a selection chip to a copy from a nested input
Cursor Bugbot: a copy from a field inside the editor — Monaco's find box being the common one — bubbles to the container while the document still holds a highlight, so the bridge attached the editor selection to text the user never copied. Chat paste then prefers the custom MIME and inserts a reference chip instead of the search term. Skips INPUT targets only. Copying the table grid's INPUT/TEXTAREA guard would have suppressed the chip on the main copy path this hook exists for: Monaco's own editing surface is a hidden textarea, unlike the grid's cell editors, which really are form fields. Tests cover both directions — chip attached from the textarea surface, skipped from a nested input — and were verified to fail against the missing guard and against the INPUT+TEXTAREA variant.
1 parent d3b6878 commit 1392a74

2 files changed

Lines changed: 97 additions & 0 deletions

File tree

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/**
2+
* @vitest-environment jsdom
3+
*/
4+
import { act, createRef, type RefObject } from 'react'
5+
import { createRoot, type Root } from 'react-dom/client'
6+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
7+
import { SIM_SELECTION_MIME } from '@/lib/copilot/chat/selection-clipboard'
8+
import type { ChatContext } from '@/stores/panel'
9+
import { useSelectionCopyBridge } from './use-selection-copy-bridge'
10+
11+
const selection: ChatContext = {
12+
kind: 'file_selection',
13+
fileId: 'f1',
14+
fileName: 'notes.md',
15+
label: 'notes.md:2-4',
16+
text: 'the exact passage',
17+
}
18+
19+
let container: HTMLDivElement
20+
let root: Root
21+
let containerRef: RefObject<HTMLDivElement | null>
22+
let buildContext: ReturnType<typeof vi.fn>
23+
24+
/**
25+
* Mirrors the editors this hook wraps: Monaco's editing surface is a hidden
26+
* textarea, and its find widget is a real input nested in the same container.
27+
*/
28+
function Host() {
29+
useSelectionCopyBridge(containerRef, buildContext as () => ChatContext | null)
30+
return (
31+
<div ref={containerRef}>
32+
<textarea id='editor-surface' />
33+
<input id='find-box' />
34+
</div>
35+
)
36+
}
37+
38+
/** Dispatches a bubbling copy from `id` and returns what was written. */
39+
function dispatchCopy(id: string): Record<string, string> {
40+
const written: Record<string, string> = {}
41+
const event = new Event('copy', { bubbles: true }) as ClipboardEvent
42+
Object.defineProperty(event, 'clipboardData', {
43+
value: {
44+
setData: (type: string, value: string) => {
45+
written[type] = value
46+
},
47+
},
48+
})
49+
act(() => {
50+
container.querySelector(`#${id}`)?.dispatchEvent(event)
51+
})
52+
return written
53+
}
54+
55+
describe('useSelectionCopyBridge', () => {
56+
beforeEach(() => {
57+
container = document.createElement('div')
58+
document.body.appendChild(container)
59+
containerRef = createRef<HTMLDivElement>()
60+
buildContext = vi.fn(() => selection)
61+
root = createRoot(container)
62+
act(() => {
63+
root.render(<Host />)
64+
})
65+
})
66+
67+
afterEach(() => {
68+
act(() => root.unmount())
69+
container.remove()
70+
vi.clearAllMocks()
71+
})
72+
73+
it('attaches the selection when copying from the editor surface', () => {
74+
const written = dispatchCopy('editor-surface')
75+
76+
expect(buildContext).toHaveBeenCalled()
77+
expect(written[SIM_SELECTION_MIME]).toContain('file_selection')
78+
})
79+
80+
it('ignores a copy from a nested input such as the find box', () => {
81+
// The document still holds a highlight, so without the guard the chip would
82+
// ride onto text the user never copied.
83+
const written = dispatchCopy('find-box')
84+
85+
expect(buildContext).not.toHaveBeenCalled()
86+
expect(written[SIM_SELECTION_MIME]).toBeUndefined()
87+
})
88+
})

apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/use-selection-copy-bridge.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,15 @@ export function useSelectionCopyBridge(
2626
const dom = containerRef.current
2727
if (!dom || !enabled) return
2828
const onCopy = (e: ClipboardEvent) => {
29+
// A copy from a field nested in the editor — Monaco's find box being the
30+
// common one — bubbles here while the document still holds a highlight,
31+
// so the selection would be attached to text the user never copied.
32+
//
33+
// Only INPUT is skipped, deliberately: Monaco's own editing surface is a
34+
// hidden TEXTAREA, so excluding textareas (as the table grid does, where
35+
// the cell editors really are form fields) would suppress the chip on the
36+
// main copy path this hook exists for.
37+
if ((e.target as HTMLElement | null)?.tagName === 'INPUT') return
2938
const context = buildContext()
3039
if (context) attachSelectionContextToClipboard(e.clipboardData, context)
3140
}

0 commit comments

Comments
 (0)