Undo chat history (#74)
This commit is contained in:
1
drizzle/0003_open_bucky.sql
Normal file
1
drizzle/0003_open_bucky.sql
Normal file
@@ -0,0 +1 @@
|
||||
ALTER TABLE `messages` ADD `commit_hash` text;
|
||||
213
drizzle/meta/0003_snapshot.json
Normal file
213
drizzle/meta/0003_snapshot.json
Normal 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": {}
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,13 @@
|
||||
"when": 1745359640409,
|
||||
"tag": "0002_unique_morlocks",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 3,
|
||||
"version": "6",
|
||||
"when": 1746209201530,
|
||||
"tag": "0003_open_bucky",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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())`),
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user