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>
176 lines
4.8 KiB
TypeScript
176 lines
4.8 KiB
TypeScript
import { ipcMain } from "electron";
|
|
import { db } from "../../db";
|
|
import { apps, chats, messages } from "../../db/schema";
|
|
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";
|
|
|
|
import log from "electron-log";
|
|
import { getDyadAppPath } from "../../paths/paths";
|
|
import { UpdateChatParams } from "../ipc_types";
|
|
|
|
const logger = log.scope("chat_handlers");
|
|
const handle = createLoggedHandler(logger);
|
|
|
|
export function registerChatHandlers() {
|
|
handle("create-chat", async (_, appId: number): Promise<number> => {
|
|
// Get the app's path first
|
|
const app = await db.query.apps.findFirst({
|
|
where: eq(apps.id, appId),
|
|
columns: {
|
|
path: true,
|
|
},
|
|
});
|
|
|
|
if (!app) {
|
|
throw new Error("App not found");
|
|
}
|
|
|
|
let initialCommitHash = null;
|
|
try {
|
|
// Get the current git revision of main branch
|
|
initialCommitHash = await git.resolveRef({
|
|
fs,
|
|
dir: getDyadAppPath(app.path),
|
|
ref: "main",
|
|
});
|
|
} catch (error) {
|
|
logger.error("Error getting git revision:", error);
|
|
// Continue without the git revision
|
|
}
|
|
|
|
// Create a new chat
|
|
const [chat] = await db
|
|
.insert(chats)
|
|
.values({
|
|
appId,
|
|
initialCommitHash,
|
|
})
|
|
.returning();
|
|
logger.info(
|
|
"Created chat:",
|
|
chat.id,
|
|
"for app:",
|
|
appId,
|
|
"with initial commit hash:",
|
|
initialCommitHash,
|
|
);
|
|
return chat.id;
|
|
});
|
|
|
|
ipcMain.handle("get-chat", async (_, chatId: number) => {
|
|
const chat = await db.query.chats.findFirst({
|
|
where: eq(chats.id, chatId),
|
|
with: {
|
|
messages: {
|
|
orderBy: (messages, { asc }) => [asc(messages.createdAt)],
|
|
},
|
|
},
|
|
});
|
|
|
|
if (!chat) {
|
|
throw new Error("Chat not found");
|
|
}
|
|
|
|
return chat;
|
|
});
|
|
|
|
handle("get-chats", async (_, appId?: number): Promise<ChatSummary[]> => {
|
|
// If appId is provided, filter chats for that app
|
|
const query = appId
|
|
? db.query.chats.findMany({
|
|
where: eq(chats.appId, appId),
|
|
columns: {
|
|
id: true,
|
|
title: true,
|
|
createdAt: true,
|
|
appId: true,
|
|
},
|
|
orderBy: [desc(chats.createdAt)],
|
|
})
|
|
: db.query.chats.findMany({
|
|
columns: {
|
|
id: true,
|
|
title: true,
|
|
createdAt: true,
|
|
appId: true,
|
|
},
|
|
orderBy: [desc(chats.createdAt)],
|
|
});
|
|
|
|
const allChats = await query;
|
|
return allChats;
|
|
});
|
|
|
|
handle("delete-chat", async (_, chatId: number): Promise<void> => {
|
|
await db.delete(chats).where(eq(chats.id, chatId));
|
|
});
|
|
|
|
handle("update-chat", async (_, { chatId, title }: UpdateChatParams) => {
|
|
await db.update(chats).set({ title }).where(eq(chats.id, chatId));
|
|
});
|
|
|
|
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;
|
|
},
|
|
);
|
|
}
|