From a6ce268b0411082ab557c01f5ffc4b084f916182 Mon Sep 17 00:00:00 2001 From: Lars Date: Sat, 17 Jan 2026 10:11:30 +0100 Subject: [PATCH] Enhance note adoption process with confidence evaluation and settings - Introduced a new function to evaluate adoption confidence based on file creation time and content criteria. - Updated the adoption confirmation logic to determine when to show confirmation based on user settings. - Added new settings for adoption confirmation mode and high-confidence time window in the settings interface. - Improved the user experience by providing clearer options for managing note adoption behavior. --- src/main.ts | 35 ++++- src/settings.ts | 4 + .../unresolvedLink/adoptConfidence.test.ts | 145 ++++++++++++++++++ src/ui/MindnetSettingTab.ts | 35 +++++ src/unresolvedLink/adoptHelpers.ts | 42 ++++- 5 files changed, 255 insertions(+), 6 deletions(-) create mode 100644 src/tests/unresolvedLink/adoptConfidence.test.ts diff --git a/src/main.ts b/src/main.ts index 9c05a5a..d7c8bfd 100644 --- a/src/main.ts +++ b/src/main.ts @@ -39,6 +39,7 @@ import { isAdoptCandidate, matchesPendingHint, mergeFrontmatter, + evaluateAdoptionConfidence, type PendingCreateHint, } from "./unresolvedLink/adoptHelpers"; import { AdoptNoteModal } from "./ui/AdoptNoteModal"; @@ -745,21 +746,31 @@ export default class MindnetCausalAssistantPlugin extends Plugin { return; } - // Check if matches pending hint (high confidence) - const highConfidence = matchesPendingHint(file, this.pendingCreateHint); + // Evaluate confidence level + const confidence = evaluateAdoptionConfidence( + file, + content, + this.settings.adoptMaxChars, + this.pendingCreateHint, + this.settings.highConfidenceWindowMs + ); if (this.settings.debugLogging) { console.log(`[Mindnet] Adopt candidate detected: ${file.path}`, { - highConfidence, + confidence, pendingHint: this.pendingCreateHint, + fileCtime: file.stat.ctime, + timeSinceCreation: Date.now() - file.stat.ctime, }); } // Clear pending hint (used once) this.pendingCreateHint = null; - // Show confirm modal if not high confidence - if (!highConfidence) { + // Determine if we need to show confirmation + const shouldShowConfirm = this.shouldShowAdoptionConfirm(confidence); + + if (shouldShowConfirm) { const adoptModal = new AdoptNoteModal(this.app, file.basename); const result = await adoptModal.show(); if (!result.adopt) { @@ -797,6 +808,20 @@ export default class MindnetCausalAssistantPlugin extends Plugin { } } + /** + * Determine if adoption confirmation should be shown based on mode and confidence. + */ + private shouldShowAdoptionConfirm(confidence: "high" | "low"): boolean { + if (this.settings.adoptConfirmMode === "always") { + return true; + } + if (this.settings.adoptConfirmMode === "never") { + return false; + } + // onlyLowConfidence: show only for low confidence + return confidence === "low"; + } + /** * Adopt a note: convert to Mindnet format and start wizard. */ diff --git a/src/settings.ts b/src/settings.ts index febea56..e726031 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -16,6 +16,8 @@ export interface MindnetSettings { debugLogging: boolean; // Enable debug logging for unresolved link handling adoptNewNotesInEditor: boolean; // Auto-adopt newly created notes in editor adoptMaxChars: number; // Max content length to consider note as adopt-candidate + adoptConfirmMode: "always" | "onlyLowConfidence" | "never"; // When to show adoption confirmation + highConfidenceWindowMs: number; // Time window in ms for high-confidence adoption // Semantic mapping builder settings mappingWrapperCalloutType: string; // default: "abstract" mappingWrapperTitle: string; // default: "🕸️ Semantic Mapping" @@ -44,6 +46,8 @@ export interface MindnetSettings { debugLogging: false, adoptNewNotesInEditor: true, adoptMaxChars: 200, + adoptConfirmMode: "onlyLowConfidence", + highConfidenceWindowMs: 3000, mappingWrapperCalloutType: "abstract", mappingWrapperTitle: "🕸️ Semantic Mapping", mappingWrapperFolded: true, diff --git a/src/tests/unresolvedLink/adoptConfidence.test.ts b/src/tests/unresolvedLink/adoptConfidence.test.ts new file mode 100644 index 0000000..c3f9ec7 --- /dev/null +++ b/src/tests/unresolvedLink/adoptConfidence.test.ts @@ -0,0 +1,145 @@ +import { describe, it, expect } from "vitest"; +import { + evaluateAdoptionConfidence, + type PendingCreateHint, +} from "../../unresolvedLink/adoptHelpers"; +import { TFile } from "obsidian"; + +describe("evaluateAdoptionConfidence", () => { + it("returns high confidence for recent file with matching hint", () => { + const file = { + basename: "test-note", + stat: { ctime: Date.now() - 1000 }, // 1 second ago + } as TFile; + const content = "Some content"; + const hint: PendingCreateHint = { + basename: "test-note", + sourcePath: "source.md", + timestamp: Date.now() - 500, + }; + + const result = evaluateAdoptionConfidence(file, content, 200, hint, 3000); + expect(result).toBe("high"); + }); + + it("returns low confidence for old file", () => { + const file = { + basename: "test-note", + stat: { ctime: Date.now() - 10000 }, // 10 seconds ago + } as TFile; + const content = "Some content"; + const hint: PendingCreateHint = { + basename: "test-note", + sourcePath: "source.md", + timestamp: Date.now() - 500, + }; + + const result = evaluateAdoptionConfidence(file, content, 200, hint, 3000); + expect(result).toBe("low"); + }); + + it("returns low confidence for file with id", () => { + const file = { + basename: "test-note", + stat: { ctime: Date.now() - 1000 }, + } as TFile; + const content = `--- +id: note_123 +--- +Body`; + const hint: PendingCreateHint = { + basename: "test-note", + sourcePath: "source.md", + timestamp: Date.now() - 500, + }; + + const result = evaluateAdoptionConfidence(file, content, 200, hint, 3000); + expect(result).toBe("low"); + }); + + it("returns low confidence for file exceeding maxChars", () => { + const file = { + basename: "test-note", + stat: { ctime: Date.now() - 1000 }, + } as TFile; + const content = "x".repeat(300); // Exceeds maxChars + const hint: PendingCreateHint = { + basename: "test-note", + sourcePath: "source.md", + timestamp: Date.now() - 500, + }; + + const result = evaluateAdoptionConfidence(file, content, 200, hint, 3000); + expect(result).toBe("low"); + }); + + it("returns high confidence for very recent file even without hint", () => { + const file = { + basename: "test-note", + stat: { ctime: Date.now() - 500 }, // Very recent (0.5 seconds) + } as TFile; + const content = "Some content"; + + const result = evaluateAdoptionConfidence(file, content, 200, null, 3000); + expect(result).toBe("high"); + }); + + it("returns low confidence for recent file without hint if not very recent", () => { + const file = { + basename: "test-note", + stat: { ctime: Date.now() - 2500 }, // 2.5 seconds ago (not very recent) + } as TFile; + const content = "Some content"; + + const result = evaluateAdoptionConfidence(file, content, 200, null, 3000); + expect(result).toBe("low"); + }); + + it("returns high confidence when hint matches even if file is slightly older", () => { + const file = { + basename: "test-note", + stat: { ctime: Date.now() - 2500 }, // 2.5 seconds ago + } as TFile; + const content = "Some content"; + const hint: PendingCreateHint = { + basename: "test-note", + sourcePath: "source.md", + timestamp: Date.now() - 2000, // Hint is 2 seconds ago, within window + }; + + const result = evaluateAdoptionConfidence(file, content, 200, hint, 3000); + expect(result).toBe("high"); + }); +}); + +describe("adoptConfirmMode behavior", () => { + // Pure function to test confirm mode logic + function shouldShowAdoptionConfirm( + mode: "always" | "onlyLowConfidence" | "never", + confidence: "high" | "low" + ): boolean { + if (mode === "always") { + return true; + } + if (mode === "never") { + return false; + } + // onlyLowConfidence: show only for low confidence + return confidence === "low"; + } + + it("always mode shows confirm for both high and low confidence", () => { + expect(shouldShowAdoptionConfirm("always", "high")).toBe(true); + expect(shouldShowAdoptionConfirm("always", "low")).toBe(true); + }); + + it("never mode never shows confirm", () => { + expect(shouldShowAdoptionConfirm("never", "high")).toBe(false); + expect(shouldShowAdoptionConfirm("never", "low")).toBe(false); + }); + + it("onlyLowConfidence mode shows confirm only for low confidence", () => { + expect(shouldShowAdoptionConfirm("onlyLowConfidence", "high")).toBe(false); + expect(shouldShowAdoptionConfirm("onlyLowConfidence", "low")).toBe(true); + }); +}); diff --git a/src/ui/MindnetSettingTab.ts b/src/ui/MindnetSettingTab.ts index 7e05183..398df4f 100644 --- a/src/ui/MindnetSettingTab.ts +++ b/src/ui/MindnetSettingTab.ts @@ -347,6 +347,41 @@ export class MindnetSettingTab extends PluginSettingTab { }) ); + // 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" }); diff --git a/src/unresolvedLink/adoptHelpers.ts b/src/unresolvedLink/adoptHelpers.ts index 224968a..4af3d63 100644 --- a/src/unresolvedLink/adoptHelpers.ts +++ b/src/unresolvedLink/adoptHelpers.ts @@ -38,7 +38,7 @@ export function isAdoptCandidate( /** * Check if a created file matches a pending create hint. - * Returns true if basename matches and within time window (3 seconds). + * Returns true if basename matches and within time window. */ export function matchesPendingHint( file: TFile, @@ -57,6 +57,46 @@ export function matchesPendingHint( ); } +/** + * Evaluate confidence level for adoption. + * Returns "high" if file is recently created and matches criteria, "low" otherwise. + */ +export function evaluateAdoptionConfidence( + file: TFile, + content: string, + maxChars: number, + hint: PendingCreateHint | null, + timeWindowMs: number +): "high" | "low" { + // Check if file is recently created (within time window) + const fileCtime = file.stat.ctime; + const timeSinceCreation = Date.now() - fileCtime; + const isRecent = timeSinceCreation >= 0 && timeSinceCreation <= timeWindowMs; + + if (!isRecent) { + return "low"; + } + + // Check if adopt candidate (no id, content length OK) + if (!isAdoptCandidate(content, maxChars)) { + return "low"; + } + + // High confidence if pending hint matches (optional but recommended) + if (matchesPendingHint(file, hint, timeWindowMs)) { + return "high"; + } + + // Also high confidence if file is very recent (within window) and adopt candidate + // This handles cases where pending hint might not be set + if (isRecent && timeSinceCreation <= 2000) { + // Very recent (within 2 seconds) = high confidence + return "high"; + } + + return "low"; +} + /** * Merge new frontmatter into existing content. * Preserves existing frontmatter fields and body content.