From 69b7f4cd1be5be4ad413cfd94b4f93b28a842b4b Mon Sep 17 00:00:00 2001 From: Bohan Jiang <52446949+Bohan-J@users.noreply.github.com> Date: Wed, 13 May 2026 17:15:09 +0800 Subject: [PATCH] fix(chat): commit rename only on real outside click, not on hover (#2524) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Base UI's Menu uses focus-follows-cursor — hovering a sibling row drags DOM focus to that row, which made the rename input's onBlur=save fire just from moving the mouse. The result: clicking the pencil and then nudging the cursor would silently commit a half-typed title. Replace the blur handler with a document-level pointerdown listener (capture phase, so it runs before Base UI's outside-click close handler unmounts the input). The listener only commits when the user actually clicks somewhere outside the input. Enter still commits, Escape still cancels, mouse hover is now a no-op. MUL-2110 Co-authored-by: multica-agent --- .../views/chat/components/chat-window.tsx | 36 ++++++++++++++++--- 1 file changed, 31 insertions(+), 5 deletions(-) diff --git a/packages/views/chat/components/chat-window.tsx b/packages/views/chat/components/chat-window.tsx index 04fc27e8b..d6935eb81 100644 --- a/packages/views/chat/components/chat-window.tsx +++ b/packages/views/chat/components/chat-window.tsx @@ -1019,11 +1019,16 @@ function SessionDropdown({ /** * Inline editor for a session title. Mounts focused with the existing * title pre-selected so the user can either replace it outright or arrow - * into the existing text. Enter commits, Escape / blur cancels. + * into the existing text. Enter commits, Escape cancels, a real click + * outside the input also commits. * - * Lives inside a DropdownMenuItem; we stop propagation on keys and clicks - * so Base UI's menu doesn't intercept arrow / space / enter for navigation - * while the user is typing. + * We do NOT commit on the input's `blur` event: Base UI's Menu uses + * focus-follows-cursor (hovering a sibling row drags DOM focus there), + * so a blur handler would fire on every mouse-move and "save" the user's + * half-typed title without them clicking anywhere. Instead a document- + * level `pointerdown` listener — registered in capture phase so it runs + * before Base UI's outside-click close handler — commits when the user + * actually clicks outside the input. */ function SessionRenameInput({ initialValue, @@ -1037,10 +1042,32 @@ function SessionRenameInput({ const { t } = useT("chat"); const [value, setValue] = useState(initialValue); const inputRef = useRef(null); + // Hold the latest value + callback in refs so the mount-only effect's + // listener always sees fresh state without re-subscribing on every + // keystroke (which would briefly leave a window where pointerdown isn't + // observed). + const valueRef = useRef(value); + valueRef.current = value; + const onSubmitRef = useRef(onSubmit); + onSubmitRef.current = onSubmit; useEffect(() => { inputRef.current?.focus(); inputRef.current?.select(); + + const handlePointerDown = (e: PointerEvent) => { + const input = inputRef.current; + if (!input) return; + if (input.contains(e.target as Node)) return; + onSubmitRef.current(valueRef.current); + }; + // Capture phase — Base UI registers its own outside-click handler in + // bubble; running first lets us commit before the menu starts to + // close (and unmount this component). + document.addEventListener("pointerdown", handlePointerDown, true); + return () => { + document.removeEventListener("pointerdown", handlePointerDown, true); + }; }, []); return ( @@ -1064,7 +1091,6 @@ function SessionRenameInput({ onCancel(); } }} - onBlur={() => onSubmit(value)} className="w-full rounded-sm bg-background px-1 py-0.5 text-sm outline-none ring-1 ring-border focus-visible:ring-brand" /> );