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,
|
"when": 1745359640409,
|
||||||
"tag": "0002_unique_morlocks",
|
"tag": "0002_unique_morlocks",
|
||||||
"breakpoints": true
|
"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) {
|
if (isChatRoute) {
|
||||||
const id = routerState.location.search.id;
|
const id = routerState.location.search.id;
|
||||||
if (id) {
|
if (id) {
|
||||||
|
console.log("Setting selected chat id to", id);
|
||||||
setSelectedChatId(id);
|
setSelectedChatId(id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { PanelRightOpen, History, PlusCircle } from "lucide-react";
|
|||||||
import { PanelRightClose } from "lucide-react";
|
import { PanelRightClose } from "lucide-react";
|
||||||
import { useAtomValue, useSetAtom } from "jotai";
|
import { useAtomValue, useSetAtom } from "jotai";
|
||||||
import { selectedAppIdAtom } from "@/atoms/appAtoms";
|
import { selectedAppIdAtom } from "@/atoms/appAtoms";
|
||||||
import { useLoadVersions } from "@/hooks/useLoadVersions";
|
import { useVersions } from "@/hooks/useVersions";
|
||||||
import { Button } from "../ui/button";
|
import { Button } from "../ui/button";
|
||||||
import { IpcClient } from "@/ipc/ipc_client";
|
import { IpcClient } from "@/ipc/ipc_client";
|
||||||
import { useRouter } from "@tanstack/react-router";
|
import { useRouter } from "@tanstack/react-router";
|
||||||
@@ -22,7 +22,7 @@ export function ChatHeader({
|
|||||||
onVersionClick,
|
onVersionClick,
|
||||||
}: ChatHeaderProps) {
|
}: ChatHeaderProps) {
|
||||||
const appId = useAtomValue(selectedAppIdAtom);
|
const appId = useAtomValue(selectedAppIdAtom);
|
||||||
const { versions, loading } = useLoadVersions(appId);
|
const { versions, loading } = useVersions(appId);
|
||||||
const { navigate } = useRouter();
|
const { navigate } = useRouter();
|
||||||
const setSelectedChatId = useSetAtom(selectedChatIdAtom);
|
const setSelectedChatId = useSetAtom(selectedChatIdAtom);
|
||||||
const { refreshChats } = useChats(appId);
|
const { refreshChats } = useChats(appId);
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useAtom, useAtomValue } from "jotai";
|
import { useAtom, useAtomValue } from "jotai";
|
||||||
import { selectedAppIdAtom, selectedVersionIdAtom } from "@/atoms/appAtoms";
|
import { selectedAppIdAtom, selectedVersionIdAtom } from "@/atoms/appAtoms";
|
||||||
import { useLoadVersions } from "@/hooks/useLoadVersions";
|
import { useVersions } from "@/hooks/useVersions";
|
||||||
import { formatDistanceToNow } from "date-fns";
|
import { formatDistanceToNow } from "date-fns";
|
||||||
import { RotateCcw, X } from "lucide-react";
|
import { RotateCcw, X } from "lucide-react";
|
||||||
import type { Version } from "@/ipc/ipc_types";
|
import type { Version } from "@/ipc/ipc_types";
|
||||||
@@ -15,7 +15,8 @@ interface VersionPaneProps {
|
|||||||
|
|
||||||
export function VersionPane({ isVisible, onClose }: VersionPaneProps) {
|
export function VersionPane({ isVisible, onClose }: VersionPaneProps) {
|
||||||
const appId = useAtomValue(selectedAppIdAtom);
|
const appId = useAtomValue(selectedAppIdAtom);
|
||||||
const { versions, loading, refreshVersions } = useLoadVersions(appId);
|
const { versions, loading, refreshVersions, revertVersion } =
|
||||||
|
useVersions(appId);
|
||||||
const [selectedVersionId, setSelectedVersionId] = useAtom(
|
const [selectedVersionId, setSelectedVersionId] = useAtom(
|
||||||
selectedVersionIdAtom
|
selectedVersionIdAtom
|
||||||
);
|
);
|
||||||
@@ -108,11 +109,9 @@ export function VersionPane({ isVisible, onClose }: VersionPaneProps) {
|
|||||||
onClick={async (e) => {
|
onClick={async (e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
setSelectedVersionId(null);
|
setSelectedVersionId(null);
|
||||||
await IpcClient.getInstance().revertVersion({
|
await revertVersion({
|
||||||
appId: appId!,
|
versionId: version.oid,
|
||||||
previousVersionId: version.oid,
|
|
||||||
});
|
});
|
||||||
refreshVersions();
|
|
||||||
}}
|
}}
|
||||||
className={cn(
|
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",
|
"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", {
|
approvalState: text("approval_state", {
|
||||||
enum: ["approved", "rejected"],
|
enum: ["approved", "rejected"],
|
||||||
}),
|
}),
|
||||||
|
commitHash: text("commit_hash"),
|
||||||
createdAt: integer("created_at", { mode: "timestamp" })
|
createdAt: integer("created_at", { mode: "timestamp" })
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(sql`(unixepoch())`),
|
.default(sql`(unixepoch())`),
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import type { ChatResponseEnd } from "@/ipc/ipc_types";
|
|||||||
import { useChats } from "./useChats";
|
import { useChats } from "./useChats";
|
||||||
import { useLoadApp } from "./useLoadApp";
|
import { useLoadApp } from "./useLoadApp";
|
||||||
import { selectedAppIdAtom } from "@/atoms/appAtoms";
|
import { selectedAppIdAtom } from "@/atoms/appAtoms";
|
||||||
import { useLoadVersions } from "./useLoadVersions";
|
import { useVersions } from "./useVersions";
|
||||||
import { showError } from "@/lib/toast";
|
import { showError } from "@/lib/toast";
|
||||||
import { useProposal } from "./useProposal";
|
import { useProposal } from "./useProposal";
|
||||||
import { useSearch } from "@tanstack/react-router";
|
import { useSearch } from "@tanstack/react-router";
|
||||||
@@ -35,7 +35,7 @@ export function useStreamChat({
|
|||||||
const { refreshChats } = useChats(selectedAppId);
|
const { refreshChats } = useChats(selectedAppId);
|
||||||
const { refreshApp } = useLoadApp(selectedAppId);
|
const { refreshApp } = useLoadApp(selectedAppId);
|
||||||
const setStreamCount = useSetAtom(chatStreamCountAtom);
|
const setStreamCount = useSetAtom(chatStreamCountAtom);
|
||||||
const { refreshVersions } = useLoadVersions(selectedAppId);
|
const { refreshVersions } = useVersions(selectedAppId);
|
||||||
const { refreshAppIframe } = useRunApp();
|
const { refreshAppIframe } = useRunApp();
|
||||||
const { countTokens } = useCountTokens();
|
const { countTokens } = useCountTokens();
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,16 @@
|
|||||||
import { useState, useEffect, useCallback } from "react";
|
import { useState, useEffect, useCallback } from "react";
|
||||||
import { useAtom } from "jotai";
|
import { useAtom, useAtomValue } from "jotai";
|
||||||
import { versionsListAtom } from "@/atoms/appAtoms";
|
import { versionsListAtom } from "@/atoms/appAtoms";
|
||||||
import { IpcClient } from "@/ipc/ipc_client";
|
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 [versions, setVersions] = useAtom(versionsListAtom);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<Error | null>(null);
|
const [error, setError] = useState<Error | null>(null);
|
||||||
|
const selectedChatId = useAtomValue(selectedChatIdAtom);
|
||||||
|
const [messages, setMessages] = useAtom(chatMessagesAtom);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadVersions = async () => {
|
const loadVersions = async () => {
|
||||||
// If no app is selected, clear versions and return
|
// If no app is selected, clear versions and return
|
||||||
@@ -50,5 +53,26 @@ export function useLoadVersions(appId: number | null) {
|
|||||||
}
|
}
|
||||||
}, [appId, setVersions, setError]);
|
}, [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 { ipcMain } from "electron";
|
||||||
import { db, getDatabasePath } from "../../db";
|
import { db, getDatabasePath } from "../../db";
|
||||||
import { apps, chats } from "../../db/schema";
|
import { apps, chats, messages } from "../../db/schema";
|
||||||
import { desc, eq } from "drizzle-orm";
|
import { desc, eq, and, gte, sql, gt } from "drizzle-orm";
|
||||||
import type {
|
import type {
|
||||||
App,
|
App,
|
||||||
CreateAppParams,
|
CreateAppParams,
|
||||||
@@ -572,6 +572,44 @@ export function registerAppHandlers() {
|
|||||||
author: await getGitAuthor(),
|
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 };
|
return { success: true };
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
logger.error(
|
logger.error(
|
||||||
|
|||||||
@@ -435,7 +435,7 @@ export async function processFullResponseActions(
|
|||||||
changes.push(`executed ${dyadExecuteSqlQueries.length} SQL queries`);
|
changes.push(`executed ${dyadExecuteSqlQueries.length} SQL queries`);
|
||||||
|
|
||||||
// Use chat summary, if provided, or default for commit message
|
// Use chat summary, if provided, or default for commit message
|
||||||
await git.commit({
|
const commitHash = await git.commit({
|
||||||
fs,
|
fs,
|
||||||
dir: appPath,
|
dir: appPath,
|
||||||
message: chatSummary
|
message: chatSummary
|
||||||
@@ -444,6 +444,14 @@ export async function processFullResponseActions(
|
|||||||
author: await getGitAuthor(),
|
author: await getGitAuthor(),
|
||||||
});
|
});
|
||||||
logger.log(`Successfully committed changes: ${changes.join(", ")}`);
|
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);
|
logger.log("mark as approved: hasChanges", hasChanges);
|
||||||
// Update the message to approved
|
// Update the message to approved
|
||||||
|
|||||||
Reference in New Issue
Block a user