logancyang_obsidian-copilot/src/system-prompts/systemPromptManager.ts
Zero Liu 500bc347a0
chore(eslint): enable no-unsafe-member-access; fix 124 violations (#2438)
Enables @typescript-eslint/no-unsafe-member-access globally. Tests and 41
heavy source files (>5 violations each) are exempted via per-file overrides
with current violation counts annotated for follow-up PRs. Fixes 124
violations across 51 source files using narrow type assertions, instanceof
Error guards, and proper structural types instead of blanket any casts.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 22:35:38 -07:00

264 lines
7.9 KiB
TypeScript

import { TFile, Vault } from "obsidian";
import { UserSystemPrompt } from "@/system-prompts/type";
import {
ensurePromptFrontmatter,
fetchAllSystemPrompts,
generateCopyPromptName,
getPromptFilePath,
getSystemPromptsFolder,
loadAllSystemPrompts,
validatePromptName,
} from "@/system-prompts/systemPromptUtils";
import {
getCachedSystemPrompts,
upsertCachedSystemPrompt,
deleteCachedSystemPrompt,
addPendingFileWrite,
removePendingFileWrite,
getSelectedPromptTitle,
setSelectedPromptTitle,
} from "@/system-prompts/state";
import {
COPILOT_SYSTEM_PROMPT_CREATED,
COPILOT_SYSTEM_PROMPT_MODIFIED,
COPILOT_SYSTEM_PROMPT_LAST_USED,
} from "@/system-prompts/constants";
import { logInfo } from "@/logger";
import { ensureFolderExists } from "@/utils";
import { trashFile } from "@/utils/vaultAdapterUtils";
import { getSettings, updateSetting } from "@/settings/model";
/**
* Singleton manager for system prompts
* Provides centralized CRUD operations for system prompts
*/
export class SystemPromptManager {
private static instance: SystemPromptManager;
private vault: Vault;
private constructor(vault: Vault) {
this.vault = vault;
}
/**
* Get the singleton instance
*/
public static getInstance(vault?: Vault): SystemPromptManager {
if (!SystemPromptManager.instance) {
if (!vault) {
throw new Error("Vault is required for first initialization");
}
SystemPromptManager.instance = new SystemPromptManager(vault);
}
return SystemPromptManager.instance;
}
/**
* Initialize the manager by loading all prompts
*/
public async initialize(): Promise<void> {
logInfo("Initializing SystemPromptManager");
await loadAllSystemPrompts();
}
/**
* Create a new system prompt
* @param prompt - The system prompt to create
* @param skipStoreUpdate - If true, skip updating the cache (useful for batch operations)
*/
public async createPrompt(prompt: UserSystemPrompt, skipStoreUpdate = false): Promise<void> {
const existingPrompts = getCachedSystemPrompts();
const error = validatePromptName(prompt.title, existingPrompts);
if (error) {
throw new Error(error);
}
const filePath = getPromptFilePath(prompt.title);
const folderPath = getSystemPromptsFolder();
try {
addPendingFileWrite(filePath);
// Ensure nested folders are created cross-platform
await ensureFolderExists(folderPath);
// Create the file
await this.vault.create(filePath, prompt.content);
// Add frontmatter
const file = this.vault.getAbstractFileByPath(filePath);
if (file instanceof TFile) {
await ensurePromptFrontmatter(file, prompt);
}
// Update cache directly instead of reloading all files
if (!skipStoreUpdate) {
upsertCachedSystemPrompt(prompt);
}
logInfo(`Created system prompt: ${prompt.title}`);
} finally {
removePendingFileWrite(filePath);
}
}
/**
* Update an existing system prompt
* @param oldTitle - The current title of the prompt
* @param newPrompt - The updated prompt data
* @param skipStoreUpdate - If true, skip updating the cache (useful for batch operations)
*/
public async updatePrompt(
oldTitle: string,
newPrompt: UserSystemPrompt,
skipStoreUpdate = false
): Promise<void> {
const oldPath = getPromptFilePath(oldTitle);
const newPath = getPromptFilePath(newPrompt.title);
const isRename = oldTitle !== newPrompt.title;
// Validate new name if renaming
if (isRename) {
const existingPrompts = getCachedSystemPrompts();
const validationError = validatePromptName(newPrompt.title, existingPrompts, oldTitle);
if (validationError) {
throw new Error(validationError);
}
}
try {
addPendingFileWrite(newPath);
if (isRename) {
addPendingFileWrite(oldPath);
}
// If title changed, rename the file
if (isRename) {
const oldFile = this.vault.getAbstractFileByPath(oldPath);
if (oldFile instanceof TFile) {
await app.fileManager.renameFile(oldFile, newPath);
}
}
// Update content
const file = this.vault.getAbstractFileByPath(newPath);
if (file instanceof TFile) {
await this.vault.modify(file, newPrompt.content);
// Update frontmatter - write back ALL fields since vault.modify clears frontmatter
// Reference: Command module writes all fields in processFrontMatter
await app.fileManager.processFrontMatter(file, (frontmatter: Record<string, unknown>) => {
frontmatter[COPILOT_SYSTEM_PROMPT_CREATED] = newPrompt.createdMs;
frontmatter[COPILOT_SYSTEM_PROMPT_MODIFIED] = newPrompt.modifiedMs;
frontmatter[COPILOT_SYSTEM_PROMPT_LAST_USED] = newPrompt.lastUsedMs;
});
}
// Update cache directly instead of reloading all files
if (!skipStoreUpdate) {
// If renamed, delete old cache entry first
if (isRename) {
deleteCachedSystemPrompt(oldTitle);
// Update defaultSystemPromptTitle if it was pointing to the old title
const settings = getSettings();
if (settings.defaultSystemPromptTitle === oldTitle) {
updateSetting("defaultSystemPromptTitle", newPrompt.title);
logInfo(`Updated defaultSystemPromptTitle: ${oldTitle} -> ${newPrompt.title}`);
}
}
upsertCachedSystemPrompt(newPrompt);
}
logInfo(`Updated system prompt: ${oldTitle} -> ${newPrompt.title}`);
} finally {
removePendingFileWrite(newPath);
if (isRename) {
removePendingFileWrite(oldPath);
}
}
}
/**
* Delete a system prompt
* @param title - The title of the prompt to delete
*/
public async deletePrompt(title: string): Promise<void> {
const filePath = getPromptFilePath(title);
try {
addPendingFileWrite(filePath);
// Delete the file first
const file = this.vault.getAbstractFileByPath(filePath);
if (file instanceof TFile) {
await trashFile(app, file);
}
// Clear state only after successful deletion to maintain consistency
const settings = getSettings();
if (settings.defaultSystemPromptTitle === title) {
updateSetting("defaultSystemPromptTitle", "");
logInfo(`Cleared defaultSystemPromptTitle (deleted: ${title})`);
}
if (getSelectedPromptTitle() === title) {
setSelectedPromptTitle("");
logInfo(`Cleared session selectedPromptTitle (deleted: ${title})`);
}
// Update cache directly instead of reloading all files
deleteCachedSystemPrompt(title);
logInfo(`Deleted system prompt: ${title}`);
} finally {
removePendingFileWrite(filePath);
}
}
/**
* Duplicate a system prompt
*/
public async duplicatePrompt(prompt: UserSystemPrompt): Promise<UserSystemPrompt> {
const existingPrompts = getCachedSystemPrompts();
const newTitle = generateCopyPromptName(prompt.title, existingPrompts);
const now = Date.now();
const duplicatedPrompt: UserSystemPrompt = {
title: newTitle,
content: prompt.content,
createdMs: now,
modifiedMs: now,
lastUsedMs: 0,
};
await this.createPrompt(duplicatedPrompt);
logInfo(`Duplicated system prompt: ${prompt.title} -> ${newTitle}`);
return duplicatedPrompt;
}
/**
* Get all prompts from cache
*/
public getPrompts(): UserSystemPrompt[] {
return getCachedSystemPrompts();
}
/**
* Reload all prompts from file system and update cache
*/
public async reloadPrompts(): Promise<UserSystemPrompt[]> {
return await loadAllSystemPrompts();
}
/**
* Fetch all prompts from file system without updating cache
* Use this when you need to control cache updates yourself (e.g., for latest-wins semantics)
*/
public async fetchPrompts(): Promise<UserSystemPrompt[]> {
return await fetchAllSystemPrompts();
}
}