mirror of
https://github.com/andy-stack/vaultkeeper-ai.git
synced 2026-07-22 16:30:27 +00:00
Replace object-based subscription tracking with token-based system to prevent memory leaks. Add dispose() method to BaseAIClass and ensure all components properly unsubscribe in onDestroy. Update SettingsService.subscribeToSettingsChanged to return subscription token instead of requiring subscriber object parameter.
190 lines
No EOL
5.9 KiB
TypeScript
190 lines
No EOL
5.9 KiB
TypeScript
import type VaultkeeperAIPlugin from "main";
|
|
import { Resolve } from "./DependencyService";
|
|
import { Services } from "./Services";
|
|
import { ChatMode } from "Enums/ChatMode";
|
|
import {
|
|
AIProvider,
|
|
AIProviderModel,
|
|
DEFAULT_MODEL_BY_PROVIDER,
|
|
DEFAULT_PLANNING_MODEL_BY_PROVIDER,
|
|
fromModel,
|
|
isvalidProvider,
|
|
isValidProviderModel,
|
|
modelMatchesProvider
|
|
} from "Enums/ApiProvider";
|
|
|
|
const DEFAULT_SETTINGS: IVaultkeeperAISettings = {
|
|
firstTimeStart: true,
|
|
|
|
chatMode: ChatMode.ReadOnly,
|
|
userInstruction: "",
|
|
|
|
provider: AIProvider.Claude,
|
|
model: AIProviderModel.ClaudeHaiku_4_5,
|
|
planningModel: AIProviderModel.ClaudeSonnet_4_6,
|
|
quickActionModel: AIProviderModel.ClaudeHaiku_4_5,
|
|
|
|
apiKeys: {
|
|
claude: "",
|
|
openai: "",
|
|
gemini: "",
|
|
mistral: ""
|
|
},
|
|
exclusions: [],
|
|
|
|
searchResultsLimit: 30,
|
|
snippetSizeLimit: 100,
|
|
|
|
enableMemories: false,
|
|
allowUpdatingMemories: true,
|
|
|
|
enableWebSearch: true,
|
|
enableWebViewer: false,
|
|
|
|
enableContextMenuActions: true,
|
|
enableToolbarActions: true,
|
|
|
|
hideDrawerElements: true
|
|
}
|
|
|
|
export interface IVaultkeeperAISettings {
|
|
firstTimeStart: boolean;
|
|
|
|
chatMode: ChatMode;
|
|
userInstruction: string;
|
|
|
|
provider: AIProvider;
|
|
model: AIProviderModel;
|
|
planningModel: AIProviderModel;
|
|
quickActionModel: AIProviderModel;
|
|
|
|
apiKeys: {
|
|
claude: string;
|
|
openai: string;
|
|
gemini: string;
|
|
mistral: string;
|
|
};
|
|
exclusions: string[];
|
|
|
|
searchResultsLimit: number;
|
|
snippetSizeLimit: number;
|
|
|
|
enableMemories: boolean;
|
|
allowUpdatingMemories: boolean;
|
|
|
|
enableWebSearch: boolean;
|
|
enableWebViewer: boolean;
|
|
|
|
enableContextMenuActions: boolean;
|
|
enableToolbarActions: boolean;
|
|
|
|
hideDrawerElements: boolean;
|
|
}
|
|
|
|
export class SettingsService {
|
|
|
|
public readonly settings: Readonly<IVaultkeeperAISettings>;
|
|
|
|
private readonly plugin: VaultkeeperAIPlugin;
|
|
private readonly subscribers: WeakMap<object, (() => void) | (() => Promise<void>)> = new WeakMap();
|
|
private readonly subscriberRefs: Set<WeakRef<object>> = new Set();
|
|
|
|
private settingsSnapshot: string;
|
|
|
|
public constructor(loadedSettings: Partial<IVaultkeeperAISettings>) {
|
|
this.plugin = Resolve<VaultkeeperAIPlugin>(Services.VaultkeeperAIPlugin);
|
|
this.settings = Object.assign({}, DEFAULT_SETTINGS, loadedSettings);
|
|
this.settingsSnapshot = JSON.stringify(this.settings);
|
|
this.ensureValidModels();
|
|
}
|
|
|
|
public subscribeToSettingsChanged(callback: (() => void) | (() => Promise<void>)): object {
|
|
const token = {};
|
|
this.subscribers.set(token, callback);
|
|
this.subscriberRefs.add(new WeakRef(token));
|
|
return token;
|
|
}
|
|
|
|
public unsubscribe(subscriber: object): void {
|
|
this.subscribers.delete(subscriber);
|
|
}
|
|
|
|
public async updateSettings(updateAction: ((settings: IVaultkeeperAISettings) => void) | ((settings: IVaultkeeperAISettings) => Promise<void>)) {
|
|
await updateAction(this.settings);
|
|
await this.saveSettings();
|
|
}
|
|
|
|
public getApiKeyForCurrentModel(): string {
|
|
const provider = fromModel(this.settings.model);
|
|
return this.getApiKeyForProvider(provider);
|
|
}
|
|
|
|
public getApiKeyForProvider(provider: AIProvider): string {
|
|
switch (provider) {
|
|
case AIProvider.Claude:
|
|
return this.settings.apiKeys.claude;
|
|
case AIProvider.OpenAI:
|
|
return this.settings.apiKeys.openai;
|
|
case AIProvider.Gemini:
|
|
return this.settings.apiKeys.gemini;
|
|
case AIProvider.Mistral:
|
|
return this.settings.apiKeys.mistral;
|
|
}
|
|
}
|
|
|
|
public async setApiKeyForProvider(provider: AIProvider, key: string) {
|
|
switch (provider) {
|
|
case AIProvider.Claude:
|
|
await this.updateSettings(settings => settings.apiKeys.claude = key);
|
|
break;
|
|
case AIProvider.OpenAI:
|
|
await this.updateSettings(settings => settings.apiKeys.openai = key);
|
|
break;
|
|
case AIProvider.Gemini:
|
|
await this.updateSettings(settings => settings.apiKeys.gemini = key);
|
|
break;
|
|
case AIProvider.Mistral:
|
|
await this.updateSettings(settings => settings.apiKeys.mistral = key);
|
|
break;
|
|
}
|
|
}
|
|
|
|
private async saveSettings() {
|
|
await this.plugin.saveData(this.settings);
|
|
const snapshot = JSON.stringify(this.settings);
|
|
if (this.settingsSnapshot !== snapshot) {
|
|
this.settingsSnapshot = snapshot;
|
|
for (const ref of this.subscriberRefs) {
|
|
const subscriber = ref.deref();
|
|
if (!subscriber) {
|
|
this.subscriberRefs.delete(ref);
|
|
continue;
|
|
}
|
|
await this.subscribers.get(subscriber)?.();
|
|
}
|
|
}
|
|
}
|
|
|
|
private ensureValidModels(): void {
|
|
void this.updateSettings(settings => {
|
|
let provider = settings.provider;
|
|
|
|
if (!isvalidProvider(provider)) {
|
|
provider = DEFAULT_SETTINGS.provider;
|
|
}
|
|
|
|
if (!isValidProviderModel(this.settings.model) || !modelMatchesProvider(this.settings.model, provider)) {
|
|
settings.model = DEFAULT_MODEL_BY_PROVIDER[provider];
|
|
}
|
|
|
|
if (!isValidProviderModel(this.settings.planningModel) || !modelMatchesProvider(this.settings.planningModel, provider)) {
|
|
settings.planningModel = DEFAULT_PLANNING_MODEL_BY_PROVIDER[provider];
|
|
}
|
|
|
|
if (!isValidProviderModel(this.settings.quickActionModel) || !modelMatchesProvider(this.settings.quickActionModel, provider)) {
|
|
settings.quickActionModel = DEFAULT_MODEL_BY_PROVIDER[provider];
|
|
}
|
|
});
|
|
}
|
|
|
|
} |