Undo chat history (#74)

This commit is contained in:
Will Chen
2025-05-02 13:47:59 -07:00
committed by GitHub
parent 4fb4e49c5d
commit 3529627172
11 changed files with 309 additions and 17 deletions

View File

@@ -31,6 +31,7 @@ export function ChatList({ show }: { show?: boolean }) {
if (isChatRoute) {
const id = routerState.location.search.id;
if (id) {
console.log("Setting selected chat id to", id);
setSelectedChatId(id);
}
}

View File

@@ -2,7 +2,7 @@ import { PanelRightOpen, History, PlusCircle } from "lucide-react";
import { PanelRightClose } from "lucide-react";
import { useAtomValue, useSetAtom } from "jotai";
import { selectedAppIdAtom } from "@/atoms/appAtoms";
import { useLoadVersions } from "@/hooks/useLoadVersions";
import { useVersions } from "@/hooks/useVersions";
import { Button } from "../ui/button";
import { IpcClient } from "@/ipc/ipc_client";
import { useRouter } from "@tanstack/react-router";
@@ -22,7 +22,7 @@ export function ChatHeader({
onVersionClick,
}: ChatHeaderProps) {
const appId = useAtomValue(selectedAppIdAtom);
const { versions, loading } = useLoadVersions(appId);
const { versions, loading } = useVersions(appId);
const { navigate } = useRouter();
const setSelectedChatId = useSetAtom(selectedChatIdAtom);
const { refreshChats } = useChats(appId);

View File

@@ -1,6 +1,6 @@
import { useAtom, useAtomValue } from "jotai";
import { selectedAppIdAtom, selectedVersionIdAtom } from "@/atoms/appAtoms";
import { useLoadVersions } from "@/hooks/useLoadVersions";
import { useVersions } from "@/hooks/useVersions";
import { formatDistanceToNow } from "date-fns";
import { RotateCcw, X } from "lucide-react";
import type { Version } from "@/ipc/ipc_types";
@@ -15,7 +15,8 @@ interface VersionPaneProps {
export function VersionPane({ isVisible, onClose }: VersionPaneProps) {
const appId = useAtomValue(selectedAppIdAtom);
const { versions, loading, refreshVersions } = useLoadVersions(appId);
const { versions, loading, refreshVersions, revertVersion } =
useVersions(appId);
const [selectedVersionId, setSelectedVersionId] = useAtom(
selectedVersionIdAtom
);
@@ -108,11 +109,9 @@ export function VersionPane({ isVisible, onClose }: VersionPaneProps) {
onClick={async (e) => {
e.stopPropagation();
setSelectedVersionId(null);
await IpcClient.getInstance().revertVersion({
appId: appId!,
previousVersionId: version.oid,
await revertVersion({
versionId: version.oid,
});
refreshVersions();
}}
className={cn(
"invisible mt-1 flex items-center gap-1 px-2 py-0.5 text-sm font-medium bg-(--primary) text-(--primary-foreground) hover:bg-background-lightest rounded-md transition-colors",

View File

@@ -38,6 +38,7 @@ export const messages = sqliteTable("messages", {
approvalState: text("approval_state", {
enum: ["approved", "rejected"],
}),
commitHash: text("commit_hash"),
createdAt: integer("created_at", { mode: "timestamp" })
.notNull()
.default(sql`(unixepoch())`),

View File

@@ -13,7 +13,7 @@ import type { ChatResponseEnd } from "@/ipc/ipc_types";
import { useChats } from "./useChats";
import { useLoadApp } from "./useLoadApp";
import { selectedAppIdAtom } from "@/atoms/appAtoms";
import { useLoadVersions } from "./useLoadVersions";
import { useVersions } from "./useVersions";
import { showError } from "@/lib/toast";
import { useProposal } from "./useProposal";
import { useSearch } from "@tanstack/react-router";
@@ -35,7 +35,7 @@ export function useStreamChat({
const { refreshChats } = useChats(selectedAppId);
const { refreshApp } = useLoadApp(selectedAppId);
const setStreamCount = useSetAtom(chatStreamCountAtom);
const { refreshVersions } = useLoadVersions(selectedAppId);
const { refreshVersions } = useVersions(selectedAppId);
const { refreshAppIframe } = useRunApp();
const { countTokens } = useCountTokens();

View File

@@ -1,13 +1,16 @@
import { useState, useEffect, useCallback } from "react";
import { useAtom } from "jotai";
import { useAtom, useAtomValue } from "jotai";
import { versionsListAtom } from "@/atoms/appAtoms";
import { IpcClient } from "@/ipc/ipc_client";
import { showError } from "@/lib/toast";
import { chatMessagesAtom, selectedChatIdAtom } from "@/atoms/chatAtoms";
export function useLoadVersions(appId: number | null) {
export function useVersions(appId: number | null) {
const [versions, setVersions] = useAtom(versionsListAtom);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const selectedChatId = useAtomValue(selectedChatIdAtom);
const [messages, setMessages] = useAtom(chatMessagesAtom);
useEffect(() => {
const loadVersions = async () => {
// If no app is selected, clear versions and return
@@ -50,5 +53,26 @@ export function useLoadVersions(appId: number | null) {
}
}, [appId, setVersions, setError]);
return { versions, loading, error, refreshVersions };
const revertVersion = useCallback(
async ({ versionId }: { versionId: string }) => {
if (appId === null) {
return;
}
try {
const ipcClient = IpcClient.getInstance();
await ipcClient.revertVersion({ appId, previousVersionId: versionId });
await refreshVersions();
if (selectedChatId) {
const chat = await IpcClient.getInstance().getChat(selectedChatId);
setMessages(chat.messages);
}
} catch (error) {
showError(error);
}
},
[appId, setVersions, setError, selectedChatId, setMessages]
);
return { versions, loading, error, refreshVersions, revertVersion };
}

View File

@@ -1,7 +1,7 @@
import { ipcMain } from "electron";
import { db, getDatabasePath } from "../../db";
import { apps, chats } from "../../db/schema";
import { desc, eq } from "drizzle-orm";
import { apps, chats, messages } from "../../db/schema";
import { desc, eq, and, gte, sql, gt } from "drizzle-orm";
import type {
App,
CreateAppParams,
@@ -572,6 +572,44 @@ export function registerAppHandlers() {
author: await getGitAuthor(),
});
// Find the chat and message associated with the commit hash
const messageWithCommit = await db.query.messages.findFirst({
where: eq(messages.commitHash, previousVersionId),
with: {
chat: true,
},
});
// If we found a message with this commit hash, delete all subsequent messages (but keep this message)
if (messageWithCommit) {
const chatId = messageWithCommit.chatId;
// Find all messages in this chat with IDs > the one with our commit hash
const messagesToDelete = await db.query.messages.findMany({
where: and(
eq(messages.chatId, chatId),
gt(messages.id, messageWithCommit.id)
),
orderBy: desc(messages.id),
});
logger.log(
`Deleting ${messagesToDelete.length} messages after commit ${previousVersionId} from chat ${chatId}`
);
// Delete the messages
if (messagesToDelete.length > 0) {
await db
.delete(messages)
.where(
and(
eq(messages.chatId, chatId),
gt(messages.id, messageWithCommit.id)
)
);
}
}
return { success: true };
} catch (error: any) {
logger.error(

View File

@@ -435,7 +435,7 @@ export async function processFullResponseActions(
changes.push(`executed ${dyadExecuteSqlQueries.length} SQL queries`);
// Use chat summary, if provided, or default for commit message
await git.commit({
const commitHash = await git.commit({
fs,
dir: appPath,
message: chatSummary
@@ -444,6 +444,14 @@ export async function processFullResponseActions(
author: await getGitAuthor(),
});
logger.log(`Successfully committed changes: ${changes.join(", ")}`);
// Save the commit hash to the message
await db
.update(messages)
.set({
commitHash: commitHash,
})
.where(eq(messages.id, messageId));
}
logger.log("mark as approved: hasChanges", hasChanges);
// Update the message to approved