diff --git a/ui/src/components/ChatConversationList.tsx b/ui/src/components/ChatConversationList.tsx index 13011e01..a6e1ae51 100644 --- a/ui/src/components/ChatConversationList.tsx +++ b/ui/src/components/ChatConversationList.tsx @@ -1,11 +1,12 @@ import { useEffect, useRef, useState } from "react"; -import { Plus } from "lucide-react"; +import { Plus, Search, X } from "lucide-react"; import { useChatConversations } from "../hooks/useChatConversations"; import { useChatPanel } from "../context/ChatPanelContext"; import { ChatConversationItem } from "./ChatConversationItem"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Skeleton } from "@/components/ui/skeleton"; import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; import { Dialog, DialogContent, @@ -22,8 +23,25 @@ interface ChatConversationListProps { export function ChatConversationList({ companyId }: ChatConversationListProps) { const { activeConversationId, setActiveConversationId } = useChatPanel(); + + const [searchTerm, setSearchTerm] = useState(""); + const [debouncedSearch, setDebouncedSearch] = useState(""); + const searchInputRef = useRef(null); + + useEffect(() => { + const timer = setTimeout(() => setDebouncedSearch(searchTerm), 300); + return () => clearTimeout(timer); + }, [searchTerm]); + + // Listen for focus-chat-search custom event (dispatched by Cmd+K in Layout) + useEffect(() => { + const handler = () => searchInputRef.current?.focus(); + window.addEventListener("nexus:focus-chat-search", handler); + return () => window.removeEventListener("nexus:focus-chat-search", handler); + }, []); + const { data, isLoading, hasNextPage, fetchNextPage, createMutation, updateMutation, deleteMutation } = - useChatConversations(companyId); + useChatConversations(companyId, { search: debouncedSearch || undefined }); const [deletingId, setDeletingId] = useState(null); const sentinelRef = useRef(null); @@ -112,6 +130,29 @@ export function ChatConversationList({ companyId }: ChatConversationListProps) { + {/* Search input */} +
+
+ + setSearchTerm(e.target.value)} + placeholder="Search conversations..." + className="h-7 pl-7 pr-7 text-xs" + /> + {searchTerm && ( + + )} +
+
+
{isLoading ? ( diff --git a/ui/src/components/Layout.tsx b/ui/src/components/Layout.tsx index 10057054..28773668 100644 --- a/ui/src/components/Layout.tsx +++ b/ui/src/components/Layout.tsx @@ -54,7 +54,7 @@ export function Layout() { const { sidebarOpen, setSidebarOpen, toggleSidebar, isMobile } = useSidebar(); const { openNewIssue, openOnboarding } = useDialog(); const { togglePanelVisible, setPanelVisible } = usePanel(); - const { chatOpen, toggleChat } = useChatPanel(); + const { chatOpen, setChatOpen, toggleChat } = useChatPanel(); const { companies, loading: companiesLoading, @@ -167,6 +167,10 @@ export function Layout() { onNewIssue: () => openNewIssue(), onToggleSidebar: toggleSidebar, onTogglePanel: togglePanel, + onSearch: () => { + if (!chatOpen) setChatOpen(true); + requestAnimationFrame(() => window.dispatchEvent(new Event("nexus:focus-chat-search"))); + }, }); useEffect(() => { diff --git a/ui/src/hooks/useKeyboardShortcuts.ts b/ui/src/hooks/useKeyboardShortcuts.ts index 6f5b33c4..417a39f8 100644 --- a/ui/src/hooks/useKeyboardShortcuts.ts +++ b/ui/src/hooks/useKeyboardShortcuts.ts @@ -6,6 +6,7 @@ interface ShortcutHandlers { onNewIssue?: () => void; onToggleSidebar?: () => void; onTogglePanel?: () => void; + onSearch?: () => void; } export function useKeyboardShortcuts({ @@ -18,6 +19,13 @@ export function useKeyboardShortcuts({ if (!enabled) return; function handleKeyDown(e: KeyboardEvent) { + // Cmd+K / Ctrl+K → Search (global, works even from inputs) + if (e.key === "k" && (e.metaKey || e.ctrlKey)) { + e.preventDefault(); + onSearch?.(); + return; + } + // Don't fire shortcuts when typing in inputs if (isKeyboardShortcutTextInputTarget(e.target)) { return;