diff --git a/apps/web/src/components/ComposerPromptEditor.tsx b/apps/web/src/components/ComposerPromptEditor.tsx index 169126788ae8..d419fc0d87d0 100644 --- a/apps/web/src/components/ComposerPromptEditor.tsx +++ b/apps/web/src/components/ComposerPromptEditor.tsx @@ -868,6 +868,7 @@ export interface ComposerPromptEditorHandle { focus: () => void; focusAt: (cursor: number) => void; focusAtEnd: () => void; + insertText: (text: string) => void; readSnapshot: () => { value: string; cursor: number; @@ -1691,9 +1692,19 @@ function ComposerPromptEditorInner({ ), ); }, + insertText: (text: string) => { + editor.update(() => { + const selection = $getSelection(); + if ($isRangeSelection(selection)) { + selection.insertRawText(text); + return; + } + $getRoot().selectEnd().insertRawText(text); + }); + }, readSnapshot, }), - [focusAt, readSnapshot], + [editor, focusAt, readSnapshot], ); const handleEditorChange = useCallback((editorState: EditorState) => { diff --git a/apps/web/src/components/chat/ChatComposer.tsx b/apps/web/src/components/chat/ChatComposer.tsx index e92ecd497e3d..bcf918ea7711 100644 --- a/apps/web/src/components/chat/ChatComposer.tsx +++ b/apps/web/src/components/chat/ChatComposer.tsx @@ -273,6 +273,17 @@ function isInsideComposerFloatingLayer(element: Element): boolean { return element.closest(COMPOSER_FLOATING_LAYER_SELECTOR) !== null; } +/** A pasted link the user meant to keep, as opposed to accessory clipboard text. */ +function isHttpUrlText(text: string): boolean { + if (!/^https?:\/\//i.test(text)) return false; + try { + const url = new URL(text); + return url.protocol === "http:" || url.protocol === "https:"; + } catch { + return false; + } +} + const ComposerFooterModeControls = memo(function ComposerFooterModeControls(props: { showInteractionModeToggle: boolean; interactionMode: ProviderInteractionMode; @@ -2392,6 +2403,16 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps) if (imageFiles.length === 0) return; event.preventDefault(); void addComposerImages(imageFiles); + // Copying a link from a browser or chat app often puts the URL text and a + // preview image on the clipboard together. preventDefault above suppresses + // the native text insertion, so re-insert the text half instead of + // silently dropping it with the attachment. Only URL-shaped text + // qualifies: OS file copies and image copies can carry accessory + // text/plain (file names, local paths) the user never meant to insert. + const text = event.clipboardData.getData("text/plain").trim(); + if (isHttpUrlText(text)) { + composerEditorRef.current?.insertText(text); + } }; const onComposerDragEnter = (event: React.DragEvent) => {