679 lines
24 KiB
TypeScript
679 lines
24 KiB
TypeScript
|
|
/**
|
||
|
|
* Chain Workbench Modal - UI for chain workbench.
|
||
|
|
*/
|
||
|
|
|
||
|
|
import { Modal, Setting, Notice } from "obsidian";
|
||
|
|
import type { App } from "obsidian";
|
||
|
|
import type { WorkbenchModel, WorkbenchMatch, WorkbenchTodoUnion } from "../workbench/types";
|
||
|
|
import type { MindnetSettings } from "../settings";
|
||
|
|
import type { ChainRolesConfig, ChainTemplatesConfig } from "../dictionary/types";
|
||
|
|
import type { Vocabulary } from "../vocab/Vocabulary";
|
||
|
|
|
||
|
|
export class ChainWorkbenchModal extends Modal {
|
||
|
|
private model: WorkbenchModel;
|
||
|
|
private settings: MindnetSettings;
|
||
|
|
private chainRoles: ChainRolesConfig | null;
|
||
|
|
private chainTemplates: ChainTemplatesConfig | null;
|
||
|
|
private vocabulary: Vocabulary;
|
||
|
|
private pluginInstance: any;
|
||
|
|
|
||
|
|
private selectedMatch: WorkbenchMatch | null = null;
|
||
|
|
private filterStatus: string | null = null;
|
||
|
|
private searchQuery: string = "";
|
||
|
|
|
||
|
|
constructor(
|
||
|
|
app: App,
|
||
|
|
model: WorkbenchModel,
|
||
|
|
settings: MindnetSettings,
|
||
|
|
chainRoles: ChainRolesConfig | null,
|
||
|
|
chainTemplates: ChainTemplatesConfig | null,
|
||
|
|
vocabulary: Vocabulary,
|
||
|
|
pluginInstance: any
|
||
|
|
) {
|
||
|
|
super(app);
|
||
|
|
this.model = model;
|
||
|
|
this.settings = settings;
|
||
|
|
this.chainRoles = chainRoles;
|
||
|
|
this.chainTemplates = chainTemplates;
|
||
|
|
this.vocabulary = vocabulary;
|
||
|
|
this.pluginInstance = pluginInstance;
|
||
|
|
}
|
||
|
|
|
||
|
|
onOpen(): void {
|
||
|
|
const { contentEl } = this;
|
||
|
|
contentEl.empty();
|
||
|
|
|
||
|
|
// Header
|
||
|
|
contentEl.createEl("h2", { text: "Chain Workbench" });
|
||
|
|
contentEl.createEl("p", {
|
||
|
|
text: `Context: ${this.model.context.file}${this.model.context.heading ? `#${this.model.context.heading}` : ""}`,
|
||
|
|
});
|
||
|
|
|
||
|
|
// Filters
|
||
|
|
const filterContainer = contentEl.createDiv({ cls: "workbench-filters" });
|
||
|
|
filterContainer.createEl("label", { text: "Filter by Status:" });
|
||
|
|
const statusSelect = filterContainer.createEl("select");
|
||
|
|
statusSelect.createEl("option", { text: "All", value: "" });
|
||
|
|
statusSelect.createEl("option", { text: "Complete", value: "complete" });
|
||
|
|
statusSelect.createEl("option", { text: "Near Complete", value: "near_complete" });
|
||
|
|
statusSelect.createEl("option", { text: "Partial", value: "partial" });
|
||
|
|
statusSelect.createEl("option", { text: "Weak", value: "weak" });
|
||
|
|
statusSelect.addEventListener("change", (e) => {
|
||
|
|
const target = e.target as HTMLSelectElement;
|
||
|
|
this.filterStatus = target.value || null;
|
||
|
|
this.render();
|
||
|
|
});
|
||
|
|
|
||
|
|
filterContainer.createEl("label", { text: "Search:" });
|
||
|
|
const searchInput = filterContainer.createEl("input", { type: "text", placeholder: "Template name..." });
|
||
|
|
searchInput.addEventListener("input", (e) => {
|
||
|
|
const target = e.target as HTMLInputElement;
|
||
|
|
this.searchQuery = target.value.toLowerCase();
|
||
|
|
this.render();
|
||
|
|
});
|
||
|
|
|
||
|
|
// Main container: list + details
|
||
|
|
const mainContainer = contentEl.createDiv({ cls: "workbench-main" });
|
||
|
|
|
||
|
|
// Left: Match list
|
||
|
|
const listContainer = mainContainer.createDiv({ cls: "workbench-list" });
|
||
|
|
listContainer.createEl("h3", { text: "Template Matches" });
|
||
|
|
|
||
|
|
// Right: Details
|
||
|
|
const detailsContainer = mainContainer.createDiv({ cls: "workbench-details" });
|
||
|
|
detailsContainer.createEl("h3", { text: "Details" });
|
||
|
|
|
||
|
|
// Global todos section (if any)
|
||
|
|
if (this.model.globalTodos && this.model.globalTodos.length > 0) {
|
||
|
|
const globalTodosSection = contentEl.createDiv({ cls: "workbench-global-todos" });
|
||
|
|
globalTodosSection.createEl("h3", { text: "Section-Level Issues" });
|
||
|
|
globalTodosSection.createEl("p", {
|
||
|
|
text: `Found ${this.model.globalTodos.length} issue(s) from Chain Inspector findings`,
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
this.render();
|
||
|
|
}
|
||
|
|
|
||
|
|
private render(): void {
|
||
|
|
const { contentEl } = this;
|
||
|
|
|
||
|
|
// Filter matches
|
||
|
|
let filteredMatches = this.model.matches;
|
||
|
|
if (this.filterStatus) {
|
||
|
|
filteredMatches = filteredMatches.filter((m) => m.status === this.filterStatus);
|
||
|
|
}
|
||
|
|
if (this.searchQuery) {
|
||
|
|
filteredMatches = filteredMatches.filter((m) =>
|
||
|
|
m.templateName.toLowerCase().includes(this.searchQuery)
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Sort: near_complete first (default), then by score descending
|
||
|
|
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;
|
||
|
|
});
|
||
|
|
|
||
|
|
// Render list
|
||
|
|
const listContainer = contentEl.querySelector(".workbench-list");
|
||
|
|
if (listContainer) {
|
||
|
|
listContainer.empty();
|
||
|
|
listContainer.createEl("h3", { text: `Template Matches (${filteredMatches.length})` });
|
||
|
|
|
||
|
|
for (const match of filteredMatches) {
|
||
|
|
const matchEl = listContainer.createDiv({ cls: "workbench-match-item" });
|
||
|
|
if (this.selectedMatch === match) {
|
||
|
|
matchEl.addClass("selected");
|
||
|
|
}
|
||
|
|
|
||
|
|
const statusIcon = this.getStatusIcon(match.status);
|
||
|
|
matchEl.createEl("div", {
|
||
|
|
cls: "match-header",
|
||
|
|
text: `${statusIcon} ${match.templateName}`,
|
||
|
|
});
|
||
|
|
|
||
|
|
matchEl.createEl("div", {
|
||
|
|
cls: "match-stats",
|
||
|
|
text: `Slots: ${match.slotsFilled}/${match.slotsTotal} | Links: ${match.satisfiedLinks}/${match.requiredLinks} | Score: ${match.score}`,
|
||
|
|
});
|
||
|
|
|
||
|
|
matchEl.createEl("div", {
|
||
|
|
cls: "match-todos-count",
|
||
|
|
text: `${match.todos.length} todo(s)`,
|
||
|
|
});
|
||
|
|
|
||
|
|
matchEl.addEventListener("click", () => {
|
||
|
|
this.selectedMatch = match;
|
||
|
|
this.renderDetails();
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Render details
|
||
|
|
this.renderDetails();
|
||
|
|
|
||
|
|
// Render global todos
|
||
|
|
this.renderGlobalTodos();
|
||
|
|
}
|
||
|
|
|
||
|
|
private renderGlobalTodos(): void {
|
||
|
|
const globalTodosSection = this.contentEl.querySelector(".workbench-global-todos");
|
||
|
|
if (!globalTodosSection || !this.model.globalTodos || this.model.globalTodos.length === 0) {
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Clear and re-render
|
||
|
|
const existingList = globalTodosSection.querySelector(".global-todos-list");
|
||
|
|
if (existingList) {
|
||
|
|
existingList.remove();
|
||
|
|
}
|
||
|
|
|
||
|
|
const todosList = globalTodosSection.createDiv({ cls: "global-todos-list" });
|
||
|
|
|
||
|
|
for (const todo of this.model.globalTodos) {
|
||
|
|
const todoEl = todosList.createDiv({ cls: "global-todo-item" });
|
||
|
|
todoEl.createEl("div", { cls: "todo-type", text: todo.type });
|
||
|
|
todoEl.createEl("div", { cls: "todo-description", text: todo.description });
|
||
|
|
todoEl.createEl("div", { cls: "todo-priority", text: `Priority: ${todo.priority}` });
|
||
|
|
|
||
|
|
// Render actions
|
||
|
|
const actionsContainer = todoEl.createDiv({ cls: "todo-actions" });
|
||
|
|
if (
|
||
|
|
todo.type === "dangling_target" ||
|
||
|
|
todo.type === "dangling_target_heading" ||
|
||
|
|
todo.type === "only_candidates" ||
|
||
|
|
todo.type === "missing_edges" ||
|
||
|
|
todo.type === "no_causal_roles"
|
||
|
|
) {
|
||
|
|
const actions = (todo as any).actions || [];
|
||
|
|
for (const action of actions) {
|
||
|
|
const actionBtn = actionsContainer.createEl("button", {
|
||
|
|
text: this.getActionLabel(action),
|
||
|
|
});
|
||
|
|
actionBtn.addEventListener("click", () => {
|
||
|
|
this.handleGlobalTodoAction(todo, action);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
} else if (todo.type === "one_sided_connectivity") {
|
||
|
|
// Informational only - no actions
|
||
|
|
actionsContainer.createEl("span", { text: "Informational only", cls: "info-text" });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private renderDetails(): void {
|
||
|
|
const detailsContainer = this.contentEl.querySelector(".workbench-details");
|
||
|
|
if (!detailsContainer) return;
|
||
|
|
|
||
|
|
detailsContainer.empty();
|
||
|
|
detailsContainer.createEl("h3", { text: "Details" });
|
||
|
|
|
||
|
|
if (!this.selectedMatch) {
|
||
|
|
detailsContainer.createEl("p", { text: "Select a match to view details" });
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const match = this.selectedMatch;
|
||
|
|
|
||
|
|
// Match info
|
||
|
|
detailsContainer.createEl("h4", { text: match.templateName });
|
||
|
|
detailsContainer.createEl("p", {
|
||
|
|
text: `Status: ${match.status} | Score: ${match.score} | Confidence: ${match.confidence}`,
|
||
|
|
});
|
||
|
|
|
||
|
|
// Slots
|
||
|
|
detailsContainer.createEl("h5", { text: "Slots" });
|
||
|
|
const slotsList = detailsContainer.createEl("ul");
|
||
|
|
for (const [slotId, assignment] of Object.entries(match.slotAssignments)) {
|
||
|
|
if (assignment) {
|
||
|
|
const nodeStr = assignment.heading
|
||
|
|
? `${assignment.file}#${assignment.heading}`
|
||
|
|
: assignment.file;
|
||
|
|
slotsList.createEl("li", { text: `${slotId}: ${nodeStr} [${assignment.noteType}]` });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if (match.missingSlots.length > 0) {
|
||
|
|
slotsList.createEl("li", {
|
||
|
|
text: `Missing: ${match.missingSlots.join(", ")}`,
|
||
|
|
cls: "missing",
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// Links
|
||
|
|
detailsContainer.createEl("h5", { text: "Links" });
|
||
|
|
detailsContainer.createEl("p", {
|
||
|
|
text: `${match.satisfiedLinks}/${match.requiredLinks} satisfied (${match.linksComplete ? "complete" : "incomplete"})`,
|
||
|
|
});
|
||
|
|
|
||
|
|
// Todos
|
||
|
|
detailsContainer.createEl("h5", { text: `Todos (${match.todos.length})` });
|
||
|
|
const todosList = detailsContainer.createEl("div", { cls: "todos-list" });
|
||
|
|
|
||
|
|
for (const todo of match.todos) {
|
||
|
|
const todoEl = todosList.createDiv({ cls: `todo-item todo-${todo.type}` });
|
||
|
|
todoEl.createEl("div", { cls: "todo-description", text: todo.description });
|
||
|
|
todoEl.createEl("div", { cls: "todo-priority", text: `Priority: ${todo.priority}` });
|
||
|
|
|
||
|
|
// Render actions
|
||
|
|
const actionsContainer = todoEl.createDiv({ cls: "todo-actions" });
|
||
|
|
if (
|
||
|
|
todo.type === "missing_slot" ||
|
||
|
|
todo.type === "missing_link" ||
|
||
|
|
todo.type === "weak_roles" ||
|
||
|
|
todo.type === "candidate_cleanup"
|
||
|
|
) {
|
||
|
|
const actions = (todo as any).actions || [];
|
||
|
|
for (const action of actions) {
|
||
|
|
const actionBtn = actionsContainer.createEl("button", {
|
||
|
|
text: this.getActionLabel(action),
|
||
|
|
});
|
||
|
|
actionBtn.addEventListener("click", () => {
|
||
|
|
this.handleTodoAction(todo as WorkbenchTodoUnion, action, match);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private getStatusIcon(status: string): string {
|
||
|
|
switch (status) {
|
||
|
|
case "complete":
|
||
|
|
return "✓";
|
||
|
|
case "near_complete":
|
||
|
|
return "~";
|
||
|
|
case "partial":
|
||
|
|
return "○";
|
||
|
|
case "weak":
|
||
|
|
return "⚠";
|
||
|
|
default:
|
||
|
|
return "?";
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private getActionLabel(action: string): string {
|
||
|
|
const labels: Record<string, string> = {
|
||
|
|
link_existing: "Link Existing",
|
||
|
|
create_note_via_interview: "Create Note",
|
||
|
|
insert_edge_forward: "Insert Edge",
|
||
|
|
insert_edge_inverse: "Insert Inverse",
|
||
|
|
choose_target_anchor: "Choose Anchor",
|
||
|
|
change_edge_type: "Change Type",
|
||
|
|
promote_candidate: "Promote",
|
||
|
|
resolve_candidate: "Resolve",
|
||
|
|
create_missing_note: "Create Missing Note",
|
||
|
|
retarget_link: "Retarget Link",
|
||
|
|
create_missing_heading: "Create Missing Heading",
|
||
|
|
retarget_to_existing_heading: "Retarget to Existing Heading",
|
||
|
|
promote_all_candidates: "Promote All Candidates",
|
||
|
|
create_explicit_edges: "Create Explicit Edges",
|
||
|
|
add_edges_to_section: "Add Edges to Section",
|
||
|
|
};
|
||
|
|
return labels[action] || action;
|
||
|
|
}
|
||
|
|
|
||
|
|
private async handleTodoAction(
|
||
|
|
todo: WorkbenchTodoUnion,
|
||
|
|
action: string,
|
||
|
|
match: WorkbenchMatch
|
||
|
|
): Promise<void> {
|
||
|
|
console.log("[Chain Workbench] Action:", action, "for todo:", todo.type);
|
||
|
|
|
||
|
|
try {
|
||
|
|
const activeFile = this.app.workspace.getActiveFile();
|
||
|
|
const activeEditor = this.app.workspace.activeEditor?.editor;
|
||
|
|
|
||
|
|
if (!activeFile || !activeEditor) {
|
||
|
|
new Notice("No active file or editor");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Import action handlers
|
||
|
|
const { insertEdgeForward, promoteCandidate } = await import("../workbench/writerActions");
|
||
|
|
const graphSchema = await this.pluginInstance.ensureGraphSchemaLoaded();
|
||
|
|
|
||
|
|
switch (action) {
|
||
|
|
case "insert_edge_forward":
|
||
|
|
if (todo.type === "missing_link") {
|
||
|
|
console.log("[Chain Workbench] Starting insert_edge_forward for missing_link todo");
|
||
|
|
// Let user choose zone
|
||
|
|
console.log("[Chain Workbench] Opening zone chooser...");
|
||
|
|
const zoneChoice = await this.chooseZone(activeFile);
|
||
|
|
console.log("[Chain Workbench] Zone choice:", zoneChoice);
|
||
|
|
if (zoneChoice === null) {
|
||
|
|
console.log("[Chain Workbench] User cancelled zone selection");
|
||
|
|
return; // User cancelled
|
||
|
|
}
|
||
|
|
|
||
|
|
// Get edge vocabulary
|
||
|
|
console.log("[Chain Workbench] Loading edge vocabulary...");
|
||
|
|
const { VocabularyLoader } = await import("../vocab/VocabularyLoader");
|
||
|
|
const { parseEdgeVocabulary } = await import("../vocab/parseEdgeVocabulary");
|
||
|
|
const vocabText = await VocabularyLoader.loadText(this.app, this.settings.edgeVocabularyPath);
|
||
|
|
const edgeVocabulary = parseEdgeVocabulary(vocabText);
|
||
|
|
console.log("[Chain Workbench] Edge vocabulary loaded, entries:", edgeVocabulary.byCanonical.size);
|
||
|
|
|
||
|
|
console.log("[Chain Workbench] Calling insertEdgeForward...");
|
||
|
|
try {
|
||
|
|
await insertEdgeForward(
|
||
|
|
this.app,
|
||
|
|
activeEditor,
|
||
|
|
activeFile,
|
||
|
|
todo,
|
||
|
|
this.vocabulary,
|
||
|
|
edgeVocabulary,
|
||
|
|
this.settings,
|
||
|
|
graphSchema,
|
||
|
|
zoneChoice
|
||
|
|
);
|
||
|
|
console.log("[Chain Workbench] insertEdgeForward completed successfully");
|
||
|
|
} catch (error) {
|
||
|
|
console.error("[Chain Workbench] Error in insertEdgeForward:", error);
|
||
|
|
new Notice(`Error inserting edge: ${error instanceof Error ? error.message : String(error)}`);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Re-run analysis
|
||
|
|
console.log("[Chain Workbench] Refreshing workbench...");
|
||
|
|
await this.refreshWorkbench();
|
||
|
|
new Notice("Edge inserted successfully");
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
|
||
|
|
case "promote_candidate":
|
||
|
|
if (todo.type === "candidate_cleanup") {
|
||
|
|
await promoteCandidate(
|
||
|
|
this.app,
|
||
|
|
activeEditor,
|
||
|
|
activeFile,
|
||
|
|
todo,
|
||
|
|
this.settings
|
||
|
|
);
|
||
|
|
|
||
|
|
// Re-run analysis
|
||
|
|
await this.refreshWorkbench();
|
||
|
|
new Notice("Candidate promoted successfully");
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
|
||
|
|
case "link_existing":
|
||
|
|
if (todo.type === "missing_slot") {
|
||
|
|
const { linkExisting } = await import("../workbench/linkExistingAction");
|
||
|
|
await linkExisting(
|
||
|
|
this.app,
|
||
|
|
activeEditor,
|
||
|
|
activeFile,
|
||
|
|
todo,
|
||
|
|
this.model.context
|
||
|
|
);
|
||
|
|
|
||
|
|
// Re-run analysis
|
||
|
|
await this.refreshWorkbench();
|
||
|
|
new Notice("Link inserted. Re-run workbench to update slot assignments.");
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
|
||
|
|
case "create_note_via_interview":
|
||
|
|
if (todo.type === "missing_slot") {
|
||
|
|
const { createNoteViaInterview } = await import("../workbench/interviewOrchestration");
|
||
|
|
|
||
|
|
// Get interview config from plugin instance
|
||
|
|
let interviewConfig = null;
|
||
|
|
if (this.pluginInstance.ensureInterviewConfigLoaded) {
|
||
|
|
interviewConfig = await this.pluginInstance.ensureInterviewConfigLoaded();
|
||
|
|
}
|
||
|
|
|
||
|
|
if (!interviewConfig) {
|
||
|
|
new Notice("Interview config not available");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Find the match for this todo
|
||
|
|
const matchForTodo = this.model.matches.find((m) =>
|
||
|
|
m.todos.some((t) => t.id === todo.id)
|
||
|
|
);
|
||
|
|
|
||
|
|
if (!matchForTodo) {
|
||
|
|
new Notice("Could not find match for todo");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Get edge vocabulary - we need to load it from settings
|
||
|
|
const { VocabularyLoader } = await import("../vocab/VocabularyLoader");
|
||
|
|
const { parseEdgeVocabulary } = await import("../vocab/parseEdgeVocabulary");
|
||
|
|
const vocabText = await VocabularyLoader.loadText(this.app, this.settings.edgeVocabularyPath);
|
||
|
|
const edgeVocabulary = parseEdgeVocabulary(vocabText);
|
||
|
|
|
||
|
|
await createNoteViaInterview(
|
||
|
|
this.app,
|
||
|
|
todo,
|
||
|
|
matchForTodo.templateName,
|
||
|
|
matchForTodo,
|
||
|
|
interviewConfig,
|
||
|
|
this.chainTemplates,
|
||
|
|
this.chainRoles,
|
||
|
|
this.settings,
|
||
|
|
this.vocabulary,
|
||
|
|
edgeVocabulary,
|
||
|
|
this.pluginInstance
|
||
|
|
);
|
||
|
|
|
||
|
|
// Re-run analysis after note creation
|
||
|
|
await this.refreshWorkbench();
|
||
|
|
new Notice("Note created via interview");
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
|
||
|
|
default:
|
||
|
|
new Notice(`Action "${action}" not yet implemented`);
|
||
|
|
}
|
||
|
|
} catch (e) {
|
||
|
|
const msg = e instanceof Error ? e.message : String(e);
|
||
|
|
console.error("[Chain Workbench] Error handling action:", e);
|
||
|
|
new Notice(`Failed to execute action: ${msg}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private async handleGlobalTodoAction(todo: WorkbenchTodoUnion, action: string): Promise<void> {
|
||
|
|
console.log("[Chain Workbench] Global Todo Action:", action, "for todo:", todo.type);
|
||
|
|
|
||
|
|
try {
|
||
|
|
const activeFile = this.app.workspace.getActiveFile();
|
||
|
|
const activeEditor = this.app.workspace.activeEditor?.editor;
|
||
|
|
|
||
|
|
if (!activeFile || !activeEditor) {
|
||
|
|
new Notice("No active file or editor");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Import action handlers
|
||
|
|
const { promoteCandidate } = await import("../workbench/writerActions");
|
||
|
|
const { linkExisting } = await import("../workbench/linkExistingAction");
|
||
|
|
|
||
|
|
switch (action) {
|
||
|
|
case "create_missing_note":
|
||
|
|
if (todo.type === "dangling_target") {
|
||
|
|
const danglingTodo = todo as any;
|
||
|
|
// Use linkExisting action to create note
|
||
|
|
await linkExisting(
|
||
|
|
this.app,
|
||
|
|
activeEditor,
|
||
|
|
activeFile,
|
||
|
|
{
|
||
|
|
type: "missing_slot",
|
||
|
|
id: `dangling_target_create_${danglingTodo.targetFile}`,
|
||
|
|
description: `Create missing note: ${danglingTodo.targetFile}`,
|
||
|
|
priority: "high",
|
||
|
|
slotId: "",
|
||
|
|
allowedNodeTypes: [],
|
||
|
|
actions: ["create_note_via_interview"],
|
||
|
|
},
|
||
|
|
this.model.context
|
||
|
|
);
|
||
|
|
await this.refreshWorkbench();
|
||
|
|
new Notice("Note creation initiated");
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
|
||
|
|
case "retarget_link":
|
||
|
|
if (todo.type === "dangling_target") {
|
||
|
|
const danglingTodo = todo as any;
|
||
|
|
// Use linkExisting to retarget
|
||
|
|
await linkExisting(
|
||
|
|
this.app,
|
||
|
|
activeEditor,
|
||
|
|
activeFile,
|
||
|
|
{
|
||
|
|
type: "missing_slot",
|
||
|
|
id: `dangling_target_retarget_${danglingTodo.targetFile}`,
|
||
|
|
description: `Retarget link to existing note`,
|
||
|
|
priority: "high",
|
||
|
|
slotId: "",
|
||
|
|
allowedNodeTypes: [],
|
||
|
|
actions: ["link_existing"],
|
||
|
|
},
|
||
|
|
this.model.context
|
||
|
|
);
|
||
|
|
await this.refreshWorkbench();
|
||
|
|
new Notice("Link retargeting initiated");
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
|
||
|
|
case "promote_all_candidates":
|
||
|
|
if (todo.type === "only_candidates") {
|
||
|
|
const onlyCandidatesTodo = todo as any;
|
||
|
|
// Promote each candidate edge
|
||
|
|
for (const candidateEdge of onlyCandidatesTodo.candidateEdges || []) {
|
||
|
|
// Find the actual edge in the graph
|
||
|
|
// This is a simplified version - full implementation would need to find the edge
|
||
|
|
new Notice(`Promoting candidate edge: ${candidateEdge.rawEdgeType}`);
|
||
|
|
}
|
||
|
|
await this.refreshWorkbench();
|
||
|
|
new Notice("Candidates promoted");
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
|
||
|
|
case "promote_candidate":
|
||
|
|
// Similar to template-based promote_candidate
|
||
|
|
if (todo.type === "candidate_cleanup") {
|
||
|
|
await promoteCandidate(
|
||
|
|
this.app,
|
||
|
|
activeEditor,
|
||
|
|
activeFile,
|
||
|
|
todo,
|
||
|
|
this.settings
|
||
|
|
);
|
||
|
|
await this.refreshWorkbench();
|
||
|
|
new Notice("Candidate promoted");
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
|
||
|
|
case "change_edge_type":
|
||
|
|
if (todo.type === "no_causal_roles" || todo.type === "weak_roles") {
|
||
|
|
// Use existing change_edge_type logic
|
||
|
|
new Notice("Change edge type - feature coming soon");
|
||
|
|
}
|
||
|
|
break;
|
||
|
|
|
||
|
|
default:
|
||
|
|
new Notice(`Action "${action}" not yet implemented for ${todo.type}`);
|
||
|
|
break;
|
||
|
|
}
|
||
|
|
} catch (e) {
|
||
|
|
console.error("[Chain Workbench] Error handling global todo action:", e);
|
||
|
|
new Notice(`Error: ${e instanceof Error ? e.message : String(e)}`);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private async chooseZone(file: any): Promise<"section" | "note_links" | "candidates" | null> {
|
||
|
|
console.log("[chooseZone] Starting zone selection");
|
||
|
|
return new Promise((resolve) => {
|
||
|
|
let resolved = false;
|
||
|
|
const { Modal, Setting } = require("obsidian");
|
||
|
|
const modal = new Modal(this.app);
|
||
|
|
modal.titleEl.textContent = "Choose Zone";
|
||
|
|
modal.contentEl.createEl("p", { text: "Where should the edge be inserted?" });
|
||
|
|
|
||
|
|
const doResolve = (value: "section" | "note_links" | "candidates" | null) => {
|
||
|
|
if (!resolved) {
|
||
|
|
resolved = true;
|
||
|
|
resolve(value);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
new Setting(modal.contentEl)
|
||
|
|
.setName("Section-scope")
|
||
|
|
.setDesc("Insert in source section (standard)")
|
||
|
|
.addButton((btn: any) =>
|
||
|
|
btn.setButtonText("Select").onClick(() => {
|
||
|
|
console.log("[chooseZone] User selected: section");
|
||
|
|
doResolve("section");
|
||
|
|
modal.close();
|
||
|
|
})
|
||
|
|
);
|
||
|
|
|
||
|
|
new Setting(modal.contentEl)
|
||
|
|
.setName("Note-Verbindungen")
|
||
|
|
.setDesc("Insert in Note-Verbindungen zone (note-scope)")
|
||
|
|
.addButton((btn: any) =>
|
||
|
|
btn.setButtonText("Select").onClick(() => {
|
||
|
|
console.log("[chooseZone] User selected: note_links");
|
||
|
|
doResolve("note_links");
|
||
|
|
modal.close();
|
||
|
|
})
|
||
|
|
);
|
||
|
|
|
||
|
|
new Setting(modal.contentEl)
|
||
|
|
.setName("Kandidaten")
|
||
|
|
.setDesc("Insert in Kandidaten zone (candidate)")
|
||
|
|
.addButton((btn: any) =>
|
||
|
|
btn.setButtonText("Select").onClick(() => {
|
||
|
|
console.log("[chooseZone] User selected: candidates");
|
||
|
|
doResolve("candidates");
|
||
|
|
modal.close();
|
||
|
|
})
|
||
|
|
);
|
||
|
|
|
||
|
|
modal.onClose = () => {
|
||
|
|
console.log("[chooseZone] Modal closed, resolved:", resolved);
|
||
|
|
if (!resolved) {
|
||
|
|
console.log("[chooseZone] Resolving with null (user cancelled)");
|
||
|
|
doResolve(null);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
console.log("[chooseZone] Opening modal...");
|
||
|
|
modal.open();
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
private async refreshWorkbench(): Promise<void> {
|
||
|
|
// Close and reopen workbench to refresh
|
||
|
|
this.close();
|
||
|
|
|
||
|
|
// Re-run command
|
||
|
|
const { executeChainWorkbench } = await import("../commands/chainWorkbenchCommand");
|
||
|
|
const activeFile = this.app.workspace.getActiveFile();
|
||
|
|
const activeEditor = this.app.workspace.activeEditor?.editor;
|
||
|
|
|
||
|
|
if (activeFile && activeEditor) {
|
||
|
|
await executeChainWorkbench(
|
||
|
|
this.app,
|
||
|
|
activeEditor,
|
||
|
|
activeFile.path,
|
||
|
|
this.chainRoles,
|
||
|
|
this.chainTemplates,
|
||
|
|
undefined,
|
||
|
|
this.settings,
|
||
|
|
this.pluginInstance
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
onClose(): void {
|
||
|
|
const { contentEl } = this;
|
||
|
|
contentEl.empty();
|
||
|
|
}
|
||
|
|
}
|