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

@@ -0,0 +1 @@
ALTER TABLE `messages` ADD `commit_hash` text;

View File

@@ -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": {}
}
}

View File

@@ -22,6 +22,13 @@
"when": 1745359640409,
"tag": "0002_unique_morlocks",
"breakpoints": true
},
{
"idx": 3,
"version": "6",
"when": 1746209201530,
"tag": "0003_open_bucky",
"breakpoints": true
}
]
}

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