diff --git a/apps/web/src/components/ComposerPromptEditorTiptap.tsx b/apps/web/src/components/ComposerPromptEditorTiptap.tsx index 5126246dd05c7e5e74602f57bbf251dd233d851a..c108207304c43ac69e250b1664ba2099dd7a601f 100644 --- a/apps/web/src/components/ComposerPromptEditorTiptap.tsx +++ b/apps/web/src/components/ComposerPromptEditorTiptap.tsx @@ -725,6 +725,19 @@ function ComposerPromptEditorTiptapInner(props: ComposerPromptEditorProps) { ); }, []); + const editorAttributes = useMemo( + () => ({ + class: cn( + "composer-tiptap block max-h-50 min-h-17.5 w-full overflow-y-auto whitespace-pre-wrap wrap-break-word bg-transparent leading-relaxed text-foreground focus:outline-none", + className, + ), + "data-testid": "composer-editor", + "data-composer-rich-text": richText ? "true" : "false", + "aria-placeholder": placeholder, + }), + [className, placeholder, richText], + ); + const editor = useEditor( { extensions: [ @@ -787,15 +800,7 @@ function ComposerPromptEditorTiptapInner(props: ComposerPromptEditorProps) { ), editable: !disabled, editorProps: { - attributes: { - class: cn( - "composer-tiptap block max-h-50 min-h-17.5 w-full overflow-y-auto whitespace-pre-wrap wrap-break-word bg-transparent leading-relaxed text-foreground focus:outline-none", - className, - ), - "data-testid": "composer-editor", - "data-composer-rich-text": richText ? "true" : "false", - "aria-placeholder": placeholder, - }, + attributes: editorAttributes, handleKeyDown: (view, event) => { if ( isMacPlatform(navigator.platform) && @@ -995,6 +1000,17 @@ function ComposerPromptEditorTiptapInner(props: ComposerPromptEditorProps) { editorHolder.current = editor; }, [editor]); + // Tiptap forwards option changes to the view from a passive effect, so a + // class change here would reach the ProseMirror element one tick after + // React commits. The chat composer measures its resting and expanded + // geometry in layout effects that run first, and it clamps the prompt + // through `className`, so the attributes are pushed to the view here for + // those measurements to see the layout they are about to reserve for. + useLayoutEffect(() => { + if (!editor?.isInitialized) return; + editor.view.setProps({ attributes: editorAttributes }); + }, [editor, editorAttributes]); + const readSnapshot = useCallback(() => { const snapshot = snapshotRef.current; if (!editor) return snapshot;