Enhance chain inspection functionality with editor content support
Some checks are pending
Node.js build / build (20.x) (push) Waiting to run
Node.js build / build (22.x) (push) Waiting to run

- Updated `inspectChains` to accept optional `editorContent`, allowing for real-time inspection without relying on potentially stale vault data.
- Introduced `buildNoteIndexFromContent` to facilitate graph indexing directly from provided content.
- Improved handling of template matching profiles in `ChainWorkbenchModal`, ensuring accurate context during chain inspections.
- Added debug logging for better traceability of the chain inspection process.
This commit is contained in:
Lars 2026-02-06 12:02:51 +01:00
parent dbd76b764d
commit 99c77ef616
5 changed files with 200 additions and 20 deletions

View File

@ -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<ChainInspectorReport> {
// 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 || "";

View File

@ -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[] = [];

View File

@ -105,7 +105,8 @@ export async function executeChainWorkbench(
chainRoles,
chainTemplates,
vocabulary,
pluginInstance
pluginInstance,
templatesLoadResult
);
modal.open();
} catch (e) {

View File

@ -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<ChainTemplatesConfig> | 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<ChainTemplatesConfig>
) {
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)
@ -126,6 +146,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<string, WorkbenchMatch[]>();
for (const match of matches) {
@ -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<void> {
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)}`);

View File

@ -26,6 +26,11 @@ 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: [],
@ -33,6 +38,8 @@ export async function buildWorkbenchModel(
};
}
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"
const candidateEdges = allEdges.filter((e) => e.scope === "candidate");