From 35296271727eb4699df1a933bfb9c3691d12e249 Mon Sep 17 00:00:00 2001 From: Will Chen Date: Fri, 2 May 2025 13:47:59 -0700 Subject: [PATCH] Undo chat history (#74) --- drizzle/0003_open_bucky.sql | 1 + drizzle/meta/0003_snapshot.json | 213 ++++++++++++++++++ drizzle/meta/_journal.json | 7 + src/components/ChatList.tsx | 1 + src/components/chat/ChatHeader.tsx | 4 +- src/components/chat/VersionPane.tsx | 11 +- src/db/schema.ts | 1 + src/hooks/useStreamChat.ts | 4 +- .../{useLoadVersions.ts => useVersions.ts} | 32 ++- src/ipc/handlers/app_handlers.ts | 42 +++- src/ipc/processors/response_processor.ts | 10 +- 11 files changed, 309 insertions(+), 17 deletions(-) create mode 100644 drizzle/0003_open_bucky.sql create mode 100644 drizzle/meta/0003_snapshot.json rename src/hooks/{useLoadVersions.ts => useVersions.ts} (59%) diff --git a/drizzle/0003_open_bucky.sql b/drizzle/0003_open_bucky.sql new file mode 100644 index 0000000..b4d741e --- /dev/null +++ b/drizzle/0003_open_bucky.sql @@ -0,0 +1 @@ +ALTER TABLE `messages` ADD `commit_hash` text; \ No newline at end of file diff --git a/drizzle/meta/0003_snapshot.json b/drizzle/meta/0003_snapshot.json new file mode 100644 index 0000000..189f647 --- /dev/null +++ b/drizzle/meta/0003_snapshot.json @@ -0,0 +1,213 @@ +{ + "version": "6", + "dialect": "sqlite", + "id": "859942b1-88b8-4a16-b2d0-77c9ece76693", + "prevId": "e1d700a4-d507-4e2a-80dc-8dbbfd91edfd", + "tables": { + "apps": { + "name": "apps", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "updated_at": { + "name": "updated_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + }, + "github_org": { + "name": "github_org", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "github_repo": { + "name": "github_repo", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "supabase_project_id": { + "name": "supabase_project_id", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "chats": { + "name": "chats", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "app_id": { + "name": "app_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": { + "chats_app_id_apps_id_fk": { + "name": "chats_app_id_apps_id_fk", + "tableFrom": "chats", + "tableTo": "apps", + "columnsFrom": [ + "app_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + }, + "messages": { + "name": "messages", + "columns": { + "id": { + "name": "id", + "type": "integer", + "primaryKey": true, + "notNull": true, + "autoincrement": true + }, + "chat_id": { + "name": "chat_id", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "autoincrement": false + }, + "approval_state": { + "name": "approval_state", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "commit_hash": { + "name": "commit_hash", + "type": "text", + "primaryKey": false, + "notNull": false, + "autoincrement": false + }, + "created_at": { + "name": "created_at", + "type": "integer", + "primaryKey": false, + "notNull": true, + "autoincrement": false, + "default": "(unixepoch())" + } + }, + "indexes": {}, + "foreignKeys": { + "messages_chat_id_chats_id_fk": { + "name": "messages_chat_id_chats_id_fk", + "tableFrom": "messages", + "tableTo": "chats", + "columnsFrom": [ + "chat_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "checkConstraints": {} + } + }, + "views": {}, + "enums": {}, + "_meta": { + "schemas": {}, + "tables": {}, + "columns": {} + }, + "internal": { + "indexes": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 08a449a..d812cc9 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -22,6 +22,13 @@ "when": 1745359640409, "tag": "0002_unique_morlocks", "breakpoints": true + }, + { + "idx": 3, + "version": "6", + "when": 1746209201530, + "tag": "0003_open_bucky", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/components/ChatList.tsx b/src/components/ChatList.tsx index f7220d1..2766ea7 100644 --- a/src/components/ChatList.tsx +++ b/src/components/ChatList.tsx @@ -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); } } diff --git a/src/components/chat/ChatHeader.tsx b/src/components/chat/ChatHeader.tsx index 974a5ff..665738f 100644 --- a/src/components/chat/ChatHeader.tsx +++ b/src/components/chat/ChatHeader.tsx @@ -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); diff --git a/src/components/chat/VersionPane.tsx b/src/components/chat/VersionPane.tsx index 87ece9d..54554b1 100644 --- a/src/components/chat/VersionPane.tsx +++ b/src/components/chat/VersionPane.tsx @@ -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", diff --git a/src/db/schema.ts b/src/db/schema.ts index 400c02d..f4e35db 100644 --- a/src/db/schema.ts +++ b/src/db/schema.ts @@ -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())`), diff --git a/src/hooks/useStreamChat.ts b/src/hooks/useStreamChat.ts index e705b89..abf7156 100644 --- a/src/hooks/useStreamChat.ts +++ b/src/hooks/useStreamChat.ts @@ -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(); diff --git a/src/hooks/useLoadVersions.ts b/src/hooks/useVersions.ts similarity index 59% rename from src/hooks/useLoadVersions.ts rename to src/hooks/useVersions.ts index 3fae2b8..55cf9cc 100644 --- a/src/hooks/useLoadVersions.ts +++ b/src/hooks/useVersions.ts @@ -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(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 }; } diff --git a/src/ipc/handlers/app_handlers.ts b/src/ipc/handlers/app_handlers.ts index 25abe1e..5249795 100644 --- a/src/ipc/handlers/app_handlers.ts +++ b/src/ipc/handlers/app_handlers.ts @@ -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( diff --git a/src/ipc/processors/response_processor.ts b/src/ipc/processors/response_processor.ts index a151eb8..8489158 100644 --- a/src/ipc/processors/response_processor.ts +++ b/src/ipc/processors/response_processor.ts @@ -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