From e32804c3e07a7faf694e6a0ba0b2f568fbd7aa31 Mon Sep 17 00:00:00 2001 From: Navid Shad Date: Tue, 7 Apr 2026 23:19:32 +0300 Subject: [PATCH 1/4] feat: implement single message deletion and content editing capabilities with re-chaining logic #86ex65mr3 --- src/main/index.ts | 8 ++ src/main/threads/index.ts | 38 +++++++++ src/preload/index.ts | 4 + src/renderer/src/assets/main.css | 7 ++ .../src/components/chat/ChatMessage.vue | 51 +++++++++-- .../src/components/graph/ConversationNode.vue | 84 +++++++++++++++++-- src/renderer/src/pages/GraphChatPage.vue | 4 +- src/renderer/src/stores/videoStore.ts | 29 +++++++ 8 files changed, 212 insertions(+), 13 deletions(-) diff --git a/src/main/index.ts b/src/main/index.ts index da0a978..6cbda25 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -358,6 +358,14 @@ app.whenReady().then(() => { ipcMain.handle('remove-message', async (_event, { threadId, messageId }) => { return await threadManager.removeMessageBranchFromThread(threadId, messageId) }) + + ipcMain.handle('remove-single-message', async (_event, { threadId, messageId }) => { + return await threadManager.removeSingleMessage(threadId, messageId) + }) + + ipcMain.handle('update-message', async (_event, { threadId, messageId, updates }) => { + return await threadManager.updateMessageInThread(threadId, messageId, updates) + }) ipcMain.handle('save-node-positions', async (_event, { threadId, positions }) => { return await threadManager.updateThreadNodePositions(threadId, positions) diff --git a/src/main/threads/index.ts b/src/main/threads/index.ts index 6d4cb9a..1f15779 100644 --- a/src/main/threads/index.ts +++ b/src/main/threads/index.ts @@ -615,6 +615,44 @@ class ThreadManager { return !!result } + + // NEW: Remove a single message from a thread and re-chain its children + async removeSingleMessage(threadId: string, messageId: string): Promise { + const thread = this.getThread(threadId) + if (!thread) return false + + const msg = thread.messages.find(m => m.id === messageId) + if (!msg) return false + + // 1. Delete associated files for THIS message only (except original and protected reference/analysis files) + if (msg.files) { + for (const file of msg.files) { + if (file.type !== FileType.Original) { + const cleanPath = file.url.replace('file://', '').replace('media://', '') + + // Only allow deletion if it belongs to generated directories + const isGenerated = cleanPath.includes(`/${THREAD_DIRS.GENERATED_IMAGES}/`) || + cleanPath.includes(`/${THREAD_DIRS.GENERATED_VIDEOS}/`) + + if (isGenerated) { + this.deleteFile(file.url) + } + } + } + } + + // 2. Re-chain children to point to this message's parent (editRefId) + const parentId = msg.editRefId + const updatedMessages = thread.messages + .filter(m => m.id !== messageId) + .map(m => m.editRefId === messageId ? { ...m, editRefId: parentId } : m) + + const result = await this.updateThread(threadId, { + messages: updatedMessages + }) + + return !!result + } } export const threadManager = new ThreadManager() diff --git a/src/preload/index.ts b/src/preload/index.ts index 42c4153..23c2984 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -54,6 +54,10 @@ const api = { ipcRenderer.invoke('save-node-positions', { threadId, positions }), removeMessage: (threadId: string, messageId: string) => ipcRenderer.invoke('remove-message', { threadId, messageId }), + removeSingleMessage: (threadId: string, messageId: string) => + ipcRenderer.invoke('remove-single-message', { threadId, messageId }), + updateMessage: (threadId: string, messageId: string, updates: any) => + ipcRenderer.invoke('update-message', { threadId, messageId, updates }), showConfirmation: (options: { title: string, message: string, detail?: string, type?: string, buttons?: string[], defaultId?: number, cancelId?: number }) => ipcRenderer.invoke('show-confirmation', options), saveVideo: (sourcePath: string) => ipcRenderer.invoke('save-video', sourcePath), diff --git a/src/renderer/src/assets/main.css b/src/renderer/src/assets/main.css index c6d6c56..f6123f4 100644 --- a/src/renderer/src/assets/main.css +++ b/src/renderer/src/assets/main.css @@ -55,4 +55,11 @@ .custom-scrollbar::-webkit-scrollbar-thumb { @apply bg-zinc-300/50 dark:bg-zinc-700/50 rounded-full hover:bg-zinc-400/50 dark:hover:bg-zinc-600/50 transition-colors; } + + .custom-textarea-full { + @apply h-full flex flex-col; + } + .custom-textarea-full textarea { + @apply h-full min-h-[50vh] !resize-none; + } } diff --git a/src/renderer/src/components/chat/ChatMessage.vue b/src/renderer/src/components/chat/ChatMessage.vue index a9194ad..3ae7382 100644 --- a/src/renderer/src/components/chat/ChatMessage.vue +++ b/src/renderer/src/components/chat/ChatMessage.vue @@ -8,12 +8,14 @@ {{ message.role === MessageRole.User ? 'You' : 'AI Assistant' }} - -
+
- +
@@ -103,12 +105,31 @@ + + + +
+
+