Skip to content

some project-based feature bug fix #1420

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 6 additions & 10 deletions src/LLMProviders/projectManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -251,14 +251,6 @@ ${contextParts.join("\n\n")}
return this.projectContextCache.getSync(project);
}

public async clearContextCache(projectId: string): Promise<void> {
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<string> {
if (!inclusions && !exclusions) {
return "";
Expand Down Expand Up @@ -336,4 +328,8 @@ ${content}`;
const results = await Promise.all(processPromises);
return results.join("");
}

public onunload(): void {
this.projectContextCache.cleanup();
}
}
111 changes: 93 additions & 18 deletions src/cache/projectContextCache.ts
Original file line number Diff line number Diff line change
@@ -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<string, string> = 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;
}

Expand All @@ -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);
Expand Down Expand Up @@ -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,
Expand All @@ -105,17 +171,15 @@ export class ProjectContextCache {
}
}

async clear(): Promise<void> {
async clearAllCache(): Promise<void> {
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);
Expand All @@ -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);
}
}
3 changes: 0 additions & 3 deletions src/components/Chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,13 @@ 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;
onSaveChat: (saveAsNote: () => Promise<void>) => void;
updateUserMessageHistory: (newMessage: string) => void;
fileParserManager: FileParserManager;
plugin: CopilotPlugin;
mode?: ChatMode;
}

const Chat: React.FC<ChatProps> = ({
Expand Down
4 changes: 4 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,10 @@ export default class CopilotPlugin extends Plugin {
this.vectorStoreManager.onunload();
}

if (this.projectManager) {
this.projectManager.onunload();
}

this.settingsUnsubscriber?.();
this.autocompleteService?.destroy();

Expand Down