From c420c6d63eda6f49fb816e41088d585983ee0559 Mon Sep 17 00:00:00 2001 From: Haileyesus <118998054+blackmammoth@users.noreply.github.com> Date: Tue, 30 Jun 2026 00:06:25 +0300 Subject: [PATCH] fix(chat): header ellipsis, Codex logo on light theme, portal copy menu - MainContentTitle: truncate the session title with an ellipsis instead of horizontal-scrolling it - MessageComponent: use text-foreground for the provider logo chip so the currentColor Codex/OpenAI mark is visible on the light theme - MessageCopyControl: render the copy-format dropdown in a portal so it escapes the chat message's `contain: paint` clip box; anchor it to the trigger, flip above near the viewport bottom, close on scroll/resize --- .../view/subcomponents/MessageComponent.tsx | 2 +- .../view/subcomponents/MessageCopyControl.tsx | 61 ++++++++++++++++--- .../view/subcomponents/MainContentTitle.tsx | 2 +- 3 files changed, 53 insertions(+), 12 deletions(-) diff --git a/src/components/chat/view/subcomponents/MessageComponent.tsx b/src/components/chat/view/subcomponents/MessageComponent.tsx index b326a876..552b31cb 100644 --- a/src/components/chat/view/subcomponents/MessageComponent.tsx +++ b/src/components/chat/view/subcomponents/MessageComponent.tsx @@ -166,7 +166,7 @@ const MessageComponent = memo(({ message, prevMessage, createDiff, onFileOpen, a 🔧 ) : ( -
+
)} diff --git a/src/components/chat/view/subcomponents/MessageCopyControl.tsx b/src/components/chat/view/subcomponents/MessageCopyControl.tsx index aeacd45c..c02b5676 100644 --- a/src/components/chat/view/subcomponents/MessageCopyControl.tsx +++ b/src/components/chat/view/subcomponents/MessageCopyControl.tsx @@ -1,4 +1,6 @@ import { useEffect, useMemo, useRef, useState } from 'react'; +import type { CSSProperties } from 'react'; +import { createPortal } from 'react-dom'; import { useTranslation } from 'react-i18next'; import { copyTextToClipboard } from '../../../../utils/clipboard'; @@ -49,9 +51,32 @@ const MessageCopyControl = ({ const [selectedFormat, setSelectedFormat] = useState(defaultFormat); const [copied, setCopied] = useState(false); const [isDropdownOpen, setIsDropdownOpen] = useState(false); + const [menuStyle, setMenuStyle] = useState({}); const dropdownRef = useRef(null); + const triggerRef = useRef(null); + const menuRef = useRef(null); const copyFeedbackTimerRef = useRef | null>(null); + // The dropdown is rendered in a portal so it escapes the chat message's + // `contain: paint` box (which would otherwise clip it). Anchor it to the + // trigger, flipping above when there isn't room below. + const openDropdown = () => { + const rect = triggerRef.current?.getBoundingClientRect(); + if (rect) { + const ESTIMATED_MENU_HEIGHT = 84; + const openUp = rect.bottom + ESTIMATED_MENU_HEIGHT + 8 > window.innerHeight; + setMenuStyle({ + position: 'fixed', + right: Math.max(8, window.innerWidth - rect.right), + zIndex: 1000, + ...(openUp + ? { bottom: window.innerHeight - rect.top + 4 } + : { top: rect.bottom + 4 }), + }); + } + setIsDropdownOpen(true); + }; + const copyFormatOptions: CopyFormatOption[] = useMemo( () => [ { @@ -83,18 +108,28 @@ const MessageCopyControl = ({ }, [defaultFormat]); useEffect(() => { - // Close the dropdown when clicking anywhere outside this control. + if (!isDropdownOpen) return; + + // Close when clicking outside both the control and the portaled menu. const closeOnOutsideClick = (event: MouseEvent) => { - if (!isDropdownOpen) return; const target = event.target as Node; - if (dropdownRef.current && !dropdownRef.current.contains(target)) { - setIsDropdownOpen(false); + if (dropdownRef.current?.contains(target) || menuRef.current?.contains(target)) { + return; } + setIsDropdownOpen(false); }; + // The menu is fixed-positioned; close it if the page scrolls so it can't + // detach from the trigger. + const closeOnScroll = () => setIsDropdownOpen(false); + window.addEventListener('mousedown', closeOnOutsideClick); + window.addEventListener('scroll', closeOnScroll, true); + window.addEventListener('resize', closeOnScroll); return () => { window.removeEventListener('mousedown', closeOnOutsideClick); + window.removeEventListener('scroll', closeOnScroll, true); + window.removeEventListener('resize', closeOnScroll); }; }, [isDropdownOpen]); @@ -170,8 +205,9 @@ const MessageCopyControl = ({ {canSelectCopyFormat && ( <> - {isDropdownOpen && ( -
+ {isDropdownOpen && createPortal( +
{copyFormatOptions.map((option) => { const isSelected = option.format === selectedFormat; return ( @@ -196,15 +236,16 @@ const MessageCopyControl = ({ type="button" onClick={() => handleFormatChange(option.format)} className={`block w-full rounded px-2 py-1.5 text-left transition-colors ${isSelected - ? 'bg-gray-100 text-gray-900 dark:bg-gray-800 dark:text-gray-100' - : 'text-gray-700 hover:bg-gray-50 dark:text-gray-300 dark:hover:bg-gray-800/60' + ? 'bg-accent text-foreground' + : 'text-foreground hover:bg-accent' }`} > {option.label} ); })} -
+
, + document.body, )} )} diff --git a/src/components/main-content/view/subcomponents/MainContentTitle.tsx b/src/components/main-content/view/subcomponents/MainContentTitle.tsx index af8b0dec..e3d8776f 100644 --- a/src/components/main-content/view/subcomponents/MainContentTitle.tsx +++ b/src/components/main-content/view/subcomponents/MainContentTitle.tsx @@ -70,7 +70,7 @@ export default function MainContentTitle({
{activeTab === 'chat' && selectedSession ? (
-

+

{getSessionTitle(selectedSession)}

{selectedProject.displayName}