import { App, Notice, PluginSettingTab, Setting } from "obsidian"; import type MindnetCausalAssistantPlugin from "../main"; import { VocabularyLoader } from "../vocab/VocabularyLoader"; export class MindnetSettingTab extends PluginSettingTab { plugin: MindnetCausalAssistantPlugin; constructor(app: App, plugin: MindnetCausalAssistantPlugin) { super(app, plugin); this.plugin = plugin; } display(): void { const { containerEl } = this; containerEl.empty(); containerEl.createEl("h2", { text: "Mindnet Settings" }); // Edge vocabulary path new Setting(containerEl) .setName("Edge vocabulary path") .setDesc("Vault-relative path to the edge vocabulary markdown file") .addText((text) => text .setPlaceholder("_system/dictionary/edge_vocabulary.md") .setValue(this.plugin.settings.edgeVocabularyPath) .onChange(async (value) => { this.plugin.settings.edgeVocabularyPath = value; await this.plugin.saveSettings(); }) ) .addButton((button) => button .setButtonText("Validate") .setCta() .onClick(async () => { try { const text = await VocabularyLoader.loadText( this.app, this.plugin.settings.edgeVocabularyPath ); new Notice( `Edge vocabulary file found (${text.length} characters)` ); } catch (e) { const msg = e instanceof Error ? e.message : String(e); new Notice( `Failed to load edge vocabulary: ${msg}` ); } }) ); // Graph schema path new Setting(containerEl) .setName("Graph schema path") .setDesc("Vault-relative path to the graph schema markdown file") .addText((text) => text .setPlaceholder("_system/dictionary/graph_schema.md") .setValue(this.plugin.settings.graphSchemaPath) .onChange(async (value) => { this.plugin.settings.graphSchemaPath = value; await this.plugin.saveSettings(); }) ) .addButton((button) => button .setButtonText("Validate") .setCta() .onClick(async () => { try { const { GraphSchemaLoader } = await import("../schema/GraphSchemaLoader"); const isValid = await GraphSchemaLoader.validate( this.app, this.plugin.settings.graphSchemaPath ); if (isValid) { const schema = await GraphSchemaLoader.load( this.app, this.plugin.settings.graphSchemaPath ); const ruleCount = schema.schema.size; new Notice( `Graph schema file found (${ruleCount} source types)` ); } else { new Notice("Graph schema file not found or invalid"); } } catch (e) { const msg = e instanceof Error ? e.message : String(e); new Notice(`Failed to validate graph schema: ${msg}`); } }) ); // Strict mode toggle new Setting(containerEl) .setName("Strict mode") .setDesc("Enable strict validation mode") .addToggle((toggle) => toggle .setValue(this.plugin.settings.strictMode) .onChange(async (value) => { this.plugin.settings.strictMode = value; await this.plugin.saveSettings(); }) ); // Max hops number input new Setting(containerEl) .setName("Max hops") .setDesc("Maximum number of hops for graph traversal") .addText((text) => text .setPlaceholder("3") .setValue(String(this.plugin.settings.maxHops)) .onChange(async (value) => { const numValue = parseInt(value, 10); if (!isNaN(numValue) && numValue > 0) { this.plugin.settings.maxHops = numValue; await this.plugin.saveSettings(); } }) ); // Show canonical hints toggle new Setting(containerEl) .setName("Show canonical hints") .setDesc("Show INFO findings with canonical edge type resolution in lint results") .addToggle((toggle) => toggle .setValue(this.plugin.settings.showCanonicalHints) .onChange(async (value) => { this.plugin.settings.showCanonicalHints = value; await this.plugin.saveSettings(); }) ); // Chain direction dropdown new Setting(containerEl) .setName("Chain direction") .setDesc("Direction for chain traversal: forward, backward, or both") .addDropdown((dropdown) => dropdown .addOption("forward", "Forward") .addOption("backward", "Backward") .addOption("both", "Both") .setValue(this.plugin.settings.chainDirection) .onChange(async (value) => { if (value === "forward" || value === "backward" || value === "both") { this.plugin.settings.chainDirection = value; await this.plugin.saveSettings(); } }) ); // Interview config path new Setting(containerEl) .setName("Interview config path") .setDesc("Vault-relative path to the interview config YAML file") .addText((text) => text .setPlaceholder("_system/dictionary/interview_config.yaml") .setValue(this.plugin.settings.interviewConfigPath) .onChange(async (value) => { this.plugin.settings.interviewConfigPath = value; await this.plugin.saveSettings(); }) ) .addButton((button) => button .setButtonText("Validate") .setCta() .onClick(async () => { try { const { InterviewConfigLoader } = await import("../interview/InterviewConfigLoader"); const result = await InterviewConfigLoader.loadConfig( this.app, this.plugin.settings.interviewConfigPath ); if (result.errors.length > 0) { new Notice( `Interview config loaded with ${result.errors.length} error(s). Check console.` ); console.warn("Interview config errors:", result.errors); } else { new Notice( `Interview config file found (${result.config.profiles.length} profile(s))` ); } } catch (e) { const msg = e instanceof Error ? e.message : String(e); new Notice(`Failed to load interview config: ${msg}`); } }) ); // Auto-start interview toggle new Setting(containerEl) .setName("Auto-start interview on create") .setDesc("Automatically start interview wizard when creating a note from profile") .addToggle((toggle) => toggle .setValue(this.plugin.settings.autoStartInterviewOnCreate) .onChange(async (value) => { this.plugin.settings.autoStartInterviewOnCreate = value; await this.plugin.saveSettings(); }) ); // Intercept unresolved links toggle new Setting(containerEl) .setName("Intercept unresolved link clicks") .setDesc("Open profile selection when clicking unresolved internal links") .addToggle((toggle) => toggle .setValue(this.plugin.settings.interceptUnresolvedLinkClicks) .onChange(async (value) => { this.plugin.settings.interceptUnresolvedLinkClicks = value; await this.plugin.saveSettings(); }) ); // Auto-start interview on unresolved click toggle new Setting(containerEl) .setName("Auto-start interview on unresolved link click") .setDesc("Automatically start interview wizard when creating note from unresolved link") .addToggle((toggle) => toggle .setValue(this.plugin.settings.autoStartOnUnresolvedClick) .onChange(async (value) => { this.plugin.settings.autoStartOnUnresolvedClick = value; await this.plugin.saveSettings(); }) ); // Bypass modifier (Reading View) new Setting(containerEl) .setName("Bypass modifier (Reading View)") .setDesc("Modifier key to bypass unresolved link intercept in Reading View (Alt/Ctrl/Shift/None)") .addDropdown((dropdown) => dropdown .addOption("Alt", "Alt") .addOption("Ctrl", "Ctrl/Cmd") .addOption("Shift", "Shift") .addOption("None", "None") .setValue(this.plugin.settings.bypassModifier) .onChange(async (value) => { if (value === "Alt" || value === "Ctrl" || value === "Shift" || value === "None") { this.plugin.settings.bypassModifier = value; await this.plugin.saveSettings(); } }) ); // Editor follow modifier (Live Preview/Source) new Setting(containerEl) .setName("Editor follow modifier (Live Preview/Source)") .setDesc("Modifier key required to intercept unresolved links in editor (Alt/Ctrl/Shift/None). Default: Ctrl/Cmd") .addDropdown((dropdown) => dropdown .addOption("Alt", "Alt") .addOption("Ctrl", "Ctrl/Cmd") .addOption("Shift", "Shift") .addOption("None", "None") .setValue(this.plugin.settings.editorFollowModifier) .onChange(async (value) => { if (value === "Alt" || value === "Ctrl" || value === "Shift" || value === "None") { this.plugin.settings.editorFollowModifier = value; await this.plugin.saveSettings(); } }) ); // Wait for first modify after create new Setting(containerEl) .setName("Wait for first modify after create") .setDesc("Wait for Templater to modify file before starting wizard (recommended if using Templater)") .addToggle((toggle) => toggle .setValue(this.plugin.settings.waitForFirstModifyAfterCreate) .onChange(async (value) => { this.plugin.settings.waitForFirstModifyAfterCreate = value; await this.plugin.saveSettings(); }) ); // Modify timeout new Setting(containerEl) .setName("Modify timeout (ms)") .setDesc("Timeout in milliseconds for waiting for file modify event") .addText((text) => text .setPlaceholder("1200") .setValue(String(this.plugin.settings.waitForModifyTimeoutMs)) .onChange(async (value) => { const numValue = parseInt(value, 10); if (!isNaN(numValue) && numValue > 0) { this.plugin.settings.waitForModifyTimeoutMs = numValue; await this.plugin.saveSettings(); } }) ); // Debug logging new Setting(containerEl) .setName("Debug logging") .setDesc("Enable debug logging for unresolved link handling") .addToggle((toggle) => toggle .setValue(this.plugin.settings.debugLogging) .onChange(async (value) => { this.plugin.settings.debugLogging = value; await this.plugin.saveSettings(); }) ); // Adopt new notes in editor new Setting(containerEl) .setName("Adopt new notes in editor") .setDesc("Automatically adopt newly created notes in editor and convert to Mindnet format") .addToggle((toggle) => toggle .setValue(this.plugin.settings.adoptNewNotesInEditor) .onChange(async (value) => { this.plugin.settings.adoptNewNotesInEditor = value; await this.plugin.saveSettings(); }) ); // Adopt max chars new Setting(containerEl) .setName("Adopt max chars") .setDesc("Maximum content length to consider a note as adopt-candidate (default: 200)") .addText((text) => text .setPlaceholder("200") .setValue(String(this.plugin.settings.adoptMaxChars)) .onChange(async (value) => { const numValue = parseInt(value, 10); if (!isNaN(numValue) && numValue > 0) { this.plugin.settings.adoptMaxChars = numValue; await this.plugin.saveSettings(); } }) ); // Adopt confirm mode new Setting(containerEl) .setName("Adopt confirm mode") .setDesc("When to show adoption confirmation: always, onlyLowConfidence (skip for high-confidence), or never") .addDropdown((dropdown) => dropdown .addOption("always", "Always ask") .addOption("onlyLowConfidence", "Only for low confidence") .addOption("never", "Never ask") .setValue(this.plugin.settings.adoptConfirmMode) .onChange(async (value) => { if (value === "always" || value === "onlyLowConfidence" || value === "never") { this.plugin.settings.adoptConfirmMode = value; await this.plugin.saveSettings(); } }) ); // High confidence window new Setting(containerEl) .setName("High confidence window (ms)") .setDesc("Time window in milliseconds for high-confidence adoption (default: 3000)") .addText((text) => text .setPlaceholder("3000") .setValue(String(this.plugin.settings.highConfidenceWindowMs)) .onChange(async (value) => { const numValue = parseInt(value, 10); if (!isNaN(numValue) && numValue > 0) { this.plugin.settings.highConfidenceWindowMs = numValue; await this.plugin.saveSettings(); } }) ); // Semantic Mapping Builder section containerEl.createEl("h2", { text: "Semantic Mapping Builder" }); // Wrapper callout type new Setting(containerEl) .setName("Mapping wrapper callout type") .setDesc("Callout type for the mapping wrapper (e.g., 'abstract', 'info', 'note')") .addText((text) => text .setPlaceholder("abstract") .setValue(this.plugin.settings.mappingWrapperCalloutType) .onChange(async (value) => { this.plugin.settings.mappingWrapperCalloutType = value || "abstract"; await this.plugin.saveSettings(); }) ); // Wrapper title new Setting(containerEl) .setName("Mapping wrapper title") .setDesc("Title text for the mapping wrapper callout") .addText((text) => text .setPlaceholder("🕸️ Semantic Mapping") .setValue(this.plugin.settings.mappingWrapperTitle) .onChange(async (value) => { this.plugin.settings.mappingWrapperTitle = value || "🕸️ Semantic Mapping"; await this.plugin.saveSettings(); }) ); // Wrapper folded new Setting(containerEl) .setName("Mapping wrapper folded") .setDesc("Start with mapping wrapper callout folded (collapsed)") .addToggle((toggle) => toggle .setValue(this.plugin.settings.mappingWrapperFolded) .onChange(async (value) => { this.plugin.settings.mappingWrapperFolded = value; await this.plugin.saveSettings(); }) ); // Default edge type new Setting(containerEl) .setName("Default edge type") .setDesc("Default edge type for unmapped links (if unassignedHandling is 'defaultType')") .addText((text) => text .setPlaceholder("") .setValue(this.plugin.settings.defaultEdgeType) .onChange(async (value) => { this.plugin.settings.defaultEdgeType = value; await this.plugin.saveSettings(); }) ); // Unassigned handling new Setting(containerEl) .setName("Unassigned handling") .setDesc("How to handle links without existing mappings: prompt (interactive), none, defaultType, or advisor") .addDropdown((dropdown) => dropdown .addOption("prompt", "Prompt (interactive)") .addOption("none", "None (skip unmapped)") .addOption("defaultType", "Use default edge type") .addOption("advisor", "Use advisor (future: E1 engine)") .setValue(this.plugin.settings.unassignedHandling) .onChange(async (value) => { if (value === "prompt" || value === "none" || value === "defaultType" || value === "advisor") { this.plugin.settings.unassignedHandling = value; await this.plugin.saveSettings(); } }) ); // Allow overwrite existing mappings new Setting(containerEl) .setName("Allow overwrite existing mappings") .setDesc("If enabled, will prompt to confirm before overwriting existing edge type assignments") .addToggle((toggle) => toggle .setValue(this.plugin.settings.allowOverwriteExistingMappings) .onChange(async (value) => { this.plugin.settings.allowOverwriteExistingMappings = value; await this.plugin.saveSettings(); }) ); // Default notes folder new Setting(containerEl) .setName("Default notes folder") .setDesc("Default folder for new notes (vault-relative path, empty for root). Profile defaults take precedence.") .addText((text) => text .setPlaceholder("") .setValue(this.plugin.settings.defaultNotesFolder) .onChange(async (value) => { this.plugin.settings.defaultNotesFolder = value || ""; await this.plugin.saveSettings(); }) ); } }