Files
obsidian_ollama/src/main.ts
T
fegger 96f201bf3f Add dual-model support with separate chat and agent models
Split the single model setting into `chatModel` and `agentModel` to allow
using different LLMs for conversational modes (Ask, Research) versus
agentic modes (Edit, Organize, Workflow, auto-organizer). Defaults are
`deepseek-v4-flash` for chat and `glm-5.1` for agents.

Includes backward compatibility migration from legacy `model` field,
updated settings UI, per-mode tool filtering via new `agent-modes.ts`
configs, and vault search scoring improvements (exact phrase, recency,
filename bonuses).
2026-05-21 09:00:11 +02:00

1090 lines
38 KiB
TypeScript
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Plugin, WorkspaceLeaf, App, Setting, Notice, PluginSettingTab, TFile } from 'obsidian';
import { ChatView } from './chat-view';
import { DEFAULT_SETTINGS } from './constants';
import { SemanticCacheService } from './semantic-cache';
import { VaultVectorStore } from './vault-vector-store';
import { VaultIndexer } from './vault-indexer';
import { AutoTagger, AutoLinker } from './auto-organizer';
import { PluginSettings, StructuredMemoryData, ToolTelemetryData } from './types';
import { Logger } from './utils';
import { ALL_AGENT_MODES, getAgentModeLabel } from './agent-modes';
import { AgentMode } from './types';
import { StructuredMemoryManager, createDefaultStructuredMemoryData } from './structured-memory';
import { TelemetryManager, createDefaultToolTelemetryData } from './tool-telemetry';
export default class OllamaPlugin extends Plugin {
settings: PluginSettings = DEFAULT_SETTINGS;
semanticCache?: SemanticCacheService;
vaultVectorStore?: VaultVectorStore;
autoTagger?: AutoTagger;
autoLinker?: AutoLinker;
structuredMemoryManager?: StructuredMemoryManager;
telemetryManager?: TelemetryManager;
private indexingAbortController?: AbortController;
private currentIndexingPromise?: Promise<void>;
async onload() {
await this.loadSettings();
// Initialize vault vector store if enabled
if (this.settings.vaultIndexConfig.enabled) {
await this.initializeVaultVectorStore();
}
// Register the chat view
this.registerView(
'ollama-chat-view',
(leaf: WorkspaceLeaf) =>
new ChatView(
leaf,
this.settings,
this.vaultVectorStore,
this.structuredMemoryManager,
this.telemetryManager
)
);
// 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 command to clear the vault index
this.addCommand({
id: 'clear-vault-index',
name: 'Clear Vault Index',
callback: async () => {
await this.clearVaultIndex();
new Notice('Vault index cleared.');
},
});
// Add a command to rebuild the vault index
this.addCommand({
id: 'rebuild-vault-index',
name: 'Rebuild Vault Index',
callback: async () => {
new Notice('Rebuilding vault index...');
await this.rebuildVaultIndex();
new Notice('Vault index rebuilt.');
},
});
// Add a command to auto-tag untagged notes
this.addCommand({
id: 'auto-tag-notes',
name: 'Auto-Tag Untagged Notes',
callback: () => {
this.initializeAutoOrganizer();
if (this.autoTagger) {
new Notice('Auto-tagging untagged notes...');
void this.autoTagger.run();
}
},
});
// Add a command to auto-link related notes
this.addCommand({
id: 'auto-link-notes',
name: 'Auto-Link Related Notes',
callback: () => {
this.initializeAutoOrganizer();
if (this.autoLinker) {
new Notice('Auto-linking related notes...');
void this.autoLinker.run();
}
},
});
// Add a command to clear the structured memory
this.addCommand({
id: 'clear-structured-memory',
name: 'Clear Structured Memory',
callback: async () => {
this.structuredMemoryManager?.clearAll();
await this.saveSettings();
new Notice('Structured memory cleared.');
},
});
// Add a command to clear tool telemetry
this.addCommand({
id: 'clear-tool-telemetry',
name: 'Clear Tool Telemetry',
callback: async () => {
this.telemetryManager?.clear();
await this.saveSettings();
new Notice('Tool telemetry cleared.');
},
});
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.');
}
}
// Set up vault event listeners for incremental indexing
this.registerVaultEventListeners();
}
// eslint-disable-next-line @typescript-eslint/no-misused-promises
onunload() {
// Cancel any ongoing indexing
this.cancelBackgroundIndexing();
// 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 data = ((await this.loadData()) ?? {}) as Record<string, unknown>;
// Backward compatibility: old flat format vs new nested format
const loadedSettings = (data.settings ?? data) as Partial<PluginSettings>;
this.settings = Object.assign({}, DEFAULT_SETTINGS, loadedSettings);
const legacyModel = loadedSettings.model ?? DEFAULT_SETTINGS.model;
this.settings.chatModel = loadedSettings.chatModel ?? legacyModel;
this.settings.agentModel = loadedSettings.agentModel ?? legacyModel;
this.settings.model = this.settings.chatModel;
const memoryData: StructuredMemoryData =
(data.structuredMemory as StructuredMemoryData | undefined) ??
createDefaultStructuredMemoryData();
this.structuredMemoryManager = new StructuredMemoryManager(
this.settings.structuredMemoryConfig,
memoryData
);
const telemetryData: ToolTelemetryData =
(data.toolTelemetry as ToolTelemetryData | undefined) ?? createDefaultToolTelemetryData();
this.telemetryManager = new TelemetryManager(this.settings.toolTelemetryConfig, telemetryData);
}
async saveSettings() {
await this.saveData({
settings: this.settings,
structuredMemory:
this.structuredMemoryManager?.getData() ?? createDefaultStructuredMemoryData(),
toolTelemetry: this.telemetryManager?.getData() ?? createDefaultToolTelemetryData(),
});
}
initializeAutoOrganizer(): void {
this.autoTagger = new AutoTagger(
this.app.vault,
this.app,
this.settings.ollamaUrl,
this.settings.agentModel,
this.settings.autoTagConfig
);
if (!this.autoLinker) {
const vaultIndexer = new VaultIndexer(this.app.vault, undefined, this.vaultVectorStore);
this.autoLinker = new AutoLinker(
this.app.vault,
vaultIndexer,
this.settings.autoLinkConfig,
this.settings.autoLinkConfig.targetFolder
);
} else {
this.autoLinker.updateConfig(this.settings.autoLinkConfig);
}
}
async initializeVaultVectorStore(): Promise<void> {
// Abort any ongoing indexing before re-initializing
this.cancelBackgroundIndexing();
await this.awaitBackgroundIndexing();
this.vaultVectorStore = new VaultVectorStore(
this.settings.ollamaUrl,
this.settings.vaultIndexConfig
);
try {
await this.vaultVectorStore.initialize();
// Run background indexing
this.currentIndexingPromise = this.backgroundIndexVault();
void this.currentIndexingPromise
.catch((err) => {
Logger.warn(`Background vault indexing failed: ${String(err)}`, 'main');
})
.finally(() => {
this.currentIndexingPromise = undefined;
this.indexingAbortController = undefined;
});
} catch {
new Notice('Vault vector store initialization failed. Check console for details.');
}
}
private cancelBackgroundIndexing(): void {
if (this.indexingAbortController) {
this.indexingAbortController.abort();
}
}
private async awaitBackgroundIndexing(): Promise<void> {
if (this.currentIndexingPromise) {
try {
await this.currentIndexingPromise;
} catch {
// ignore errors from cancelled indexing
}
}
}
async backgroundIndexVault(): Promise<void> {
if (!this.vaultVectorStore) return;
this.indexingAbortController = new AbortController();
const signal = this.indexingAbortController.signal;
const files = this.app.vault.getMarkdownFiles();
Logger.info(`Starting background vault indexing for ${files.length} files...`, 'main');
let indexed = 0;
const BATCH_SIZE = 1;
const DELAY_MS = 500;
for (let i = 0; i < files.length; i += BATCH_SIZE) {
if (signal.aborted) {
Logger.info('Vault indexing cancelled.', 'main');
return;
}
const batch = files.slice(i, i + BATCH_SIZE);
// Process files sequentially to avoid concurrent embedding requests
for (const file of batch) {
if (signal.aborted) break;
try {
const content = await this.app.vault.read(file);
if (signal.aborted) break;
const cache = this.app.metadataCache.getFileCache(file);
await this.vaultVectorStore.indexFile(file, content, cache ?? undefined);
indexed++;
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
Logger.warn(`Failed to index ${file.path}: ${errorMessage}`, 'main');
}
}
// Delay between batches to avoid overloading Ollama
if (i + BATCH_SIZE < files.length) {
await new Promise((resolve) => setTimeout(resolve, DELAY_MS));
}
}
if (!signal.aborted) {
this.settings.lastIndexTime = Date.now();
await this.saveSettings();
Logger.info(`Vault indexing complete: ${indexed}/${files.length} files indexed.`, 'main');
new Notice(`Vault index updated: ${indexed} files indexed.`);
}
}
async rebuildVaultIndex(): Promise<void> {
// Cancel and await any ongoing indexing before clearing
this.cancelBackgroundIndexing();
await this.awaitBackgroundIndexing();
if (this.vaultVectorStore) {
await this.vaultVectorStore.clearIndex();
await this.vaultVectorStore.initialize();
}
this.currentIndexingPromise = this.backgroundIndexVault();
void this.currentIndexingPromise
.catch((err) => {
Logger.warn(`Rebuild vault indexing failed: ${String(err)}`, 'main');
})
.finally(() => {
this.currentIndexingPromise = undefined;
this.indexingAbortController = undefined;
});
}
async clearVaultIndex(): Promise<void> {
if (this.vaultVectorStore) {
await this.vaultVectorStore.clearIndex();
}
}
registerVaultEventListeners(): void {
// Listen for file creation
this.registerEvent(
this.app.vault.on('create', (file) => {
if (file instanceof TFile && file.extension === 'md' && this.vaultVectorStore) {
void this.app.vault.read(file).then((content) => {
// Skip if a full rebuild is in progress to avoid race conditions
if (!this.currentIndexingPromise) {
void this.vaultVectorStore?.indexFile(file, content);
}
});
}
})
);
// Listen for file modification
this.registerEvent(
this.app.vault.on('modify', (file) => {
if (file instanceof TFile && file.extension === 'md' && this.vaultVectorStore) {
void this.app.vault.read(file).then((content) => {
if (!this.currentIndexingPromise) {
void this.vaultVectorStore?.indexFile(file, content);
}
});
}
})
);
// Listen for file deletion
this.registerEvent(
this.app.vault.on('delete', (file) => {
if (file instanceof TFile && file.extension === 'md' && this.vaultVectorStore) {
void this.vaultVectorStore.deleteFile(file.path);
}
})
);
// Listen for file renames
this.registerEvent(
this.app.vault.on('rename', (file, oldPath) => {
if (file instanceof TFile && file.extension === 'md' && this.vaultVectorStore) {
void this.vaultVectorStore.deleteFile(oldPath);
void this.app.vault.read(file).then((content) => {
if (!this.currentIndexingPromise) {
void this.vaultVectorStore?.indexFile(file, content);
}
});
}
})
);
}
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);
leaf.view.setVectorStore(this.vaultVectorStore);
}
});
}
}
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('Chat Model')
.setDesc('Model for normal chat, Ask mode, and Research mode (default: deepseek-v4-flash)')
.addText((text) =>
text.setValue(this.plugin.settings.chatModel).onChange(async (value) => {
this.plugin.settings.chatModel = value.trim();
this.plugin.settings.model = this.plugin.settings.chatModel;
await this.plugin.saveSettings();
this.plugin.notifyChatViews();
})
);
new Setting(containerEl)
.setName('Agent Model')
.setDesc('Model for Edit, Organize, Workflow, and auto-organizer tasks (default: glm-5.1)')
.addText((text) =>
text.setValue(this.plugin.settings.agentModel).onChange(async (value) => {
this.plugin.settings.agentModel = value.trim();
await this.plugin.saveSettings();
this.plugin.notifyChatViews();
})
);
new Setting(containerEl)
.setName('Vault Search Limit')
.setDesc('Maximum number of vault entries to include in context (default: 5)')
.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 Context Length')
.setDesc('Maximum characters of vault content to send to the AI per message (default: 8000)')
.addText((text) =>
text.setValue(String(this.plugin.settings.maxContextLength)).onChange(async (value) => {
const parsed = parseInt(value);
if (!isNaN(parsed) && parsed > 0) {
this.plugin.settings.maxContextLength = parsed;
await this.plugin.saveSettings();
} else {
new Notice('Max context length 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.');
}
})
);
// Agent Mode Setting
containerEl.createEl('h3', { text: 'Agent Mode' });
containerEl.createEl('p', {
text: 'Default chat mode that controls available tools and system behavior.',
});
new Setting(containerEl)
.setName('Default Agent Mode')
.setDesc('Select the default mode for new chat sessions.')
.addDropdown((dropdown) => {
for (const mode of ALL_AGENT_MODES) {
dropdown.addOption(mode, getAgentModeLabel(mode));
}
dropdown.setValue(this.plugin.settings.agentMode ?? 'ask');
dropdown.onChange(async (value) => {
this.plugin.settings.agentMode = value as AgentMode;
await this.plugin.saveSettings();
this.plugin.notifyChatViews();
});
});
// Vault Index Settings
containerEl.createEl('h3', { text: 'Vault Semantic Index' });
new Setting(containerEl)
.setName('Enable Vault Semantic Index')
.setDesc(
'Automatically index vault notes into a vector database for semantic/RAG search. Requires ChromaDB and an embedding model.'
)
.addToggle((toggle) =>
toggle.setValue(this.plugin.settings.vaultIndexConfig.enabled).onChange(async (value) => {
this.plugin.settings.vaultIndexConfig.enabled = value;
await this.plugin.saveSettings();
this.plugin.notifyChatViews();
if (value) {
new Notice('Vault semantic index enabled. Rebuilding index...');
await this.plugin.initializeVaultVectorStore();
await this.plugin.rebuildVaultIndex();
} else {
await this.plugin.clearVaultIndex();
this.plugin.vaultVectorStore = undefined;
this.plugin.notifyChatViews();
}
})
);
new Setting(containerEl)
.setName('Vault Index ChromaDB URL')
.setDesc(
'URL for your ChromaDB instance used for the vault index (default: http://localhost:8000)'
)
.addText((text) =>
text
.setValue(this.plugin.settings.vaultIndexConfig.chromaURL || 'http://localhost:8000')
.onChange(async (value) => {
const trimmed = value.trim();
this.plugin.settings.vaultIndexConfig.chromaURL =
trimmed && trimmed.includes('://') ? trimmed : 'http://localhost:8000';
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName('Vault Index Embedding Model')
.setDesc(
'Ollama model used to generate embeddings for vault notes (default: nomic-embed-text)'
)
.addText((text) =>
text
.setValue(this.plugin.settings.vaultIndexConfig.embeddingModel)
.onChange(async (value) => {
this.plugin.settings.vaultIndexConfig.embeddingModel = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName('Vault Index Similarity Threshold')
.setDesc(
'Minimum cosine similarity (01) for a vault search hit. Higher values require closer matches (default: 0.75).'
)
.addText((text) =>
text
.setValue(String(this.plugin.settings.vaultIndexConfig.similarityThreshold))
.onChange(async (value) => {
const parsed = parseFloat(value);
if (!isNaN(parsed) && parsed >= 0 && parsed <= 1) {
this.plugin.settings.vaultIndexConfig.similarityThreshold = parsed;
await this.plugin.saveSettings();
} else {
new Notice('Similarity threshold must be a number between 0 and 1.');
}
})
);
new Setting(containerEl)
.setName('Rebuild Vault Index')
.setDesc('Delete and rebuild the entire vault semantic index')
.addButton((button) =>
button.setButtonText('Rebuild Index').onClick(async () => {
try {
new Notice('Rebuilding vault index...');
await this.plugin.rebuildVaultIndex();
new Notice('Vault index rebuilt.');
} catch {
new Notice('Failed to rebuild vault index. Is ChromaDB running?');
}
})
);
new Setting(containerEl)
.setName('Clear Vault Index')
.setDesc('Delete all indexed vault notes from ChromaDB')
.addButton((button) =>
button.setButtonText('Clear Index').onClick(async () => {
try {
await this.plugin.clearVaultIndex();
new Notice('Vault index cleared.');
} catch {
new Notice('Failed to clear vault index. Is ChromaDB running?');
}
})
);
// Semantic Cache Settings
containerEl.createEl('h3', { text: 'Semantic Cache' });
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 (01) 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?');
}
})
);
// Auto-Organize Settings
containerEl.createEl('h3', { text: 'Auto-Organize' });
// Auto-Tag Settings
containerEl.createEl('h4', { text: 'Auto-Tagging' });
new Setting(containerEl)
.setName('Enable Auto-Tagging')
.setDesc('Use AI to automatically suggest and apply tags to untagged notes')
.addToggle((toggle) =>
toggle.setValue(this.plugin.settings.autoTagConfig.enabled).onChange(async (value) => {
this.plugin.settings.autoTagConfig.enabled = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName('Max Tags Per Note')
.setDesc('Maximum number of tags to generate for each note (default: 5)')
.addText((text) =>
text
.setValue(String(this.plugin.settings.autoTagConfig.maxTagsPerNote))
.onChange(async (value) => {
const parsed = parseInt(value);
if (!isNaN(parsed) && parsed > 0 && parsed <= 20) {
this.plugin.settings.autoTagConfig.maxTagsPerNote = parsed;
await this.plugin.saveSettings();
} else {
new Notice('Max tags must be between 1 and 20.');
}
})
);
new Setting(containerEl)
.setName('Min Note Length')
.setDesc('Minimum character length for a note to be tagged (default: 50)')
.addText((text) =>
text
.setValue(String(this.plugin.settings.autoTagConfig.minNoteLength))
.onChange(async (value) => {
const parsed = parseInt(value);
if (!isNaN(parsed) && parsed >= 0) {
this.plugin.settings.autoTagConfig.minNoteLength = parsed;
await this.plugin.saveSettings();
} else {
new Notice('Min note length must be a non-negative integer.');
}
})
);
new Setting(containerEl)
.setName('Max Note Length')
.setDesc('Maximum characters of content sent to the model for tagging (default: 8000)')
.addText((text) =>
text
.setValue(String(this.plugin.settings.autoTagConfig.maxNoteLength))
.onChange(async (value) => {
const parsed = parseInt(value);
if (!isNaN(parsed) && parsed > 0) {
this.plugin.settings.autoTagConfig.maxNoteLength = parsed;
await this.plugin.saveSettings();
} else {
new Notice('Max note length must be a positive integer.');
}
})
);
new Setting(containerEl)
.setName('Normalize Tags')
.setDesc(
'Normalize generated tags against existing vault tag vocabulary (e.g., prefer "machine-learning" over "machine learning")'
)
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.autoTagConfig.normalizeTags)
.onChange(async (value) => {
this.plugin.settings.autoTagConfig.normalizeTags = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName('Target Folder (Auto-Tag)')
.setDesc('Only auto-tag notes inside this folder path. Leave empty for all notes.')
.addText((text) =>
text.setValue(this.plugin.settings.autoTagConfig.targetFolder).onChange(async (value) => {
this.plugin.settings.autoTagConfig.targetFolder = value.trim();
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName('Tag Prompt Template')
.setDesc(
'Prompt template for tag generation. Use {{maxTags}}, {{title}}, {{content}} as placeholders.'
)
.addTextArea((text) =>
text
.setValue(this.plugin.settings.autoTagConfig.tagPromptTemplate)
.onChange(async (value) => {
this.plugin.settings.autoTagConfig.tagPromptTemplate = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName('Run Auto-Tagging Now')
.setDesc('Process all untagged notes and generate tags')
.addButton((button) =>
button.setButtonText('Auto-Tag Notes').onClick(async () => {
try {
this.plugin.initializeAutoOrganizer();
if (this.plugin.autoTagger) {
await this.plugin.autoTagger.run();
}
} catch {
new Notice('Auto-tagging failed. Check console for details.');
}
})
);
// Auto-Link Settings
containerEl.createEl('h4', { text: 'Auto-Linking' });
new Setting(containerEl)
.setName('Enable Auto-Linking')
.setDesc('Add "Related Notes" sections to notes based on semantic similarity')
.addToggle((toggle) =>
toggle.setValue(this.plugin.settings.autoLinkConfig.enabled).onChange(async (value) => {
this.plugin.settings.autoLinkConfig.enabled = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName('Max Links Per Note')
.setDesc('Maximum number of related notes to link (default: 3)')
.addText((text) =>
text
.setValue(String(this.plugin.settings.autoLinkConfig.maxLinksPerNote))
.onChange(async (value) => {
const parsed = parseInt(value);
if (!isNaN(parsed) && parsed > 0 && parsed <= 10) {
this.plugin.settings.autoLinkConfig.maxLinksPerNote = parsed;
await this.plugin.saveSettings();
} else {
new Notice('Max links must be between 1 and 10.');
}
})
);
new Setting(containerEl)
.setName('Target Folder (Auto-Link)')
.setDesc(
'Only add related links to notes inside this folder path. Leave empty for all notes.'
)
.addText((text) =>
text.setValue(this.plugin.settings.autoLinkConfig.targetFolder).onChange(async (value) => {
this.plugin.settings.autoLinkConfig.targetFolder = value.trim();
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName('Dry Run Mode')
.setDesc('Preview proposed link changes without applying them')
.addToggle((toggle) =>
toggle.setValue(this.plugin.settings.autoLinkConfig.dryRun).onChange(async (value) => {
this.plugin.settings.autoLinkConfig.dryRun = value;
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName('Auto-Link Similarity Threshold')
.setDesc('Minimum similarity score for notes to be considered related (default: 0.6)')
.addText((text) =>
text
.setValue(String(this.plugin.settings.autoLinkConfig.similarityThreshold))
.onChange(async (value) => {
const parsed = parseFloat(value);
if (!isNaN(parsed) && parsed >= 0 && parsed <= 1) {
this.plugin.settings.autoLinkConfig.similarityThreshold = parsed;
await this.plugin.saveSettings();
} else {
new Notice('Similarity threshold must be between 0 and 1.');
}
})
);
new Setting(containerEl)
.setName('Run Auto-Linking Now')
.setDesc('Process all notes and add related note links')
.addButton((button) =>
button.setButtonText('Auto-Link Notes').onClick(async () => {
try {
this.plugin.initializeAutoOrganizer();
if (this.plugin.autoLinker) {
await this.plugin.autoLinker.run();
}
} catch {
new Notice('Auto-linking failed. Check console for details.');
}
})
);
// Structured Memory Settings
containerEl.createEl('h3', { text: 'Structured Memory' });
containerEl.createEl('p', {
text: 'Persist conversation summaries, user preferences, and learned facts across sessions.',
});
new Setting(containerEl)
.setName('Enable Structured Memory')
.setDesc('Inject remembered context from past sessions into the system prompt.')
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.structuredMemoryConfig.enabled)
.onChange(async (value) => {
this.plugin.settings.structuredMemoryConfig.enabled = value;
this.plugin.structuredMemoryManager!.updateConfig(
this.plugin.settings.structuredMemoryConfig
);
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName('Max Conversation Summaries')
.setDesc('Maximum number of past conversation summaries to retain (default: 10).')
.addText((text) =>
text
.setValue(String(this.plugin.settings.structuredMemoryConfig.maxSummaries))
.onChange(async (value) => {
const parsed = parseInt(value);
if (!isNaN(parsed) && parsed >= 0 && parsed <= 100) {
this.plugin.settings.structuredMemoryConfig.maxSummaries = parsed;
this.plugin.structuredMemoryManager!.updateConfig(
this.plugin.settings.structuredMemoryConfig
);
await this.plugin.saveSettings();
} else {
new Notice('Max summaries must be between 0 and 100.');
}
})
);
new Setting(containerEl)
.setName('Max User Preferences')
.setDesc('Maximum number of user preferences to retain (default: 20).')
.addText((text) =>
text
.setValue(String(this.plugin.settings.structuredMemoryConfig.maxPreferences))
.onChange(async (value) => {
const parsed = parseInt(value);
if (!isNaN(parsed) && parsed >= 0 && parsed <= 200) {
this.plugin.settings.structuredMemoryConfig.maxPreferences = parsed;
this.plugin.structuredMemoryManager!.updateConfig(
this.plugin.settings.structuredMemoryConfig
);
await this.plugin.saveSettings();
} else {
new Notice('Max preferences must be between 0 and 200.');
}
})
);
new Setting(containerEl)
.setName('Max Learned Facts')
.setDesc('Maximum number of learned facts to retain (default: 50).')
.addText((text) =>
text
.setValue(String(this.plugin.settings.structuredMemoryConfig.maxFacts))
.onChange(async (value) => {
const parsed = parseInt(value);
if (!isNaN(parsed) && parsed >= 0 && parsed <= 500) {
this.plugin.settings.structuredMemoryConfig.maxFacts = parsed;
this.plugin.structuredMemoryManager!.updateConfig(
this.plugin.settings.structuredMemoryConfig
);
await this.plugin.saveSettings();
} else {
new Notice('Max facts must be between 0 and 500.');
}
})
);
new Setting(containerEl)
.setName('Clear Structured Memory')
.setDesc('Delete all stored conversation summaries, preferences, and facts.')
.addButton((button) =>
button.setButtonText('Clear Memory').onClick(async () => {
this.plugin.structuredMemoryManager!.clearAll();
await this.plugin.saveSettings();
new Notice('Structured memory cleared.');
})
);
// Tool Telemetry Settings
containerEl.createEl('h3', { text: 'Tool Telemetry' });
containerEl.createEl('p', {
text: 'Track which tools were called, which notes were searched, and LLM token usage.',
});
new Setting(containerEl)
.setName('Enable Tool Telemetry')
.setDesc('Record tool calls, searches, and LLM token counts for analysis.')
.addToggle((toggle) =>
toggle
.setValue(this.plugin.settings.toolTelemetryConfig.enabled)
.onChange(async (value) => {
this.plugin.settings.toolTelemetryConfig.enabled = value;
this.plugin.telemetryManager?.updateConfig(this.plugin.settings.toolTelemetryConfig);
await this.plugin.saveSettings();
})
);
new Setting(containerEl)
.setName('Max Telemetry Entries')
.setDesc('Maximum number of telemetry events to retain (default: 100).')
.addText((text) =>
text
.setValue(String(this.plugin.settings.toolTelemetryConfig.maxEntries))
.onChange(async (value) => {
const parsed = parseInt(value);
if (!isNaN(parsed) && parsed >= 0 && parsed <= 1000) {
this.plugin.settings.toolTelemetryConfig.maxEntries = parsed;
this.plugin.telemetryManager?.updateConfig(this.plugin.settings.toolTelemetryConfig);
await this.plugin.saveSettings();
} else {
new Notice('Max entries must be between 0 and 1000.');
}
})
);
new Setting(containerEl)
.setName('Clear Tool Telemetry')
.setDesc('Delete all recorded tool telemetry.')
.addButton((button) =>
button.setButtonText('Clear Telemetry').onClick(async () => {
this.plugin.telemetryManager?.clear();
await this.plugin.saveSettings();
new Notice('Tool telemetry cleared.');
})
);
// Recent Telemetry Summary
const recentEntries = this.plugin.telemetryManager?.getRecentEntries(10) ?? [];
if (recentEntries.length > 0) {
containerEl.createEl('h4', { text: 'Recent Activity' });
const telemetryList = containerEl.createEl('ul');
for (const entry of recentEntries) {
const li = telemetryList.createEl('li');
if (entry.type === 'tool_call') {
li.setText(
`${new Date(entry.timestamp).toLocaleString()}: Tool "${entry.toolName}" — ${entry.success ? 'success' : 'failed'} (${entry.durationMs}ms)`
);
} else if (entry.type === 'llm_call') {
li.setText(
`${new Date(entry.timestamp).toLocaleString()}: LLM call — ${entry.totalTokens} tokens (${entry.durationMs}ms)`
);
} else if (entry.type === 'vault_search') {
li.setText(
`${new Date(entry.timestamp).toLocaleString()}: Search "${entry.query}" — ${entry.resultsCount} results`
);
}
}
}
}
hide() {
// Clear the container to prevent duplicate elements
this.containerEl.empty();
}
}