From e48088c22e885de8d8773ce84ca4072378e10650 Mon Sep 17 00:00:00 2001 From: Konstantin Kolesnyak Date: Thu, 18 Jun 2026 16:07:07 +0200 Subject: [PATCH] feat(sidebar): replace header with Search/New chat pills - Remove 'Conversations' header label - Convert search field into a pill button labeled 'Search sessions'; the input mounts on hover (or focus / when a query exists) with a 200ms fade and unmounts on leave so it never lingers in the DOM - Add a 'New chat' pill on the right; the search input overlays it while open so the create button is hidden behind the field - Both controls now share rounded-full styling with a subtle border to read clearly as buttons --- web/src/components/Chat/SessionSidebar.tsx | 121 +++++++++++++++------ 1 file changed, 90 insertions(+), 31 deletions(-) diff --git a/web/src/components/Chat/SessionSidebar.tsx b/web/src/components/Chat/SessionSidebar.tsx index cc0c49c..5e90b20 100644 --- a/web/src/components/Chat/SessionSidebar.tsx +++ b/web/src/components/Chat/SessionSidebar.tsx @@ -42,12 +42,50 @@ export function SessionSidebar({ sessions, activeSession, agentStatus, onSelect, }) { const [systemExpanded, setSystemExpanded] = useState(false); const [localQuery, setLocalQuery] = useState(''); + const [searchHovered, setSearchHovered] = useState(false); + const [searchFocused, setSearchFocused] = useState(false); + const [searchMounted, setSearchMounted] = useState(false); + const [searchVisible, setSearchVisible] = useState(false); + const closeTimerRef = useRef | null>(null); const debounceRef = useRef | null>(null); const inputRef = useRef(null); const { searchResults, searchLoading, searchSessions, clearSearch, renameSession, toggleStar } = useChatStore(); const isSearching = localQuery.trim().length > 0; + const shouldShowSearch = searchHovered || searchFocused || isSearching; + + // Mount/unmount the search input with a fade transition (200ms). + useEffect(() => { + if (shouldShowSearch) { + if (closeTimerRef.current) { + clearTimeout(closeTimerRef.current); + closeTimerRef.current = null; + } + setSearchMounted(true); + } else if (searchMounted) { + setSearchVisible(false); + closeTimerRef.current = setTimeout(() => { + setSearchMounted(false); + closeTimerRef.current = null; + }, 200); + } + }, [shouldShowSearch, searchMounted]); + + // After mount, flip to visible on next frame so the CSS transition runs. + useEffect(() => { + if (searchMounted && !searchVisible) { + const id = requestAnimationFrame(() => setSearchVisible(true)); + return () => cancelAnimationFrame(id); + } + }, [searchMounted, searchVisible]); + + // Clean up pending close timer on unmount. + useEffect(() => { + return () => { + if (closeTimerRef.current) clearTimeout(closeTimerRef.current); + }; + }, []); // Debounced search const handleSearchChange = useCallback((value: string) => { @@ -125,37 +163,58 @@ export function SessionSidebar({ sessions, activeSession, agentStatus, onSelect, return (
- {/* Header */} -
- Conversations - -
- - {/* Search */} -
-
- - handleSearchChange(e.target.value)} - placeholder="Search sessions..." - className="w-full bg-surface-raised border border-border rounded-md text-[12px] text-text-secondary placeholder-text-faint pl-7 pr-7 py-1.5 outline-none focus:border-text-faint transition-colors" - /> - {isSearching && ( - + {/* Search + New chat */} +
+
+ {/* Search pill (always visible, hover-zone trigger) */} + + + {/* New chat pill (hidden under input when open) */} + + + {searchMounted && ( + <> + handleSearchChange(e.target.value)} + onFocus={() => setSearchFocused(true)} + onBlur={() => setSearchFocused(false)} + onMouseEnter={() => setSearchHovered(true)} + onMouseLeave={() => setSearchHovered(false)} + placeholder="Search sessions..." + className={`absolute inset-0 w-full h-full bg-surface-raised border border-border rounded-md text-[12px] text-text-secondary placeholder-text-faint pl-7 pr-7 outline-none focus:border-text-faint transition-all duration-200 ease-out z-20 ${ + searchVisible ? 'opacity-100' : 'opacity-0 pointer-events-none' + }`} + /> + {isSearching && ( + + )} + )}