mirror of
https://github.com/multica-ai/multica.git
synced 2026-08-12 10:59:06 +02:00
* fix(editor): stop pasted @ text from hijacking Enter and Escape (MUL-5429) Pasting a line containing `@` into the create-issue composer left an empty mention picker open that swallowed Enter, and Escape then closed the whole dialog instead of just the picker. Two independent causes: 1. The Tiptap suggestion plugin re-runs findSuggestionMatch on EVERY transaction, including the one that commits a paste, so pasted text containing `@` opened the picker. With `allowSpaces` the match runs from the `@` to the end of the line, so the entire pasted tail became one query that matched nothing — and the empty popup deliberately captures Enter. Gate the mention picker with `shouldShow`: skip paste/drop transactions, and skip queries longer than any real mention target (60 chars). The empty-list Enter capture is intentional and is left alone; the fix stops the picker opening instead. 2. ProseMirror calls preventDefault() for a handled key but never stops propagation, and Base UI's dismiss layer listens for Escape on `document` in the bubble phase without consulting defaultPrevented. So the Escape that closed the picker also closed the host dialog. Stop propagation in the shared suggestion popup handler — the only layer that knows a picker is open — which fixes every host at once. The slash pickers get the same paste guard so a pasted path no longer opens the command menu; they were never affected by the Enter capture. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai> * fix(editor): open mention and slash pickers only for typed triggers (MUL-5429) Pasting a line containing `@` opened an empty mention picker that swallowed Enter. The first attempt at this fix aimed at the wrong target and did not work in the product; this replaces it. The picker also opened with no paste involved at all: placing the caret at the end of a line that already contained `@` was enough. Tiptap's Suggestion plugin is state-derived, not event-driven — it re-runs findSuggestionMatch on EVERY transaction and asks whether the text before the cursor looks like a trigger, never whether the user just typed one. Pasted, dropped, undone and server-loaded text produce an identical document, so it cannot tell them apart. With allowSpaces the match then runs from the `@` to the end of the line, so the whole pasted tail became one query that matched nothing, and the empty popup deliberately captures Enter. That is upstream's known, unfixed design: ueberdosis/tiptap#4183 (open since 2023, labelled `complexity: hard`, reopened in January after `shouldShow` proved insufficient) and #7371. Upstream's position is that applications supply the missing provenance themselves. `shouldShow` alone cannot: it receives the transaction without `prev.active`, while `allow` receives `prev.active` without the transaction, so no transaction-inspecting predicate can express "only on the transaction that opened the picker". Add a trigger-arming plugin instead. `handleTextInput` is the one ProseMirror hook that fires for real keyboard and IME input only — paste goes through handlePaste/doPaste and drop through handleDrop, neither of which reaches it. Typing a trigger character arms its document position; the pickers' shouldShow opens only for a match anchored there; a deliberate caret move, or losing the trigger character, disarms it. This is the same signal Tiptap's own InputRules run on, which is why typing `- ` makes a list but pasting it does not. Suggestion is the one Tiptap feature that does not use it. Also stamp the paste metas markdownPaste was dropping. ProseMirror's doPaste returns early once a handlePaste prop claims the event, and markdownPaste is a catch-all that claims nearly every paste, so the transaction that commits a paste carried no `paste` / `uiEvent` mark. That is why the previous fix never fired in the product, and it independently broke issueIdentifierAutolink, which reads exactly that mark: pasting `See MUL-2 now` autolinked nothing, because the unmarked transaction sent it down the typing path that only inspects the token before the caret. Both features' paste tests passed throughout because they hand-built transactions carrying a meta the real paste path never produces. Every paste case now fires a real paste event, and typing is routed through handleTextInput the way readDOMChange does, so a test cannot be green while the product is broken. The Escape containment from the previous attempt is unrelated to all of this and is kept as is. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai>