Chat search (#1224)

Based on https://github.com/dyad-sh/dyad/pull/1116
    
<!-- This is an auto-generated description by cubic. -->
---

## Summary by cubic
Adds a fast chat search dialog (Command Palette) to find and jump
between chats. Open via the sidebar button or Ctrl/Cmd+K, with title and
message text search plus inline snippets.

- New Features
  - Command palette using cmdk with keyboard shortcut (Ctrl/Cmd+K).
- Searches within the selected app across chat titles and message
content via a new IPC route (search-chats).
- Debounced queries (150ms) with React Query; results de-duplicated and
sorted by newest.
- Snippet preview with highlighted matches and custom ranking; selecting
a result navigates and closes the dialog.
- Search button added to ChatList; basic e2e tests added (currently
skipped).

- Dependencies
  - Added cmdk@1.1.1.
- Bumped @radix-ui/react-dialog to ^1.1.15 and updated Dialog to support
an optional close button.

<!-- End of auto-generated description by cubic. -->

---------

Co-authored-by: Evans Obeng <iamevansobeng@outlook.com>
Co-authored-by: Evans Obeng <60653146+iamevansobeng@users.noreply.github.com>
This commit is contained in:
Will Chen
2025-09-09 00:18:48 -07:00
committed by GitHub
parent d21497659b
commit 7818f2950a
12 changed files with 655 additions and 12 deletions

View File

@@ -2,7 +2,7 @@ import { useEffect, useState } from "react";
import { useNavigate, useRouterState } from "@tanstack/react-router";
import { formatDistanceToNow } from "date-fns";
import { PlusCircle, MoreVertical, Trash2, Edit3 } from "lucide-react";
import { PlusCircle, MoreVertical, Trash2, Edit3, Search } from "lucide-react";
import { useAtom } from "jotai";
import { selectedChatIdAtom } from "@/atoms/chatAtoms";
import { selectedAppIdAtom } from "@/atoms/appAtoms";
@@ -27,11 +27,14 @@ import { useChats } from "@/hooks/useChats";
import { RenameChatDialog } from "@/components/chat/RenameChatDialog";
import { DeleteChatDialog } from "@/components/chat/DeleteChatDialog";
import { ChatSearchDialog } from "./ChatSearchDialog";
export function ChatList({ show }: { show?: boolean }) {
const navigate = useNavigate();
const [selectedChatId, setSelectedChatId] = useAtom(selectedChatIdAtom);
const [selectedAppId, setSelectedAppId] = useAtom(selectedAppIdAtom);
const [, setIsDropdownOpen] = useAtom(dropdownOpenAtom);
const { chats, loading, refreshChats } = useChats(selectedAppId);
const routerState = useRouterState();
const isChatRoute = routerState.location.pathname === "/chat";
@@ -46,6 +49,9 @@ export function ChatList({ show }: { show?: boolean }) {
const [deleteChatId, setDeleteChatId] = useState<number | null>(null);
const [deleteChatTitle, setDeleteChatTitle] = useState("");
// search dialog state
const [isSearchDialogOpen, setIsSearchDialogOpen] = useState(false);
// Update selectedChatId when route changes
useEffect(() => {
if (isChatRoute) {
@@ -70,6 +76,7 @@ export function ChatList({ show }: { show?: boolean }) {
}) => {
setSelectedChatId(chatId);
setSelectedAppId(appId);
setIsSearchDialogOpen(false);
navigate({
to: "/chat",
search: { id: chatId },
@@ -151,7 +158,10 @@ export function ChatList({ show }: { show?: boolean }) {
return (
<>
<SidebarGroup className="overflow-y-auto h-[calc(100vh-112px)]">
<SidebarGroup
className="overflow-y-auto h-[calc(100vh-112px)]"
data-testid="chat-list-container"
>
<SidebarGroupLabel>Recent Chats</SidebarGroupLabel>
<SidebarGroupContent>
<div className="flex flex-col space-y-4">
@@ -163,6 +173,15 @@ export function ChatList({ show }: { show?: boolean }) {
<PlusCircle size={16} />
<span>New Chat</span>
</Button>
<Button
onClick={() => setIsSearchDialogOpen(!isSearchDialogOpen)}
variant="outline"
className="flex items-center justify-start gap-2 mx-2 py-3"
data-testid="search-chats-button"
>
<Search size={16} />
<span>Search chats</span>
</Button>
{loading ? (
<div className="py-3 px-4 text-sm text-gray-500">
@@ -273,6 +292,15 @@ export function ChatList({ show }: { show?: boolean }) {
onConfirmDelete={handleConfirmDelete}
chatTitle={deleteChatTitle}
/>
{/* Chat Search Dialog */}
<ChatSearchDialog
open={isSearchDialogOpen}
onOpenChange={setIsSearchDialogOpen}
onSelectChat={handleChatClick}
appId={selectedAppId}
allChats={chats}
/>
</>
);
}