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.
This commit is contained in:
Michael Ryan 2026-03-16 04:34:14 -07:00 committed by GitHub
parent 1bf571d63f
commit 87ee9d0d56
No known key found for this signature in database
GPG key ID: B5690EEEBB952194

View file

@ -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<void> {
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;
}
}