From 2f6ee5e4e4a86c04d2e14e55b6f621b6b9272c9d Mon Sep 17 00:00:00 2001 From: wyh Date: Tue, 1 Apr 2025 19:37:40 +0800 Subject: [PATCH 1/3] fix: delete the project old cache. --- src/LLMProviders/projectManager.ts | 2 +- src/components/Chat.tsx | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/LLMProviders/projectManager.ts b/src/LLMProviders/projectManager.ts index 2d0b95a9..4aa2b4ca 100644 --- a/src/LLMProviders/projectManager.ts +++ b/src/LLMProviders/projectManager.ts @@ -83,7 +83,7 @@ export default class ProjectManager { // Check if project configuration has changed if (JSON.stringify(prevProject) !== JSON.stringify(nextProject)) { // Clear context cache for this project - await this.projectContextCache.clearForProject(nextProject); + await this.projectContextCache.clearForProject(prevProject); // If this is the current project, reload its context and recreate chain if (this.currentProjectId === nextProject.id) { diff --git a/src/components/Chat.tsx b/src/components/Chat.tsx index 6fa93c84..3442a21c 100644 --- a/src/components/Chat.tsx +++ b/src/components/Chat.tsx @@ -28,8 +28,6 @@ import { Buffer } from "buffer"; import { Notice, TFile } from "obsidian"; import React, { useCallback, useContext, useEffect, useRef, useState } from "react"; -type ChatMode = "default" | "project"; - interface ChatProps { sharedState: SharedState; chainManager: ChainManager; @@ -37,7 +35,6 @@ interface ChatProps { updateUserMessageHistory: (newMessage: string) => void; fileParserManager: FileParserManager; plugin: CopilotPlugin; - mode?: ChatMode; } const Chat: React.FC = ({ From 7cb9a56fb952efb951d4fe69dffceeeb23650d8a Mon Sep 17 00:00:00 2001 From: wyh Date: Tue, 1 Apr 2025 20:46:00 +0800 Subject: [PATCH 2/3] fix: refresh the project cache. --- src/LLMProviders/projectManager.ts | 14 ++-- src/cache/projectContextCache.ts | 111 ++++++++++++++++++++++++----- src/main.ts | 4 ++ 3 files changed, 102 insertions(+), 27 deletions(-) diff --git a/src/LLMProviders/projectManager.ts b/src/LLMProviders/projectManager.ts index 4aa2b4ca..945e6a4b 100644 --- a/src/LLMProviders/projectManager.ts +++ b/src/LLMProviders/projectManager.ts @@ -39,7 +39,7 @@ export default class ProjectManager { this.plugin = plugin; this.currentProjectId = null; this.chainMangerInstance = new ChainManager(app, vectorStoreManager); - this.projectContextCache = ProjectContextCache.getInstance(); + this.projectContextCache = ProjectContextCache.getInstance(app); this.chatMessageCache = new Map(); // Set up subscriptions @@ -251,14 +251,6 @@ ${contextParts.join("\n\n")} return this.projectContextCache.getSync(project); } - public async clearContextCache(projectId: string): Promise { - const project = getSettings().projectList.find((p) => p.id === projectId); - if (project) { - await this.projectContextCache.clearForProject(project); - logInfo(`Context cache cleared for project: ${projectId}`); - } - } - private async processMarkdownContext(inclusions?: string, exclusions?: string): Promise { if (!inclusions && !exclusions) { return ""; @@ -336,4 +328,8 @@ ${content}`; const results = await Promise.all(processPromises); return results.join(""); } + + public onunload(): void { + this.projectContextCache.cleanup(); + } } diff --git a/src/cache/projectContextCache.ts b/src/cache/projectContextCache.ts index 071c2d2d..4aff5ebf 100644 --- a/src/cache/projectContextCache.ts +++ b/src/cache/projectContextCache.ts @@ -1,38 +1,104 @@ import { ProjectConfig } from "@/aiParams"; import { logError, logInfo } from "@/logger"; import { MD5 } from "crypto-js"; +import { App, TAbstractFile, TFile, Vault } from "obsidian"; +import { getMatchingPatterns, shouldIndexFile } from "@/search/searchUtils"; +import { getSettings } from "@/settings/model"; + +const DEBOUNCE_DELAY = 5000; // 5 seconds export class ProjectContextCache { private static instance: ProjectContextCache; private cacheDir: string = ".copilot/project-context-cache"; private memoryCache: Map = new Map(); + private vault: Vault; + private debounceTimer: number | null = null; - private constructor() {} + private constructor(private app: App) { + this.vault = app.vault; + this.initializeEventListeners(); + } - static getInstance(): ProjectContextCache { + static getInstance(app: App): ProjectContextCache { if (!ProjectContextCache.instance) { - ProjectContextCache.instance = new ProjectContextCache(); + ProjectContextCache.instance = new ProjectContextCache(app); } return ProjectContextCache.instance; } + private handleFileEvent = (file: TAbstractFile) => { + if (file instanceof TFile) { + this.debouncedHandleFileChange(file); + } + }; + + private initializeEventListeners() { + // Monitor file events + this.vault.on("create", this.handleFileEvent); + this.vault.on("modify", this.handleFileEvent); + this.vault.on("delete", this.handleFileEvent); + this.vault.on("rename", this.handleFileEvent); + } + + private debouncedHandleFileChange = (file: TFile) => { + if (this.debounceTimer !== null) { + window.clearTimeout(this.debounceTimer); + } + + this.debounceTimer = window.setTimeout(() => { + this.handleFileChange(file); + this.debounceTimer = null; + }, DEBOUNCE_DELAY); + }; + + private async handleFileChange(file: TFile) { + try { + // enable markdown file + if (file.extension !== "md") { + return; + } + + const settings = getSettings(); + const projects = settings.projectList || []; + + // check if the project needs to clear cache + await Promise.all([ + projects.map(async (project) => { + const { inclusions, exclusions } = getMatchingPatterns({ + inclusions: project.contextSource.inclusions, + exclusions: project.contextSource.exclusions, + isProject: true, + }); + + if (shouldIndexFile(file, inclusions, exclusions)) { + // clear cache + await this.clearForProject(project); + logInfo( + `Cleared context cache for project ${project.name} due to file change: ${file.path}` + ); + } + }), + ]); + } catch (error) { + logError("Error handling file change for project context cache:", error); + } + } + private async ensureCacheDir() { - if (!(await app.vault.adapter.exists(this.cacheDir))) { + if (!(await this.vault.adapter.exists(this.cacheDir))) { logInfo("Creating project context cache directory:", this.cacheDir); - await app.vault.adapter.mkdir(this.cacheDir); + await this.vault.adapter.mkdir(this.cacheDir); } } private getCacheKey(project: ProjectConfig): string { // Use project ID, system prompt, and context sources for a unique cache key - logInfo("Generating cache key for project context:", project.contextSource); const metadata = JSON.stringify({ id: project.id, contextSource: project.contextSource, systemPrompt: project.systemPrompt, }); const key = MD5(metadata).toString(); - logInfo("Generated cache key for project:", { name: project.name, key }); return key; } @@ -52,9 +118,9 @@ export class ProjectContextCache { } const cachePath = this.getCachePath(cacheKey); - if (await app.vault.adapter.exists(cachePath)) { + if (await this.vault.adapter.exists(cachePath)) { logInfo("File cache hit for project:", project.name); - const cacheContent = await app.vault.adapter.read(cachePath); + const cacheContent = await this.vault.adapter.read(cachePath); const context = JSON.parse(cacheContent).context; // Store in memory cache this.memoryCache.set(cacheKey, context); @@ -93,7 +159,7 @@ export class ProjectContextCache { // Store in memory cache this.memoryCache.set(cacheKey, context); // Store in file cache - await app.vault.adapter.write( + await this.vault.adapter.write( cachePath, JSON.stringify({ context, @@ -105,17 +171,15 @@ export class ProjectContextCache { } } - async clear(): Promise { + async clearAllCache(): Promise { try { // Clear memory cache this.memoryCache.clear(); // Clear file cache - if (await app.vault.adapter.exists(this.cacheDir)) { - const files = await app.vault.adapter.list(this.cacheDir); + if (await this.vault.adapter.exists(this.cacheDir)) { + const files = await this.vault.adapter.list(this.cacheDir); logInfo("Clearing project context cache, removing files:", files.files.length); - for (const file of files.files) { - await app.vault.adapter.remove(file); - } + await Promise.all([files.files.map((file) => this.vault.adapter.remove(file))]); } } catch (error) { logError("Error clearing project context cache:", error); @@ -129,12 +193,23 @@ export class ProjectContextCache { this.memoryCache.delete(cacheKey); // Clear from file cache const cachePath = this.getCachePath(cacheKey); - if (await app.vault.adapter.exists(cachePath)) { + if (await this.vault.adapter.exists(cachePath)) { logInfo("Clearing cache for project:", project.name); - await app.vault.adapter.remove(cachePath); + await this.vault.adapter.remove(cachePath); } } catch (error) { logError("Error clearing cache for project:", error); } } + + public cleanup() { + if (this.debounceTimer !== null) { + window.clearTimeout(this.debounceTimer); + } + + this.vault.off("create", this.handleFileEvent); + this.vault.off("modify", this.handleFileEvent); + this.vault.off("delete", this.handleFileEvent); + this.vault.off("rename", this.handleFileEvent); + } } diff --git a/src/main.ts b/src/main.ts index aa8d7456..deae001a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -126,6 +126,10 @@ export default class CopilotPlugin extends Plugin { this.vectorStoreManager.onunload(); } + if (this.projectManager) { + this.projectManager.onunload(); + } + this.settingsUnsubscriber?.(); this.autocompleteService?.destroy(); From 4a1b92cffe6a0f425af3055271e3dfc67fffeeab Mon Sep 17 00:00:00 2001 From: wyh Date: Wed, 9 Apr 2025 21:12:39 +0800 Subject: [PATCH 3/3] feat: Refactor project caching functionality. --- src/LLMProviders/chainRunner.ts | 4 +- src/LLMProviders/projectManager.ts | 246 ++++++++++++++++++++++++----- src/cache/projectContextCache.ts | 105 ++++++++---- 3 files changed, 281 insertions(+), 74 deletions(-) diff --git a/src/LLMProviders/chainRunner.ts b/src/LLMProviders/chainRunner.ts index 579b88c7..33c2c532 100644 --- a/src/LLMProviders/chainRunner.ts +++ b/src/LLMProviders/chainRunner.ts @@ -776,8 +776,8 @@ class ProjectChainRunner extends CopilotPlusChainRunner { return super.getSystemPrompt(); } - // Get cached context synchronously - const context = ProjectManager.instance.getProjectContext(projectConfig.id); + // Get context asynchronously + const context = await ProjectManager.instance.getProjectContext(projectConfig.id); let finalPrompt = projectConfig.systemPrompt; if (context) { diff --git a/src/LLMProviders/projectManager.ts b/src/LLMProviders/projectManager.ts index 945e6a4b..f7032749 100644 --- a/src/LLMProviders/projectManager.ts +++ b/src/LLMProviders/projectManager.ts @@ -7,7 +7,7 @@ import { subscribeToModelKeyChange, subscribeToProjectChange, } from "@/aiParams"; -import { ProjectContextCache } from "@/cache/projectContextCache"; +import { ContextCache, ProjectContextCache } from "@/cache/projectContextCache"; import { ChainType } from "@/chainFactory"; import { updateChatMemory } from "@/chatUtils"; import CopilotView from "@/components/CopilotView"; @@ -39,7 +39,7 @@ export default class ProjectManager { this.plugin = plugin; this.currentProjectId = null; this.chainMangerInstance = new ChainManager(app, vectorStoreManager); - this.projectContextCache = ProjectContextCache.getInstance(app); + this.projectContextCache = ProjectContextCache.getInstance(); this.chatMessageCache = new Map(); // Set up subscriptions @@ -82,8 +82,8 @@ export default class ProjectManager { if (prevProject) { // Check if project configuration has changed if (JSON.stringify(prevProject) !== JSON.stringify(nextProject)) { - // Clear context cache for this project - await this.projectContextCache.clearForProject(prevProject); + // Compare project configuration changes and selectively update cache + await this.compareAndUpdateCache(prevProject, nextProject); // If this is the current project, reload its context and recreate chain if (this.currentProjectId === nextProject.id) { @@ -184,54 +184,103 @@ export default class ProjectManager { } // TODO(logan): This should be reused as a generic context loading function - private async loadProjectContext(project: ProjectConfig): Promise { + private async loadProjectContext(project: ProjectConfig): Promise { try { - if (project.contextSource) { - // Try to get context from cache first - const cachedContext = await this.projectContextCache.get(project); - if (cachedContext) { - return; - } + if (!project.contextSource) { + return null; + } - const [markdownContext, webContext, youtubeContext] = await Promise.all([ - this.processMarkdownContext( - project.contextSource.inclusions, - project.contextSource.exclusions - ), - this.processWebUrlsContext(project.contextSource.webUrls), - this.processYoutubeUrlsContext(project.contextSource.youtubeUrls), - ]); + const contextCache = (await this.projectContextCache.get(project)) || { + markdownContext: "", + webContexts: {}, + youtubeContexts: {}, + timestamp: Date.now(), + markdownNeedsReload: false, + }; + + const [updatedContextCache] = await Promise.all([ + this.processMarkdownFiles(project, contextCache), + this.processWebUrls(project, contextCache), + this.processYoutubeUrls(project, contextCache), + ]); - // Build context sections only for non-null sources - const contextParts = []; + updatedContextCache.timestamp = Date.now(); + await this.projectContextCache.set(project, updatedContextCache); + return updatedContextCache; + } catch (error) { + logError(`Failed to load project context: ${error}`); + throw error; + } + } - if (project.contextSource.inclusions || project.contextSource.exclusions) { - contextParts.push(`## Markdown Files\n${markdownContext}`); - } + private async compareAndUpdateCache(prevProject: ProjectConfig, nextProject: ProjectConfig) { + try { + const cache = await this.projectContextCache.get(prevProject); - if (project.contextSource.webUrls?.trim()) { - contextParts.push(`## Web Content\n${webContext}`); - } + // If no cache exists, return true to create a new cache later + if (!cache) { + return true; + } + + // Check if Markdown configuration has changed + const prevInclusions = prevProject.contextSource?.inclusions || ""; + const nextInclusions = nextProject.contextSource?.inclusions || ""; + const prevExclusions = prevProject.contextSource?.exclusions || ""; + const nextExclusions = nextProject.contextSource?.exclusions || ""; + + if (prevInclusions !== nextInclusions || prevExclusions !== nextExclusions) { + // Markdown config changed, clear markdown context and mark for reload + cache.markdownContext = ""; + cache.markdownNeedsReload = true; + logInfo( + `Markdown configuration changed for project ${nextProject.name}, marking for reload` + ); + } + + // Check if Web URLs configuration has changed + const prevWebUrls = prevProject.contextSource?.webUrls || ""; + const nextWebUrls = nextProject.contextSource?.webUrls || ""; - if (project.contextSource.youtubeUrls?.trim()) { - contextParts.push(`## YouTube Content\n${youtubeContext}`); + if (prevWebUrls !== nextWebUrls) { + // Find removed URLs + const prevUrls = prevWebUrls.split("\n").filter((url) => url.trim()); + const nextUrls = nextWebUrls.split("\n").filter((url) => url.trim()); + + // Remove context for URLs that no longer exist + for (const url of prevUrls) { + if (!nextUrls.includes(url) && cache.webContexts[url]) { + delete cache.webContexts[url]; + logInfo(`Removed web context for URL ${url} in project ${nextProject.name}`); + } } + } - const contextText = ` -# Project Context -The following information is the relevant context for this project. Use this information to inform your responses when appropriate: + // Check if YouTube URLs configuration has changed + const prevYoutubeUrls = prevProject.contextSource?.youtubeUrls || ""; + const nextYoutubeUrls = nextProject.contextSource?.youtubeUrls || ""; - -${contextParts.join("\n\n")} - -`; + if (prevYoutubeUrls !== nextYoutubeUrls) { + // Find removed URLs + const prevUrls = prevYoutubeUrls.split("\n").filter((url) => url.trim()); + const nextUrls = nextYoutubeUrls.split("\n").filter((url) => url.trim()); + + // Remove context for URLs that no longer exist + for (const url of prevUrls) { + if (!nextUrls.includes(url) && cache.youtubeContexts[url]) { + delete cache.youtubeContexts[url]; + logInfo(`Removed YouTube context for URL ${url} in project ${nextProject.name}`); + } + } + } - // Cache the generated context - await this.projectContextCache.set(project, contextText); + // Update cache if needed + if (cache.markdownNeedsReload) { + // Save updated cache with the new project ID + await this.projectContextCache.set(nextProject, cache); + logInfo(`Updated cache for project ${nextProject.name}`); } } catch (error) { - logError(`Failed to load project context: ${error}`); - throw error; + logError(`Error comparing project configurations: ${error}`); } } @@ -243,12 +292,125 @@ ${contextParts.join("\n\n")} } } - public getProjectContext(projectId: string): string | null { + public async getProjectContext(projectId: string): Promise { const project = getSettings().projectList.find((p) => p.id === projectId); if (!project) { return null; } - return this.projectContextCache.getSync(project); + + const contextCache = this.projectContextCache.getSync(project); + if (!contextCache) { + return null; + } + + if (contextCache.markdownNeedsReload) { + const updatedCache = await this.loadProjectContext(project); + if (!updatedCache) { + return null; + } + return this.formatProjectContext(updatedCache); + } + + return this.formatProjectContext(contextCache); + } + + private formatProjectContext(contextCache: ContextCache): string { + const contextParts = []; + + if (contextCache.markdownContext) { + contextParts.push(`## Markdown Files\n${contextCache.markdownContext}`); + } + + if (Object.keys(contextCache.webContexts).length > 0) { + contextParts.push(`## Web Content\n${Object.values(contextCache.webContexts).join("\n\n")}`); + } + + if (Object.keys(contextCache.youtubeContexts).length > 0) { + contextParts.push( + `## YouTube Content\n${Object.values(contextCache.youtubeContexts).join("\n\n")}` + ); + } + + return ` +# Project Context +The following information is the relevant context for this project. Use this information to inform your responses when appropriate: + + +${contextParts.join("\n\n")} + +`; + } + + private async processMarkdownFiles( + project: ProjectConfig, + contextCache: ContextCache + ): Promise { + if (project.contextSource?.inclusions || project.contextSource?.exclusions) { + // Only process if needsReload is true or there is no existing content + if (contextCache.markdownNeedsReload || !contextCache.markdownContext.trim()) { + const markdownContext = await this.processMarkdownContext( + project.contextSource.inclusions, + project.contextSource.exclusions + ); + contextCache.markdownContext = markdownContext; + contextCache.markdownNeedsReload = false; // reset flag + } + } + return contextCache; + } + + private async processWebUrls( + project: ProjectConfig, + contextCache: ContextCache + ): Promise { + if (!project.contextSource?.webUrls?.trim()) { + return contextCache; + } + + const urls = project.contextSource.webUrls.split("\n").filter((url) => url.trim()); + const webContextPromises = urls.map(async (url) => { + if (!contextCache.webContexts[url]) { + const webContext = await this.processWebUrlsContext(url); + return { url, context: webContext }; + } + return null; + }); + + const results = await Promise.all(webContextPromises); + results.forEach((result) => { + if (result) { + contextCache.webContexts[result.url] = result.context; + } + }); + + return contextCache; + } + + private async processYoutubeUrls( + project: ProjectConfig, + contextCache: ContextCache + ): Promise { + if (!project.contextSource?.youtubeUrls?.trim()) { + return contextCache; + } + + const urls = project.contextSource.youtubeUrls.split("\n").filter((url) => url.trim()); + const youtubeContextPromises = urls.map(async (url) => { + if (!contextCache.youtubeContexts[url]) { + const youtubeContext = await this.processYoutubeUrlsContext(url); + return { url, context: youtubeContext }; + } + return null; + }); + + const results = await Promise.all(youtubeContextPromises); + results.forEach((result) => { + if (result) { + contextCache.youtubeContexts[result.url] = result.context; + } + }); + + return contextCache; } private async processMarkdownContext(inclusions?: string, exclusions?: string): Promise { diff --git a/src/cache/projectContextCache.ts b/src/cache/projectContextCache.ts index 4aff5ebf..2b90c79c 100644 --- a/src/cache/projectContextCache.ts +++ b/src/cache/projectContextCache.ts @@ -1,27 +1,35 @@ import { ProjectConfig } from "@/aiParams"; import { logError, logInfo } from "@/logger"; -import { MD5 } from "crypto-js"; -import { App, TAbstractFile, TFile, Vault } from "obsidian"; +import { TAbstractFile, TFile, Vault } from "obsidian"; import { getMatchingPatterns, shouldIndexFile } from "@/search/searchUtils"; import { getSettings } from "@/settings/model"; +import { MD5 } from "crypto-js"; const DEBOUNCE_DELAY = 5000; // 5 seconds +export interface ContextCache { + markdownContext: string; + webContexts: Record; // URL -> context + youtubeContexts: Record; // URL -> context + timestamp: number; + markdownNeedsReload: boolean; +} + export class ProjectContextCache { private static instance: ProjectContextCache; private cacheDir: string = ".copilot/project-context-cache"; - private memoryCache: Map = new Map(); + private memoryCache: Map = new Map(); private vault: Vault; private debounceTimer: number | null = null; - private constructor(private app: App) { + private constructor() { this.vault = app.vault; this.initializeEventListeners(); } - static getInstance(app: App): ProjectContextCache { + static getInstance(): ProjectContextCache { if (!ProjectContextCache.instance) { - ProjectContextCache.instance = new ProjectContextCache(app); + ProjectContextCache.instance = new ProjectContextCache(); } return ProjectContextCache.instance; } @@ -71,10 +79,10 @@ export class ProjectContextCache { }); if (shouldIndexFile(file, inclusions, exclusions)) { - // clear cache - await this.clearForProject(project); + // Only clear markdown context, keep web and youtube contexts + await this.clearMarkdownContext(project); logInfo( - `Cleared context cache for project ${project.name} due to file change: ${file.path}` + `Cleared markdown context cache for project ${project.name} due to file change: ${file.path}` ); } }), @@ -92,13 +100,8 @@ export class ProjectContextCache { } private getCacheKey(project: ProjectConfig): string { - // Use project ID, system prompt, and context sources for a unique cache key - const metadata = JSON.stringify({ - id: project.id, - contextSource: project.contextSource, - systemPrompt: project.systemPrompt, - }); - const key = MD5(metadata).toString(); + // Use project ID as cache key + const key = MD5(project.id).toString(); return key; } @@ -106,7 +109,7 @@ export class ProjectContextCache { return `${this.cacheDir}/${cacheKey}.json`; } - async get(project: ProjectConfig): Promise { + async get(project: ProjectConfig): Promise { try { const cacheKey = this.getCacheKey(project); @@ -121,10 +124,10 @@ export class ProjectContextCache { if (await this.vault.adapter.exists(cachePath)) { logInfo("File cache hit for project:", project.name); const cacheContent = await this.vault.adapter.read(cachePath); - const context = JSON.parse(cacheContent).context; + const contextCache = JSON.parse(cacheContent); // Store in memory cache - this.memoryCache.set(cacheKey, context); - return context; + this.memoryCache.set(cacheKey, contextCache); + return contextCache; } logInfo("Cache miss for project:", project.name); return null; @@ -134,7 +137,7 @@ export class ProjectContextCache { } } - getSync(project: ProjectConfig): string | null { + getSync(project: ProjectConfig): ContextCache | null { try { const cacheKey = this.getCacheKey(project); const memoryResult = this.memoryCache.get(cacheKey); @@ -150,27 +153,69 @@ export class ProjectContextCache { } } - async set(project: ProjectConfig, context: string): Promise { + async set(project: ProjectConfig, contextCache: ContextCache): Promise { try { await this.ensureCacheDir(); const cacheKey = this.getCacheKey(project); const cachePath = this.getCachePath(cacheKey); logInfo("Caching context for project:", project.name); // Store in memory cache - this.memoryCache.set(cacheKey, context); + this.memoryCache.set(cacheKey, contextCache); // Store in file cache - await this.vault.adapter.write( - cachePath, - JSON.stringify({ - context, - timestamp: Date.now(), - }) - ); + await this.vault.adapter.write(cachePath, JSON.stringify(contextCache)); } catch (error) { logError("Error writing to project context cache:", error); } } + async updateWebContext(project: ProjectConfig, url: string, context: string): Promise { + const cache = (await this.get(project)) || { + markdownContext: "", + webContexts: {}, + youtubeContexts: {}, + timestamp: Date.now(), + markdownNeedsReload: false, + }; + + cache.webContexts[url] = context; + await this.set(project, cache); + } + + async updateYoutubeContext(project: ProjectConfig, url: string, context: string): Promise { + const cache = (await this.get(project)) || { + markdownContext: "", + webContexts: {}, + youtubeContexts: {}, + timestamp: Date.now(), + markdownNeedsReload: false, + }; + + cache.youtubeContexts[url] = context; + await this.set(project, cache); + } + + async updateMarkdownContext(project: ProjectConfig, context: string): Promise { + const cache = (await this.get(project)) || { + markdownContext: "", + webContexts: {}, + youtubeContexts: {}, + timestamp: Date.now(), + markdownNeedsReload: false, + }; + + cache.markdownContext = context; + await this.set(project, cache); + } + + async clearMarkdownContext(project: ProjectConfig): Promise { + const cache = await this.get(project); + if (cache) { + cache.markdownContext = ""; + cache.markdownNeedsReload = true; + await this.set(project, cache); + } + } + async clearAllCache(): Promise { try { // Clear memory cache