diff --git a/src/analysis/chainInspector.ts b/src/analysis/chainInspector.ts index 232e8c7..724467d 100644 --- a/src/analysis/chainInspector.ts +++ b/src/analysis/chainInspector.ts @@ -679,6 +679,8 @@ function computeFindings( /** * Inspect chains for current section context. + * @param editorContent Optional: If provided, use this content instead of reading from vault. + * This is useful when the file was just modified and vault cache is stale. */ export async function inspectChains( app: App, @@ -688,7 +690,8 @@ export async function inspectChains( edgeVocabularyPath?: string, chainTemplates?: ChainTemplatesConfig | null, templatesLoadResult?: { path: string; status: string; loadedAt: number | null; templateCount: number }, - templateMatchingProfileName?: string + templateMatchingProfileName?: string, + editorContent?: string ): Promise { // Build index for current note const currentFile = app.vault.getAbstractFileByPath(context.file); @@ -700,10 +703,11 @@ export async function inspectChains( throw new Error(`File not found or not a markdown file: ${context.file}`); } - const { edges: currentEdges, sections } = await buildNoteIndex( - app, - currentFile as TFile - ); + // Use editor content if provided, otherwise read from vault + const { buildNoteIndex, buildNoteIndexFromContent } = await import("./graphIndex"); + const { edges: currentEdges, sections } = editorContent + ? await buildNoteIndexFromContent(app, currentFile as TFile, editorContent) + : await buildNoteIndex(app, currentFile as TFile); // Collect all outgoing targets to load neighbor notes // Respect includeNoteLinks and includeCandidates toggles @@ -905,7 +909,8 @@ export async function inspectChains( } // Get section content for gap analysis - const content = await app.vault.read(currentFile as TFile); + // Use editor content if provided, otherwise read from vault + const content = editorContent || await app.vault.read(currentFile as TFile); const sectionsWithContent = splitIntoSections(content); const currentSectionContent = sectionsWithContent[context.sectionIndex]?.content || ""; diff --git a/src/analysis/graphIndex.ts b/src/analysis/graphIndex.ts index 2fe90d3..758394d 100644 --- a/src/analysis/graphIndex.ts +++ b/src/analysis/graphIndex.ts @@ -83,13 +83,27 @@ function parseTarget(linkText: string, currentFilePath: string, sections: NoteSe } /** - * Build graph index for a single note. + * Build graph index for a single note from file content. + * Use buildNoteIndexFromContent if you have the content already (e.g., from editor). */ export async function buildNoteIndex( app: App, file: TFile ): Promise<{ edges: IndexedEdge[]; sections: SectionNode[] }> { const content = await app.vault.read(file); + return buildNoteIndexFromContent(app, file, content); +} + +/** + * Build graph index for a single note from provided content. + * This is useful when you have the content from an editor and want to avoid + * reading stale data from the vault cache. + */ +export async function buildNoteIndexFromContent( + app: App, + file: TFile, + content: string +): Promise<{ edges: IndexedEdge[]; sections: SectionNode[] }> { const sections = splitIntoSections(content); const edges: IndexedEdge[] = []; const sectionNodes: SectionNode[] = []; diff --git a/src/commands/chainWorkbenchCommand.ts b/src/commands/chainWorkbenchCommand.ts index 29012d2..4edfaa2 100644 --- a/src/commands/chainWorkbenchCommand.ts +++ b/src/commands/chainWorkbenchCommand.ts @@ -105,7 +105,8 @@ export async function executeChainWorkbench( chainRoles, chainTemplates, vocabulary, - pluginInstance + pluginInstance, + templatesLoadResult ); modal.open(); } catch (e) { diff --git a/src/ui/ChainWorkbenchModal.ts b/src/ui/ChainWorkbenchModal.ts index 063d1b8..2f4a977 100644 --- a/src/ui/ChainWorkbenchModal.ts +++ b/src/ui/ChainWorkbenchModal.ts @@ -8,12 +8,15 @@ import type { WorkbenchModel, WorkbenchMatch, WorkbenchTodoUnion } from "../work import type { MindnetSettings } from "../settings"; import type { ChainRolesConfig, ChainTemplatesConfig } from "../dictionary/types"; import type { Vocabulary } from "../vocab/Vocabulary"; +import type { IndexedEdge, SectionNode } from "../analysis/graphIndex"; +import type { DictionaryLoadResult } from "../dictionary/types"; export class ChainWorkbenchModal extends Modal { private model: WorkbenchModel; private settings: MindnetSettings; private chainRoles: ChainRolesConfig | null; private chainTemplates: ChainTemplatesConfig | null; + private templatesLoadResult: DictionaryLoadResult | undefined; private vocabulary: Vocabulary; private pluginInstance: any; @@ -28,13 +31,15 @@ export class ChainWorkbenchModal extends Modal { chainRoles: ChainRolesConfig | null, chainTemplates: ChainTemplatesConfig | null, vocabulary: Vocabulary, - pluginInstance: any + pluginInstance: any, + templatesLoadResult?: DictionaryLoadResult ) { super(app); this.model = model; this.settings = settings; this.chainRoles = chainRoles; this.chainTemplates = chainTemplates; + this.templatesLoadResult = templatesLoadResult; this.vocabulary = vocabulary; this.pluginInstance = pluginInstance; @@ -94,22 +99,37 @@ export class ChainWorkbenchModal extends Modal { private render(): void { const { contentEl } = this; + // Debug: Log render state + console.log("[Chain Workbench] Render - model.matches.length:", this.model.matches.length); + console.log("[Chain Workbench] Render - filterStatus:", this.filterStatus, "searchQuery:", this.searchQuery); + // Filter matches let filteredMatches = this.model.matches; if (this.filterStatus) { filteredMatches = filteredMatches.filter((m) => m.status === this.filterStatus); + console.log("[Chain Workbench] After status filter:", filteredMatches.length); } if (this.searchQuery) { filteredMatches = filteredMatches.filter((m) => m.templateName.toLowerCase().includes(this.searchQuery) ); + console.log("[Chain Workbench] After search filter:", filteredMatches.length); } + + console.log("[Chain Workbench] Final filtered matches:", filteredMatches.length); // Sort: near_complete first (default), then by score descending + // Use stable sort to preserve order within same status/score groups filteredMatches.sort((a, b) => { if (a.status === "near_complete" && b.status !== "near_complete") return -1; if (a.status !== "near_complete" && b.status === "near_complete") return 1; - return b.score - a.score; + const scoreDiff = b.score - a.score; + if (scoreDiff !== 0) return scoreDiff; + // Preserve original order for matches with same status and score + // Use templateName + slotAssignments as tiebreaker for stability + const aId = this.getMatchIdentifier(a); + const bId = this.getMatchIdentifier(b); + return aId.localeCompare(bId); }); // Render tree view (left column) @@ -125,6 +145,19 @@ export class ChainWorkbenchModal extends Modal { treeContainer.empty(); treeContainer.createEl("h3", { text: "Templates & Chains" }); + + // Show message if no matches + if (matches.length === 0) { + const emptyMessage = treeContainer.createDiv({ cls: "workbench-empty-message" }); + emptyMessage.createEl("p", { text: "Keine Chains gefunden." }); + if (this.filterStatus || this.searchQuery) { + emptyMessage.createEl("p", { + cls: "workbench-empty-hint", + text: "Versuchen Sie, die Filter zu löschen, um alle Chains anzuzeigen." + }); + } + return; + } // Group matches by template name const matchesByTemplate = new Map(); @@ -969,8 +1002,39 @@ export class ChainWorkbenchModal extends Modal { }); } + /** + * Generate a unique identifier for a match to restore selection after refresh. + */ + private getMatchIdentifier(match: WorkbenchMatch): string { + // Create a stable identifier based on template name and slot assignments + const slotKeys = Object.keys(match.slotAssignments).sort(); + const slotValues = slotKeys.map(key => { + const assignment = match.slotAssignments[key]; + if (!assignment) return `${key}:null`; + return `${key}:${assignment.file}#${assignment.heading || ''}`; + }); + return `${match.templateName}|${slotValues.join('|')}`; + } + + /** + * Find a match in the new model that corresponds to the saved identifier. + */ + private findMatchByIdentifier(identifier: string, matches: WorkbenchMatch[]): WorkbenchMatch | null { + for (const match of matches) { + if (this.getMatchIdentifier(match) === identifier) { + return match; + } + } + return null; + } + private async refreshWorkbench(): Promise { try { + // Save current state before refresh + const savedSelectedMatchId = this.selectedMatch ? this.getMatchIdentifier(this.selectedMatch) : null; + const savedFilterStatus = this.filterStatus; + const savedSearchQuery = this.searchQuery; + // Reload the workbench model without closing the modal const activeFile = this.app.workspace.getActiveFile(); const activeEditor = this.app.workspace.activeEditor?.editor; @@ -1002,8 +1066,34 @@ export class ChainWorkbenchModal extends Modal { const vocabText = await VocabularyLoader.loadText(this.app, this.settings.edgeVocabularyPath); const edgeVocabulary = parseEdgeVocabulary(vocabText); - // Inspect chains + // Prepare templates source info for inspectChains + // This is required for template matching to work! + let templatesSourceInfo: { path: string; status: string; loadedAt: number | null; templateCount: number } | undefined; + if (this.templatesLoadResult) { + templatesSourceInfo = { + path: this.templatesLoadResult.resolvedPath, + status: this.templatesLoadResult.status, + loadedAt: this.templatesLoadResult.loadedAt, + templateCount: this.chainTemplates?.templates?.length || 0, + }; + } else if (this.chainTemplates) { + // Fallback: create a minimal templatesSourceInfo if templatesLoadResult is not available + templatesSourceInfo = { + path: this.settings.chainTemplatesPath || "unknown", + status: "loaded", + loadedAt: Date.now(), + templateCount: this.chainTemplates.templates?.length || 0, + }; + } + + // Inspect chains - pass editor content if available to avoid stale vault cache const { inspectChains } = await import("../analysis/chainInspector"); + const editorContent = activeEditor ? activeEditor.getValue() : undefined; + console.log("[Chain Workbench] Calling inspectChains with context:", context); + console.log("[Chain Workbench] Using editor content:", !!editorContent); + console.log("[Chain Workbench] chainTemplates:", this.chainTemplates ? "loaded" : "null"); + console.log("[Chain Workbench] chainRoles:", this.chainRoles ? "loaded" : "null"); + console.log("[Chain Workbench] templatesSourceInfo:", templatesSourceInfo ? "provided" : "null"); const report = await inspectChains( this.app, context, @@ -1011,20 +1101,36 @@ export class ChainWorkbenchModal extends Modal { this.chainRoles, this.settings.edgeVocabularyPath, this.chainTemplates, - undefined, - this.settings.templateMatchingProfile + templatesSourceInfo, + this.settings.templateMatchingProfile, + editorContent ); + console.log("[Chain Workbench] inspectChains returned - templateMatches:", report.templateMatches?.length || 0); - // Build all edges index - const { buildNoteIndex } = await import("../analysis/graphIndex"); + // Build all edges index for workbench model (using same editor content if available) + const { buildNoteIndex, buildNoteIndexFromContent } = await import("../analysis/graphIndex"); const fileObj = this.app.vault.getAbstractFileByPath(activeFile.path); if (!fileObj || !(fileObj instanceof TFile)) { throw new Error("Active file not found"); } - const { edges: allEdges } = await buildNoteIndex(this.app, fileObj); + + // Use editor content if available to ensure consistency with inspectChains + let allEdges: IndexedEdge[]; + if (editorContent) { + const result = await buildNoteIndexFromContent(this.app, fileObj, editorContent); + allEdges = result.edges; + console.log("[Chain Workbench] Built index from editor content for workbench, edges:", allEdges.length); + } else { + const result = await buildNoteIndex(this.app, fileObj); + allEdges = result.edges; + console.log("[Chain Workbench] Built index from vault for workbench, edges:", allEdges.length); + } // Build workbench model const { buildWorkbenchModel } = await import("../workbench/workbenchBuilder"); + console.log("[Chain Workbench] Building workbench model..."); + console.log("[Chain Workbench] Report has templateMatches:", report.templateMatches?.length || 0); + console.log("[Chain Workbench] allEdges count:", allEdges.length); const newModel = await buildWorkbenchModel( this.app, report, @@ -1035,11 +1141,48 @@ export class ChainWorkbenchModal extends Modal { this.settings.debugLogging ); - // Update model and re-render + // Update model this.model = newModel; - this.selectedMatch = null; // Reset selection - this.filterStatus = null; // Reset filters - this.searchQuery = ""; // Reset search + + // Debug: Log model state + console.log("[Chain Workbench] Refresh complete - matches:", newModel.matches.length); + if (newModel.matches.length === 0) { + console.warn("[Chain Workbench] WARNING: No matches found after refresh!"); + console.log("[Chain Workbench] Report templateMatches:", report.templateMatches?.length || 0); + console.log("[Chain Workbench] Context:", context); + console.log("[Chain Workbench] chainTemplates:", this.chainTemplates ? `loaded (${this.chainTemplates.templates?.length || 0} templates)` : "null"); + console.log("[Chain Workbench] allEdges:", allEdges.length); + if (report.templateMatches && report.templateMatches.length > 0) { + console.warn("[Chain Workbench] Report HAS templateMatches but model.matches is empty!"); + console.log("[Chain Workbench] First templateMatch:", report.templateMatches[0]); + } + } else { + console.log("[Chain Workbench] Model has matches, first match:", newModel.matches[0]?.templateName); + } + + // Restore selection if possible + if (savedSelectedMatchId && newModel.matches.length > 0) { + const restoredMatch = this.findMatchByIdentifier(savedSelectedMatchId, newModel.matches); + this.selectedMatch = restoredMatch; + // If exact match not found, try to find a similar match (same template) + if (!this.selectedMatch && savedSelectedMatchId) { + const templateName = savedSelectedMatchId.split('|')[0]; + const templateMatches = newModel.matches.filter(m => m.templateName === templateName); + if (templateMatches.length > 0) { + // Select the first match of the same template as fallback + this.selectedMatch = templateMatches[0] || null; + } + } + } else { + this.selectedMatch = null; + } + + // Restore filters + this.filterStatus = savedFilterStatus; + this.searchQuery = savedSearchQuery; + + // Debug: Log filter state + console.log("[Chain Workbench] Filter status:", this.filterStatus, "Search query:", this.searchQuery); // Clear and re-render the entire UI const { contentEl } = this; @@ -1062,6 +1205,10 @@ export class ChainWorkbenchModal extends Modal { statusSelect.createEl("option", { text: "Near Complete", value: "near_complete" }); statusSelect.createEl("option", { text: "Partial", value: "partial" }); statusSelect.createEl("option", { text: "Weak", value: "weak" }); + // Restore filter value + if (this.filterStatus) { + statusSelect.value = this.filterStatus; + } statusSelect.addEventListener("change", (e) => { const target = e.target as HTMLSelectElement; this.filterStatus = target.value || null; @@ -1070,6 +1217,10 @@ export class ChainWorkbenchModal extends Modal { filterContainer.createEl("label", { text: "Search:" }); const searchInput = filterContainer.createEl("input", { type: "text", placeholder: "Template name..." }); + // Restore search query + if (this.searchQuery) { + searchInput.value = this.searchQuery; + } searchInput.addEventListener("input", (e) => { const target = e.target as HTMLInputElement; this.searchQuery = target.value.toLowerCase(); @@ -1088,7 +1239,9 @@ export class ChainWorkbenchModal extends Modal { detailsContainer.createEl("h3", { text: "Chain Details" }); // Now render the content + console.log("[Chain Workbench] About to call render() - model.matches.length:", this.model.matches.length); this.render(); + console.log("[Chain Workbench] render() completed"); } catch (error) { console.error("[Chain Workbench] Error refreshing workbench:", error); new Notice(`Error refreshing workbench: ${error instanceof Error ? error.message : String(error)}`); diff --git a/src/workbench/workbenchBuilder.ts b/src/workbench/workbenchBuilder.ts index af689e3..88d7928 100644 --- a/src/workbench/workbenchBuilder.ts +++ b/src/workbench/workbenchBuilder.ts @@ -26,12 +26,19 @@ export async function buildWorkbenchModel( const matches: WorkbenchMatch[] = []; if (!report.templateMatches || !chainTemplates) { + console.warn("[buildWorkbenchModel] No templateMatches or chainTemplates:", { + hasTemplateMatches: !!report.templateMatches, + templateMatchesCount: report.templateMatches?.length || 0, + hasChainTemplates: !!chainTemplates, + }); return { context: report.context, matches: [], timestamp: Date.now(), }; } + + console.log("[buildWorkbenchModel] Processing", report.templateMatches.length, "template matches"); // Get candidate edges from current note // Also check for confirmed edges that would make candidates "not needed"