import { Plugin, WorkspaceLeaf, App, Setting, Notice, PluginSettingTab } from 'obsidian'; import { ChatView } from './chat-view'; import { DEFAULT_SETTINGS } from './constants'; import { SemanticCacheService } from './semantic-cache'; import { PluginSettings } from './types'; export default class OllamaPlugin extends Plugin { settings: PluginSettings = DEFAULT_SETTINGS; semanticCache?: SemanticCacheService; async onload() { await this.loadSettings(); // Register the chat view this.registerView( 'ollama-chat-view', (leaf: WorkspaceLeaf) => new ChatView(leaf, this.settings) ); // Add a ribbon icon in the left sidebar this.addRibbonIcon('bot', 'Open Ollama Chat', async () => { await this.activateChatView(); }); // Add a command to open the chat view this.addCommand({ id: 'open-ollama-chat', name: 'Open Ollama Chat', callback: async () => { await this.activateChatView(); }, }); // Add a command to clear the semantic cache this.addCommand({ id: 'clear-semantic-cache', name: 'Clear Semantic Cache', callback: async () => { await this.clearSemanticCache(); new Notice('Semantic cache cleared.'); }, }); // Add a settings tab this.addSettingTab(new OllamaSettingTab(this.app, this)); // Initialize the semantic cache if (this.settings.cacheConfig) { this.semanticCache = new SemanticCacheService( this.settings.ollamaUrl, this.settings.cacheConfig ); try { await this.semanticCache.initialize(); } catch { new Notice('Semantic cache initialization failed. Check console for details.'); } } } // eslint-disable-next-line @typescript-eslint/no-misused-promises onunload() { // Clean up any active semantic cache resources on plugin unload // Using fire-and-forget pattern since onunload cannot be async per Obsidian API if (this.semanticCache) { void this.semanticCache.clearCache(); } // No explicit unregisterView needed; relying on Obsidian lifecycle management. } async loadSettings() { const loadedSettings = ((await this.loadData()) ?? {}) as Partial; this.settings = Object.assign({}, DEFAULT_SETTINGS, loadedSettings); } async saveSettings() { await this.saveData(this.settings); } async activateChatView() { const existing = this.app.workspace.getLeavesOfType('ollama-chat-view'); if (existing.length > 0) { await this.app.workspace.revealLeaf(existing[0]); } else { const leaf = this.app.workspace.getRightLeaf(false); if (leaf) { await leaf.setViewState({ type: 'ollama-chat-view', active: true, }); } } } async clearSemanticCache() { if (this.semanticCache) { await this.semanticCache.clearCache(); } } notifyChatViews() { const leaves = this.app.workspace.getLeavesOfType('ollama-chat-view'); leaves.forEach((leaf) => { if (leaf.view instanceof ChatView) { leaf.view.updateSettings(this.settings); } }); } } class OllamaSettingTab extends PluginSettingTab { plugin: OllamaPlugin; constructor(app: App, plugin: OllamaPlugin) { super(app, plugin); this.plugin = plugin; } display() { const { containerEl } = this; containerEl.empty(); containerEl.createEl('h2', { text: 'Ollama Settings' }); new Setting(containerEl) .setName('Ollama URL') .setDesc('URL for your Ollama instance (default: http://localhost:11434)') .addText((text) => text.setValue(this.plugin.settings.ollamaUrl).onChange(async (value) => { this.plugin.settings.ollamaUrl = value; await this.plugin.saveSettings(); }) ); new Setting(containerEl) .setName('Model') .setDesc('Ollama model to use (default: llama3)') .addText((text) => text.setValue(this.plugin.settings.model).onChange(async (value) => { this.plugin.settings.model = value; await this.plugin.saveSettings(); }) ); new Setting(containerEl) .setName('Vault Search Limit') .setDesc('Maximum number of vault entries to include in context (default: 3)') .addText((text) => text.setValue(String(this.plugin.settings.vaultSearchLimit)).onChange(async (value) => { const parsed = parseInt(value); if (!isNaN(parsed) && parsed > 0) { this.plugin.settings.vaultSearchLimit = parsed; await this.plugin.saveSettings(); } else { new Notice('Vault search limit must be a positive integer.'); } }) ); new Setting(containerEl) .setName('Max Message History') .setDesc('Maximum number of messages to keep in conversation history (default: 50)') .addText((text) => text.setValue(String(this.plugin.settings.maxMessageHistory)).onChange(async (value) => { const parsed = parseInt(value); if (!isNaN(parsed) && parsed > 0) { this.plugin.settings.maxMessageHistory = parsed; await this.plugin.saveSettings(); } else { new Notice('Max message history must be a positive integer.'); } }) ); new Setting(containerEl) .setName('Enable Semantic Cache') .setDesc('Use semantic cache to store and retrieve previous responses') .addToggle((toggle) => toggle.setValue(this.plugin.settings.cacheConfig.enabled).onChange(async (value) => { this.plugin.settings.cacheConfig.enabled = value; await this.plugin.saveSettings(); this.plugin.notifyChatViews(); }) ); new Setting(containerEl) .setName('ChromaDB URL') .setDesc('URL for your ChromaDB instance (default: http://localhost:8000)') .addText((text) => text .setValue(this.plugin.settings.cacheConfig.chromaURL || 'http://localhost:8000') .onChange(async (value) => { const trimmed = value.trim(); // Ensure a valid-looking URL; fall back to default if empty or malformed this.plugin.settings.cacheConfig.chromaURL = trimmed && trimmed.includes('://') ? trimmed : 'http://localhost:8000'; await this.plugin.saveSettings(); }) ); new Setting(containerEl) .setName('Cache Embedding Model') .setDesc('Ollama model used to generate embeddings for the semantic cache') .addText((text) => text.setValue(this.plugin.settings.cacheConfig.embeddingModel).onChange(async (value) => { this.plugin.settings.cacheConfig.embeddingModel = value; await this.plugin.saveSettings(); this.plugin.notifyChatViews(); }) ); new Setting(containerEl) .setName('Cache Similarity Threshold') .setDesc( 'Minimum cosine similarity (0–1) for a cache hit. Higher values require closer matches.' ) .addText((text) => text .setValue(String(this.plugin.settings.cacheConfig.similarityThreshold)) .onChange(async (value) => { const parsed = parseFloat(value); if (!isNaN(parsed) && parsed >= 0 && parsed <= 1) { this.plugin.settings.cacheConfig.similarityThreshold = parsed; await this.plugin.saveSettings(); } else { new Notice('Similarity threshold must be a number between 0 and 1.'); } }) ); new Setting(containerEl) .setName('Clear Semantic Cache') .setDesc('Delete all cached responses from ChromaDB') .addButton((button) => button.setButtonText('Clear Cache').onClick(async () => { try { await this.plugin.clearSemanticCache(); new Notice('Semantic cache cleared.'); } catch { new Notice('Failed to clear semantic cache. Is ChromaDB running?'); } }) ); } hide() { // Clear the container to prevent duplicate elements this.containerEl.empty(); } }