feat(21-06): add conversation search input and Cmd+K shortcut

- Add Search/X icons and Input to ChatConversationList with 300ms debounce
- Listen for nexus:focus-chat-search event to focus search input
- Add onSearch handler to useKeyboardShortcuts (fires before input guard)
- Wire Layout Cmd+K to open chat panel and dispatch focus event
This commit is contained in:
Nexus Dev 2026-04-01 17:14:24 +00:00
parent 6367789992
commit 0406362e04
3 changed files with 58 additions and 5 deletions

View file

@ -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<HTMLInputElement>(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<string | null>(null);
const sentinelRef = useRef<HTMLDivElement>(null);
@ -112,6 +130,29 @@ export function ChatConversationList({ companyId }: ChatConversationListProps) {
</Button>
</div>
{/* Search input */}
<div className="px-2 pb-1 pt-1">
<div className="relative">
<Search className="absolute left-2 top-1/2 -translate-y-1/2 h-3.5 w-3.5 text-muted-foreground" />
<Input
ref={searchInputRef}
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="Search conversations..."
className="h-7 pl-7 pr-7 text-xs"
/>
{searchTerm && (
<button
type="button"
onClick={() => setSearchTerm("")}
className="absolute right-2 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
>
<X className="h-3 w-3" />
</button>
)}
</div>
</div>
<ScrollArea className="flex-1">
<div className="p-1 space-y-0.5">
{isLoading ? (

View file

@ -52,7 +52,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,
@ -160,6 +160,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(() => {

View file

@ -4,11 +4,19 @@ interface ShortcutHandlers {
onNewIssue?: () => void;
onToggleSidebar?: () => void;
onTogglePanel?: () => void;
onSearch?: () => void;
}
export function useKeyboardShortcuts({ onNewIssue, onToggleSidebar, onTogglePanel }: ShortcutHandlers) {
export function useKeyboardShortcuts({ onNewIssue, onToggleSidebar, onTogglePanel, onSearch }: ShortcutHandlers) {
useEffect(() => {
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
const target = e.target as HTMLElement;
if (target.tagName === "INPUT" || target.tagName === "TEXTAREA" || target.isContentEditable) {
@ -36,5 +44,5 @@ export function useKeyboardShortcuts({ onNewIssue, onToggleSidebar, onTogglePane
document.addEventListener("keydown", handleKeyDown);
return () => document.removeEventListener("keydown", handleKeyDown);
}, [onNewIssue, onToggleSidebar, onTogglePanel]);
}, [onNewIssue, onToggleSidebar, onTogglePanel, onSearch]);
}