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:
@@ -1,8 +1,8 @@
|
||||
import { ipcMain } from "electron";
|
||||
import { db } from "../../db";
|
||||
import { apps, chats, messages } from "../../db/schema";
|
||||
import { desc, eq } from "drizzle-orm";
|
||||
import type { ChatSummary } from "../../lib/schemas";
|
||||
import { desc, eq, and, like } from "drizzle-orm";
|
||||
import type { ChatSearchResult, ChatSummary } from "../../lib/schemas";
|
||||
import * as git from "isomorphic-git";
|
||||
import * as fs from "fs";
|
||||
import { createLoggedHandler } from "./safe_handle";
|
||||
@@ -115,4 +115,61 @@ export function registerChatHandlers() {
|
||||
handle("delete-messages", async (_, chatId: number): Promise<void> => {
|
||||
await db.delete(messages).where(eq(messages.chatId, chatId));
|
||||
});
|
||||
|
||||
handle(
|
||||
"search-chats",
|
||||
async (_, appId: number, query: string): Promise<ChatSearchResult[]> => {
|
||||
// 1) Find chats by title and map to ChatSearchResult with no matched message
|
||||
const chatTitleMatches = await db
|
||||
.select({
|
||||
id: chats.id,
|
||||
appId: chats.appId,
|
||||
title: chats.title,
|
||||
createdAt: chats.createdAt,
|
||||
})
|
||||
.from(chats)
|
||||
.where(and(eq(chats.appId, appId), like(chats.title, `%${query}%`)))
|
||||
.orderBy(desc(chats.createdAt))
|
||||
.limit(10);
|
||||
|
||||
const titleResults: ChatSearchResult[] = chatTitleMatches.map((c) => ({
|
||||
id: c.id,
|
||||
appId: c.appId,
|
||||
title: c.title,
|
||||
createdAt: c.createdAt,
|
||||
matchedMessageContent: null,
|
||||
}));
|
||||
|
||||
// 2) Find messages that match and join to chats to build one result per message
|
||||
const messageResults = await db
|
||||
.select({
|
||||
id: chats.id,
|
||||
appId: chats.appId,
|
||||
title: chats.title,
|
||||
createdAt: chats.createdAt,
|
||||
matchedMessageContent: messages.content,
|
||||
})
|
||||
.from(messages)
|
||||
.innerJoin(chats, eq(messages.chatId, chats.id))
|
||||
.where(
|
||||
and(eq(chats.appId, appId), like(messages.content, `%${query}%`)),
|
||||
)
|
||||
.orderBy(desc(chats.createdAt))
|
||||
.limit(10);
|
||||
|
||||
// Combine: keep title matches and per-message matches
|
||||
const combined: ChatSearchResult[] = [...titleResults, ...messageResults];
|
||||
const uniqueChats = Array.from(
|
||||
new Map(combined.map((item) => [item.id, item])).values(),
|
||||
);
|
||||
|
||||
// Sort newest chats first
|
||||
uniqueChats.sort(
|
||||
(a, b) =>
|
||||
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
|
||||
);
|
||||
|
||||
return uniqueChats;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
ChatSummariesSchema,
|
||||
type UserSettings,
|
||||
type ContextPathResults,
|
||||
ChatSearchResultsSchema,
|
||||
} from "../lib/schemas";
|
||||
import type {
|
||||
AppOutput,
|
||||
@@ -63,7 +64,11 @@ import type {
|
||||
UpdatePromptParamsDto,
|
||||
} from "./ipc_types";
|
||||
import type { Template } from "../shared/templates";
|
||||
import type { AppChatContext, ProposalResult } from "@/lib/schemas";
|
||||
import type {
|
||||
AppChatContext,
|
||||
ChatSearchResult,
|
||||
ProposalResult,
|
||||
} from "@/lib/schemas";
|
||||
import { showError } from "@/lib/toast";
|
||||
|
||||
export interface ChatStreamCallbacks {
|
||||
@@ -288,6 +293,20 @@ export class IpcClient {
|
||||
}
|
||||
}
|
||||
|
||||
// search for chats
|
||||
public async searchChats(
|
||||
appId: number,
|
||||
query: string,
|
||||
): Promise<ChatSearchResult[]> {
|
||||
try {
|
||||
const data = await this.ipcRenderer.invoke("search-chats", appId, query);
|
||||
return ChatSearchResultsSchema.parse(data);
|
||||
} catch (error) {
|
||||
showError(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// Get all apps
|
||||
public async listApps(): Promise<ListAppsResponse> {
|
||||
return this.ipcRenderer.invoke("list-apps");
|
||||
|
||||
Reference in New Issue
Block a user