mirror of
https://github.com/logancyang/obsidian-copilot.git
synced 2026-07-22 07:50:24 +00:00
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:
parent
1bf571d63f
commit
87ee9d0d56
1 changed files with 56 additions and 8 deletions
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue