From 0d04063aa6a6a5d00505ee489a64a251f56a8ff3 Mon Sep 17 00:00:00 2001 From: Finesssee <90105158+Finesssee@users.noreply.github.com> Date: Sun, 29 Mar 2026 02:10:51 +0700 Subject: [PATCH 1/4] Refine session history UI --- .../ai/components/history/sidebar.tsx | 307 ++++++++++++------ 1 file changed, 201 insertions(+), 106 deletions(-) diff --git a/src/features/ai/components/history/sidebar.tsx b/src/features/ai/components/history/sidebar.tsx index a12a0b0c..839a5bb6 100644 --- a/src/features/ai/components/history/sidebar.tsx +++ b/src/features/ai/components/history/sidebar.tsx @@ -1,10 +1,9 @@ -import { Check, Search, Trash2 } from "lucide-react"; +import * as DialogPrimitive from "@radix-ui/react-dialog"; +import { AnimatePresence, motion } from "framer-motion"; +import { Check, Search, Trash2, X } from "lucide-react"; import { useEffect, useMemo, useRef, useState } from "react"; import { getRelativeTime } from "@/features/ai/lib/formatting"; import type { Chat } from "@/features/ai/types/ai-chat"; -import { Button } from "@/ui/button"; -import { Dropdown } from "@/ui/dropdown"; -import Input from "@/ui/input"; import { cn } from "@/utils/cn"; import { ProviderIcon } from "../icons/provider-icons"; @@ -18,8 +17,6 @@ interface ChatHistoryDropdownProps { triggerRef: React.RefObject; } -const DROPDOWN_WIDTH = 340; - export default function ChatHistoryDropdown({ isOpen, onClose, @@ -29,116 +26,214 @@ export default function ChatHistoryDropdown({ onDeleteChat, triggerRef, }: ChatHistoryDropdownProps) { - const searchRef = useRef(null); + const inputRef = useRef(null); + const resultsRef = useRef(null); const [searchQuery, setSearchQuery] = useState(""); + const [selectedIndex, setSelectedIndex] = useState(0); const filteredChats = useMemo(() => { - if (!searchQuery.trim()) return chats; - return chats.filter((chat) => chat.title.toLowerCase().includes(searchQuery.toLowerCase())); + const query = searchQuery.trim().toLowerCase(); + if (!query) return chats; + return chats.filter((chat) => { + const titleMatch = chat.title.toLowerCase().includes(query); + const providerMatch = (chat.agentId ?? "custom").toLowerCase().includes(query); + return titleMatch || providerMatch; + }); }, [chats, searchQuery]); + const handleClose = () => { + onClose(); + triggerRef.current?.focus(); + }; + useEffect(() => { - if (isOpen) { - setSearchQuery(""); - setTimeout(() => searchRef.current?.focus(), 0); - } + if (!isOpen) return; + setSearchQuery(""); + setSelectedIndex(0); + window.setTimeout(() => inputRef.current?.focus(), 0); }, [isOpen]); + useEffect(() => { + if (!isOpen) return; + + const handleKeyDown = (event: KeyboardEvent) => { + switch (event.key) { + case "Escape": + event.preventDefault(); + handleClose(); + break; + case "ArrowDown": + event.preventDefault(); + setSelectedIndex((prev) => Math.min(prev + 1, filteredChats.length - 1)); + break; + case "ArrowUp": + event.preventDefault(); + setSelectedIndex((prev) => Math.max(prev - 1, 0)); + break; + case "Enter": + if (filteredChats[selectedIndex]) { + event.preventDefault(); + onSwitchToChat(filteredChats[selectedIndex].id); + handleClose(); + } + break; + } + }; + + document.addEventListener("keydown", handleKeyDown); + return () => document.removeEventListener("keydown", handleKeyDown); + }, [filteredChats, isOpen, onSwitchToChat, selectedIndex]); + + useEffect(() => { + setSelectedIndex(0); + }, [searchQuery]); + + useEffect(() => { + if (!resultsRef.current || filteredChats.length === 0) return; + const selectedElement = resultsRef.current.children[selectedIndex] as HTMLElement | undefined; + selectedElement?.scrollIntoView({ block: "nearest", behavior: "smooth" }); + }, [filteredChats.length, selectedIndex]); + return ( - -
- setSearchQuery(e.target.value)} - leftIcon={Search} - variant="ghost" - className="w-full" - /> -
- -
- {chats.length === 0 ? ( -
No chat history
- ) : filteredChats.length === 0 ? ( -
- No chats match "{searchQuery}" -
- ) : ( -
- {filteredChats.map((chat) => ( -
-
- {chat.id === currentChatId ? ( - - ) : ( - - )} -
- - +
+ +
- {chat.title} - - - {getRelativeTime(chat.lastMessageAt)} - - - - -
- ))} -
- )} -
-
+ {chats.length === 0 ? ( +
+ No chat history yet +
+ ) : filteredChats.length === 0 ? ( +
+ No chats match "{searchQuery}" +
+ ) : ( + filteredChats.map((chat, index) => { + const isCurrent = chat.id === currentChatId; + const isSelected = index === selectedIndex; + + return ( +
{ + onSwitchToChat(chat.id); + handleClose(); + }} + className={cn( + "group relative mb-0.5 flex cursor-pointer items-start gap-3 rounded-xl px-4 py-3 transition-colors", + isSelected ? "bg-hover/80" : "hover:bg-hover/40", + isCurrent && "bg-accent/5 hover:bg-accent/10", + )} + > + {isCurrent && ( +
+ )} + +
+ {isCurrent ? ( + + ) : ( + + )} +
+ +
+
+ + {chat.title} + + + {getRelativeTime(chat.lastMessageAt)} + +
+ +
+ + {(chat.agentId || "custom").replace(/-/g, " ")} + + {isCurrent && ( + <> + + Current chat + + )} +
+
+ + +
+ ); + }) + )} +
+ + + + + + )} + ); } From 53ac772f15298f70182824b70eb5644f081874fc Mon Sep 17 00:00:00 2001 From: Finesssee <90105158+Finesssee@users.noreply.github.com> Date: Fri, 3 Apr 2026 00:10:29 +0700 Subject: [PATCH 2/4] Fix Linux cargo check for app windows --- src-tauri/src/commands/ui/window.rs | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/src-tauri/src/commands/ui/window.rs b/src-tauri/src/commands/ui/window.rs index ccfbf9e2..2bf001b0 100644 --- a/src-tauri/src/commands/ui/window.rs +++ b/src-tauri/src/commands/ui/window.rs @@ -1,8 +1,6 @@ use serde::{Deserialize, Serialize}; use std::sync::atomic::{AtomicU32, Ordering}; -use tauri::{ - AppHandle, Manager, TitleBarStyle, WebviewBuilder, WebviewUrl, WebviewWindow, command, -}; +use tauri::{AppHandle, Manager, WebviewBuilder, WebviewUrl, WebviewWindow, command}; // Counter for generating unique web viewer labels static WEB_VIEWER_COUNTER: AtomicU32 = AtomicU32::new(0); @@ -87,16 +85,22 @@ pub fn create_app_window_internal( ); let url = build_window_open_url(request.as_ref()); - let window = tauri::WebviewWindowBuilder::new(app, &label, WebviewUrl::App(url.into())) - .title("") - .inner_size(1200.0, 800.0) - .min_inner_size(400.0, 400.0) - .center() + let window_builder = + tauri::WebviewWindowBuilder::new(app, &label, WebviewUrl::App(url.into())) + .title("") + .inner_size(1200.0, 800.0) + .min_inner_size(400.0, 400.0) + .center() .decorations(true) .resizable(true) - .shadow(true) + .shadow(true); + + #[cfg(target_os = "macos")] + let window_builder = window_builder .hidden_title(true) - .title_bar_style(TitleBarStyle::Overlay) + .title_bar_style(tauri::TitleBarStyle::Overlay); + + let window = window_builder .build() .map_err(|e| format!("Failed to create app window: {e}"))?; From 78bd89b197c494ef2551bab676dd18e2be33867e Mon Sep 17 00:00:00 2001 From: Finesssee <90105158+Finesssee@users.noreply.github.com> Date: Fri, 3 Apr 2026 00:14:39 +0700 Subject: [PATCH 3/4] Format Linux app window fix --- src-tauri/src/commands/ui/window.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src-tauri/src/commands/ui/window.rs b/src-tauri/src/commands/ui/window.rs index 2bf001b0..5a6e6375 100644 --- a/src-tauri/src/commands/ui/window.rs +++ b/src-tauri/src/commands/ui/window.rs @@ -85,12 +85,11 @@ pub fn create_app_window_internal( ); let url = build_window_open_url(request.as_ref()); - let window_builder = - tauri::WebviewWindowBuilder::new(app, &label, WebviewUrl::App(url.into())) - .title("") - .inner_size(1200.0, 800.0) - .min_inner_size(400.0, 400.0) - .center() + let window_builder = tauri::WebviewWindowBuilder::new(app, &label, WebviewUrl::App(url.into())) + .title("") + .inner_size(1200.0, 800.0) + .min_inner_size(400.0, 400.0) + .center() .decorations(true) .resizable(true) .shadow(true); From 9b023ea2981fe560425e6d6d5df13551d4b84929 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mehmet=20=C3=96zg=C3=BCl?= Date: Mon, 6 Apr 2026 22:52:10 +0300 Subject: [PATCH 4/4] Fix history sidebar: exit animation, stale closure, hover selection Stabilize handleClose with useCallback and add it to the keyboard effect dependency array to prevent stale closure bugs. Move AnimatePresence inside DialogPrimitive.Root with forceMount on Portal so exit animations actually play. Add onMouseEnter to chat items so hover updates selectedIndex and Enter selects the hovered item. --- .../ai/components/history/sidebar.tsx | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/features/ai/components/history/sidebar.tsx b/src/features/ai/components/history/sidebar.tsx index 839a5bb6..e32af09b 100644 --- a/src/features/ai/components/history/sidebar.tsx +++ b/src/features/ai/components/history/sidebar.tsx @@ -1,7 +1,7 @@ import * as DialogPrimitive from "@radix-ui/react-dialog"; import { AnimatePresence, motion } from "framer-motion"; import { Check, Search, Trash2, X } from "lucide-react"; -import { useEffect, useMemo, useRef, useState } from "react"; +import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { getRelativeTime } from "@/features/ai/lib/formatting"; import type { Chat } from "@/features/ai/types/ai-chat"; import { cn } from "@/utils/cn"; @@ -41,10 +41,10 @@ export default function ChatHistoryDropdown({ }); }, [chats, searchQuery]); - const handleClose = () => { + const handleClose = useCallback(() => { onClose(); triggerRef.current?.focus(); - }; + }, [onClose, triggerRef]); useEffect(() => { if (!isOpen) return; @@ -82,7 +82,7 @@ export default function ChatHistoryDropdown({ document.addEventListener("keydown", handleKeyDown); return () => document.removeEventListener("keydown", handleKeyDown); - }, [filteredChats, isOpen, onSwitchToChat, selectedIndex]); + }, [filteredChats, handleClose, isOpen, onSwitchToChat, selectedIndex]); useEffect(() => { setSelectedIndex(0); @@ -95,10 +95,10 @@ export default function ChatHistoryDropdown({ }, [filteredChats.length, selectedIndex]); return ( - - {isOpen && ( - !open && handleClose()}> - + !open && handleClose()}> + + {isOpen && ( +
setSelectedIndex(index)} className={cn( "group relative mb-0.5 flex cursor-pointer items-start gap-3 rounded-xl px-4 py-3 transition-colors", isSelected ? "bg-hover/80" : "hover:bg-hover/40", @@ -232,8 +233,8 @@ export default function ChatHistoryDropdown({
-
- )} -
+ )} + + ); }