Skip to content

Commit bda9400

Browse files
committed
fix(rich-editor): raw-fallback paste hook + bound the filtered mention list
- RawMarkdownField now honors onPasteText (e.g. skill SKILL.md destructuring), so a full-document paste is intercepted in the raw fallback too, not only the WYSIWYG path. - Bound the @-mention list while filtering (MAX_WHEN_FILTERED) so lifting the per-group cap for search can't render thousands of rows in the non-virtualized menu on a broad query; search still reaches deep matches well before the bound. Adds a test. - Tighten an extensions.ts doc comment (the headless path omits the registry + node-view construction, not React itself).
1 parent 8ccc309 commit bda9400

4 files changed

Lines changed: 48 additions & 8 deletions

File tree

apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/extensions.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -58,9 +58,9 @@ const PipeSafeTable = Table.extend({
5858

5959
/**
6060
* Node-view variants the live editor injects in place of the headless defaults — the code-block
61-
* language picker, the resizable image, and the mention chip. They pull React (and, for the mention
62-
* chip, the block registry for brand icons), so the headless round-trip path omits them: passing
63-
* nothing keeps {@link createMarkdownContentExtensions} free of React and the registry.
61+
* language picker, the resizable image, and the mention chip. The mention chip pulls the block registry
62+
* (for brand icons), so the headless round-trip path omits it: passing nothing keeps
63+
* {@link createMarkdownContentExtensions} free of the registry and constructs no React node views.
6464
*/
6565
export interface ContentNodeViews {
6666
codeBlock?: Node

apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/mention/mention-list.test.tsx

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,29 @@ describe('MentionList keyboard nav', () => {
9898
expect(container.querySelectorAll('[role="option"]').length).toBe(12)
9999
})
100100

101+
it('bounds the filtered list so a broad query cannot flood the menu', () => {
102+
const ref = createRef<MentionListHandle>()
103+
const command = vi.fn()
104+
const store = createMentionStore()
105+
// 200 matches — far beyond any reasonable render; the list must cap the total.
106+
const flood: MentionItem[] = Array.from({ length: 200 }, (_, i) => ({
107+
kind: 'file',
108+
id: `f${i}`,
109+
label: `alpha-${i}`,
110+
group: 'Files',
111+
icon: File,
112+
}))
113+
114+
act(() => {
115+
root.render(
116+
<MentionList ref={ref} query='alpha' command={command} store={store} editor={editor} />
117+
)
118+
})
119+
act(() => store.set(flood))
120+
121+
expect(container.querySelectorAll('[role="option"]').length).toBe(50)
122+
})
123+
101124
it('accepts the active item on Tab, like Enter', () => {
102125
const ref = createRef<MentionListHandle>()
103126
const command = vi.fn()

apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/mention/mention-list.tsx

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@ interface MentionListProps {
2525
* search reaches every match, not just the first eight in a category. */
2626
const MAX_PER_GROUP = 8
2727

28+
/** Total cap while filtering: lifts the per-group limit so search reaches deep matches, but still bounds
29+
* the (non-virtualized) list so a broad single-char query on a huge workspace can't render thousands of
30+
* rows. Generous enough that a real search is never truncated before the user narrows further. */
31+
const MAX_WHEN_FILTERED = 50
32+
2833
/** Category heading order in the menu. */
2934
const GROUP_ORDER = [
3035
'Files',
@@ -51,17 +56,22 @@ export const MentionList = forwardRef<MentionListHandle, MentionListProps>(funct
5156
/**
5257
* Filtered, flattened in category order; `index` is the flat position for nav. A single pass over the
5358
* full set filters by label and buckets by group, then reads the buckets in category order — avoiding
54-
* a separate filter pass per group. The per-group cap applies only to the unfiltered menu; once a
55-
* query is active every match is shown so search can reach items past the eighth in a category.
59+
* a separate filter pass per group. Without a query each group is capped ({@link MAX_PER_GROUP}); with
60+
* a query the per-group cap is lifted (so search reaches deep matches) but the total is bounded
61+
* ({@link MAX_WHEN_FILTERED}) so a broad query can't flood the non-virtualized list.
5662
*/
5763
const { flat, groups } = useMemo(() => {
5864
const q = query.trim().toLowerCase()
5965
const byGroup = new Map<string, MentionItem[]>()
66+
let shown = 0
6067
for (const item of rawItems) {
6168
if (q && !item.label.toLowerCase().includes(q)) continue
69+
if (q && shown >= MAX_WHEN_FILTERED) break
6270
const bucket = byGroup.get(item.group)
63-
if (!bucket) byGroup.set(item.group, [item])
64-
else if (q || bucket.length < MAX_PER_GROUP) bucket.push(item)
71+
if (!q && bucket && bucket.length >= MAX_PER_GROUP) continue
72+
if (bucket) bucket.push(item)
73+
else byGroup.set(item.group, [item])
74+
shown++
6575
}
6676

6777
const ordered: { group: string; items: { item: MentionItem; index: number }[] }[] = []

apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/rich-markdown-field.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,9 @@ function LoadedRichMarkdownField({
192192

193193
/**
194194
* Raw-text fallback for content the rich editor can't round-trip losslessly — editing the markdown
195-
* source directly so an edit can't silently drop footnotes, raw HTML, or comments.
195+
* source directly so an edit can't silently drop footnotes, raw HTML, or comments. Honors the same
196+
* `onPasteText` hook as the WYSIWYG path (e.g. skill `SKILL.md` destructuring) so a full-document paste
197+
* is intercepted here too.
196198
*/
197199
function RawMarkdownField({
198200
value,
@@ -203,11 +205,16 @@ function RawMarkdownField({
203205
minHeight = 140,
204206
maxHeight = 360,
205207
error = false,
208+
onPasteText,
206209
}: RichMarkdownFieldProps) {
207210
return (
208211
<ChipTextarea
209212
value={value}
210213
onChange={(event) => onChange(event.target.value)}
214+
onPaste={(event) => {
215+
const text = event.clipboardData.getData('text/plain')
216+
if (text && onPasteText?.(text)) event.preventDefault()
217+
}}
211218
placeholder={placeholder}
212219
error={error}
213220
readOnly={disabled || isStreaming}

0 commit comments

Comments
 (0)