From 87ee9d0d56ccc7d84e070f0f49e67af8ef989524 Mon Sep 17 00:00:00 2001 From: Michael Ryan Date: Mon, 16 Mar 2026 04:34:14 -0700 Subject: [PATCH] fix(rename): Add automatic file renaming to match generated topic titles (#2240) * fix(rename): Add automatic file renaming to match generated topic titles Adds automatic file renaming so that notes reflect the finalized AI-generated topic in their filename. I believe this restores previously expected behavior. Refs #2005 * Fix chat file renaming to respect original project and hidden directories - Pass captured project context to generateFileName for async rename, ensuring the filename uses the project active at save time rather than global current-project state. - Add fallback for hidden-directory files when reading epoch for rename, using the vault adapter if metadataCache is missing, so AI-generated topic renaming works for hidden folders. Fix #2240 * fix: honor explicit null project context in async chat renaming Refactor ChatPersistenceManager to distinguish between 'undefined' project (fallback to global) and 'null' project (explicitly no project). This prevents a race condition where non-project chats were incorrectly moved into project directories upon async file renaming. Key changes: - Updated 'generateFileName' to use strict undefined checking for project fallback. - Reordered method signatures for 'generateFileName', 'generateTopicAsyncIfNeeded', and 'renameFileToMatchTopic' to prioritize the project context. - Verified all internal call sites have been updated to match the new signatures. - Confirmed fix via unit tests and local compilation. --- src/core/ChatPersistenceManager.ts | 64 ++++++++++++++++++++++++++---- 1 file changed, 56 insertions(+), 8 deletions(-) diff --git a/src/core/ChatPersistenceManager.ts b/src/core/ChatPersistenceManager.ts index ba3ee402..0a0321f3 100644 --- a/src/core/ChatPersistenceManager.ts +++ b/src/core/ChatPersistenceManager.ts @@ -1,4 +1,4 @@ -import { getCurrentProject } from "@/aiParams"; +import { getCurrentProject, ProjectConfig } from "@/aiParams"; import { AI_SENDER, USER_SENDER } from "@/constants"; import ChainManager from "@/LLMProviders/chainManager"; import { parseReasoningBlock } from "@/LLMProviders/chainRunner/utils/AgentReasoningState"; @@ -87,9 +87,11 @@ export class ChatPersistenceManager { } } + const currentProject = getCurrentProject(); + const preferredFileName = existingFile ? existingFile.path - : this.generateFileName(messages, firstMessageEpoch, existingTopic); + : this.generateFileName(currentProject, messages, firstMessageEpoch, existingTopic); const noteContent = this.generateNoteContent( chatContent, @@ -160,7 +162,6 @@ export class ChatPersistenceManager { } } else if (this.isNameTooLongError(error)) { // Single fallback: minimal guaranteed-to-work filename with project prefix - const currentProject = getCurrentProject(); const filePrefix = currentProject ? `${currentProject.id}__` : ""; const fallbackName = `${settings.defaultSaveFolder}/${filePrefix}chat-${firstMessageEpoch}.md`; @@ -212,7 +213,7 @@ export class ChatPersistenceManager { } } - this.generateTopicAsyncIfNeeded(messages, targetFile, existingTopic); + this.generateTopicAsyncIfNeeded(currentProject, targetFile, messages, existingTopic); } catch (error) { logError("[ChatPersistenceManager] Error saving chat:", error); new Notice("Failed to save chat as note. Check console for details."); @@ -614,11 +615,13 @@ ${conversationSummary}`; /** * Generate a file name for the chat. + * @param project - The project context for the filename prefix. * @param messages - The conversation messages used to derive the topic. * @param firstMessageEpoch - Epoch timestamp of the first message in the chat. * @param topic - Optional pre-computed topic to use for the filename. */ private generateFileName( + project: ProjectConfig | null, messages: ChatMessage[], firstMessageEpoch: number, topic?: string @@ -627,7 +630,7 @@ ${conversationSummary}`; const formattedDateTime = formatDateTime(new Date(firstMessageEpoch)); const timestampFileName = formattedDateTime.fileName; - // Use provided topic or fall back to first 10 words + // Use the provided topic or fall back to the first 10 words let topicForFilename: string; if (topic) { topicForFilename = topic; @@ -656,8 +659,8 @@ ${conversationSummary}`; // Parse the custom format and replace variables let customFileName = settings.defaultConversationNoteName || "{$date}_{$time}__{$topic}"; - // Get the current project prefix if any - const currentProject = getCurrentProject(); + // Prefix from an input project, global project, or empty if none + const currentProject = project === undefined ? getCurrentProject() : project; const filePrefix = currentProject ? `${currentProject.id}__` : ""; // Calculate fixed components in bytes @@ -739,8 +742,9 @@ ${chatContent}`; * Trigger asynchronous topic generation and apply it to the saved note once available */ private generateTopicAsyncIfNeeded( - messages: ChatMessage[], + project: ProjectConfig | null, file: TFile | null, + messages: ChatMessage[], existingTopic?: string ): void { const settings = getSettings(); @@ -756,6 +760,7 @@ ${chatContent}`; return; } await this.applyTopicToFrontmatter(file, topic); + await this.renameFileToMatchTopic(project, file, topic); } catch (error) { logError("[ChatPersistenceManager] Error during async topic generation:", error); } @@ -799,4 +804,47 @@ ${chatContent}`; const message = error instanceof Error ? error.message : String(error); return message.toLowerCase().includes("already exists"); } + + /** + * Rename a note file to match its finalized frontmatter topic + */ + async renameFileToMatchTopic( + project: ProjectConfig | null, + file: TFile, + topic: string + ): Promise { + if (!file || !topic) return; + + let epoch: number | undefined; + + const cache = this.app.metadataCache.getFileCache(file); + if (cache?.frontmatter?.epoch) { + epoch = cache.frontmatter.epoch; + } else { + // Fallback for hidden-directory files + try { + const adapterFm = await readFrontmatterViaAdapter(this.app, file.path); + if (adapterFm?.epoch) { + epoch = Number(adapterFm.epoch); + } + } catch { + // Ignore + } + } + + if (!epoch) { + return; + } + + const messages = this.messageRepo.getDisplayMessages(); + const newPath = this.generateFileName(project, messages, epoch, topic); + + if (file.path === newPath) { + return; + } + + await this.app.fileManager.renameFile(file, newPath); + + return; + } }