diff --git a/src/commands.ts b/src/commands.ts index 604bc72..51630a4 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -1,20 +1,14 @@ /** * ClawVault Commands * Command palette registrations - * - * Canvas dashboards removed — Kanban plugin is the task UI. - * Plugin focuses on: status panel, task creation, vault stats. + * + * Plugin focuses on vault health monitoring + quick capture. */ -import { Notice, TFile, TFolder } from "obsidian"; +import { Notice } from "obsidian"; import type ClawVaultPlugin from "./main"; import { COMMAND_IDS, STATUS_VIEW_TYPE } from "./constants"; -import { - BlockedModal, - CaptureModal, - OpenLoopsModal, - TaskModal, -} from "./modals"; +import { CaptureModal } from "./modals"; /** * Register all ClawVault commands @@ -30,25 +24,6 @@ export function registerCommands(plugin: ClawVaultPlugin): void { }, }); - // Add Task command - plugin.addCommand({ - id: COMMAND_IDS.ADD_TASK, - name: "ClawVault: Add Task", - hotkeys: [{ modifiers: ["Ctrl", "Shift"], key: "t" }], - callback: () => { - new TaskModal(plugin.app).open(); - }, - }); - - // View Blocked command - plugin.addCommand({ - id: COMMAND_IDS.VIEW_BLOCKED, - name: "ClawVault: View Blocked Tasks", - callback: () => { - new BlockedModal(plugin.app, plugin.vaultReader).open(); - }, - }); - // Open Status Panel command plugin.addCommand({ id: COMMAND_IDS.OPEN_STATUS_PANEL, @@ -60,7 +35,7 @@ export function registerCommands(plugin: ClawVaultPlugin): void { // Open Kanban Board command plugin.addCommand({ - id: "clawvault-open-kanban", + id: COMMAND_IDS.OPEN_KANBAN_BOARD, name: "ClawVault: Open Kanban Board", callback: () => { void plugin.app.workspace.openLinkText("Board.md", "", "tab"); @@ -76,18 +51,9 @@ export function registerCommands(plugin: ClawVaultPlugin): void { }, }); - // Show Open Loops command - plugin.addCommand({ - id: COMMAND_IDS.SHOW_OPEN_LOOPS, - name: "ClawVault: Show Open Loops", - callback: () => { - new OpenLoopsModal(plugin.app, plugin.vaultReader).open(); - }, - }); - // Setup Graph Colors command plugin.addCommand({ - id: "clawvault-setup-graph-colors", + id: COMMAND_IDS.SETUP_GRAPH_COLORS, name: "ClawVault: Setup graph colors (neural style)", callback: () => { void setupGraphColors(plugin); diff --git a/src/constants.ts b/src/constants.ts index b489895..974488e 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -5,7 +5,6 @@ // View type identifier for the status panel export const STATUS_VIEW_TYPE = "clawvault-status-view"; -export const TASK_BOARD_VIEW_TYPE = "clawvault-task-board"; // Default category colors for graph nodes export const DEFAULT_CATEGORY_COLORS: Record = { @@ -20,34 +19,6 @@ export const DEFAULT_CATEGORY_COLORS: Record = { default: "#7f8c8d", // default gray }; -// Task status values -export const TASK_STATUS = { - OPEN: "open", - IN_PROGRESS: "in-progress", - BLOCKED: "blocked", - DONE: "done", -} as const; - -export type TaskStatus = typeof TASK_STATUS[keyof typeof TASK_STATUS]; - -// Task priority values -export const TASK_PRIORITY = { - CRITICAL: "critical", - HIGH: "high", - MEDIUM: "medium", - LOW: "low", -} as const; - -export type TaskPriority = typeof TASK_PRIORITY[keyof typeof TASK_PRIORITY]; - -// Status icons for display -export const STATUS_ICONS: Record = { - [TASK_STATUS.OPEN]: "○", - [TASK_STATUS.IN_PROGRESS]: "●", - [TASK_STATUS.BLOCKED]: "⊘", - [TASK_STATUS.DONE]: "✓", -}; - // Default refresh interval in milliseconds (60 seconds) export const DEFAULT_REFRESH_INTERVAL = 60000; @@ -58,21 +29,15 @@ export const CLAWVAULT_GRAPH_INDEX = ".clawvault/graph-index.json"; // Default folders export const DEFAULT_FOLDERS = { INBOX: "inbox", - TASKS: "tasks", - BACKLOG: "backlog", } as const; // Command IDs export const COMMAND_IDS = { - GENERATE_DASHBOARD: "clawvault-generate-dashboard", QUICK_CAPTURE: "clawvault-quick-capture", - ADD_TASK: "clawvault-add-task", - VIEW_BLOCKED: "clawvault-view-blocked", OPEN_STATUS_PANEL: "clawvault-open-status-panel", - OPEN_TASK_BOARD: "clawvault-open-task-board", - GENERATE_CANVAS_FROM_TEMPLATE: "clawvault-generate-canvas-from-template", + OPEN_KANBAN_BOARD: "clawvault-open-kanban", REFRESH_STATS: "clawvault-refresh-stats", - SHOW_OPEN_LOOPS: "clawvault-show-open-loops", + SETUP_GRAPH_COLORS: "clawvault-setup-graph-colors", } as const; export const CANVAS_TEMPLATE_IDS = { diff --git a/src/decorations.ts b/src/decorations.ts index 7d31390..94608f3 100644 --- a/src/decorations.ts +++ b/src/decorations.ts @@ -5,7 +5,7 @@ import { TFile } from "obsidian"; import type ClawVaultPlugin from "./main"; -import { DEFAULT_FOLDERS, STATUS_ICONS, TaskStatus, TASK_STATUS } from "./constants"; +import { DEFAULT_FOLDERS } from "./constants"; /** * Manages file explorer decorations @@ -140,14 +140,6 @@ export class FileDecorations { return "📥"; } - // Check if file is a task - if (this.isInFolder(file, DEFAULT_FOLDERS.TASKS)) { - const status = await this.plugin.vaultReader.getTaskStatus(file); - if (status) { - return STATUS_ICONS[status] ?? STATUS_ICONS[TASK_STATUS.OPEN]; - } - } - return null; } @@ -192,14 +184,6 @@ export class FileDecorations { switch (decoration) { case "📥": return "Inbox item"; - case STATUS_ICONS[TASK_STATUS.OPEN]: - return "Open task"; - case STATUS_ICONS[TASK_STATUS.IN_PROGRESS]: - return "Active task"; - case STATUS_ICONS[TASK_STATUS.BLOCKED]: - return "Blocked task"; - case STATUS_ICONS[TASK_STATUS.DONE]: - return "Completed task"; default: return ""; } diff --git a/src/graph-enhancer.ts b/src/graph-enhancer.ts index a2b94c7..fb0f5d4 100644 --- a/src/graph-enhancer.ts +++ b/src/graph-enhancer.ts @@ -5,7 +5,7 @@ import { TFile } from "obsidian"; import type ClawVaultPlugin from "./main"; -import { DEFAULT_CATEGORY_COLORS, TASK_PRIORITY } from "./constants"; +import { DEFAULT_CATEGORY_COLORS } from "./constants"; export class GraphEnhancer { private plugin: ClawVaultPlugin; @@ -143,7 +143,7 @@ export class GraphEnhancer { } const priority = this.getPriority(frontmatter); - const isCritical = priority === TASK_PRIORITY.CRITICAL; + const isCritical = priority === "critical"; graphNode.classList.toggle("clawvault-node-critical", isCritical); } diff --git a/src/main.ts b/src/main.ts index 568cc5c..634ff2b 100644 --- a/src/main.ts +++ b/src/main.ts @@ -7,14 +7,13 @@ import { Plugin, WorkspaceLeaf } from "obsidian"; import { ClawVaultSettings, ClawVaultSettingTab, DEFAULT_SETTINGS } from "./settings"; import { VaultReader } from "./vault-reader"; import { ClawVaultStatusView } from "./status-view"; -// Task board view removed — Kanban plugin handles task visualization import { FileDecorations } from "./decorations"; import { GraphEnhancer } from "./graph-enhancer"; import { registerCommands } from "./commands"; import { + COMMAND_IDS, DEFAULT_CATEGORY_COLORS, STATUS_VIEW_TYPE, - // TASK_BOARD_VIEW_TYPE removed — using Kanban plugin } from "./constants"; export default class ClawVaultPlugin extends Plugin { @@ -37,8 +36,6 @@ export default class ClawVaultPlugin extends Plugin { return new ClawVaultStatusView(leaf, this); }); - // Task board view removed — Kanban plugin is the task UI - // Add ribbon icon this.addRibbonIcon("database", "ClawVault status", () => { void this.activateStatusView(); @@ -87,7 +84,7 @@ export default class ClawVaultPlugin extends Plugin { try { // Trigger the setup command programmatically (this.app as unknown as { commands: { executeCommandById: (id: string) => void } }) - .commands.executeCommandById("clawvault-setup-graph-colors"); + .commands.executeCommandById(COMMAND_IDS.SETUP_GRAPH_COLORS); // Mark as configured so we don't repeat this.settings.graphColorsConfigured = true; @@ -160,8 +157,6 @@ export default class ClawVaultPlugin extends Plugin { } } - // Task board view removed — use Kanban plugin + Board.md - /** * Update status bar visibility based on settings */ @@ -183,9 +178,8 @@ export default class ClawVaultPlugin extends Plugin { try { const stats = await this.vaultReader.getVaultStats(); - const activeTaskCount = stats.tasks.active + stats.tasks.open; this.statusBarItem.setText( - `🐘 ${stats.nodeCount.toLocaleString()} nodes · ${activeTaskCount} tasks` + `🐘 ${stats.nodeCount.toLocaleString()} nodes · ${stats.edgeCount.toLocaleString()} edges` ); } catch { this.statusBarItem.setText("🐘 ClawVault"); @@ -234,8 +228,6 @@ export default class ClawVaultPlugin extends Plugin { } } - // Task board removed — Kanban plugin handles task visualization - // Update file decorations if (this.fileDecorations) { await this.fileDecorations.decorateAllFiles(); diff --git a/src/modals/blocked-modal.ts b/src/modals/blocked-modal.ts deleted file mode 100644 index 0188e38..0000000 --- a/src/modals/blocked-modal.ts +++ /dev/null @@ -1,154 +0,0 @@ -/** - * Blocked Tasks Modal - * Modal for viewing all blocked tasks - */ - -import { App, Modal, TFile } from "obsidian"; -import type { VaultReader, TaskFrontmatter } from "../vault-reader"; - -interface BlockedTask { - file: TFile; - frontmatter: TaskFrontmatter; -} - -/** - * Modal for viewing blocked tasks - */ -export class BlockedModal extends Modal { - private vaultReader: VaultReader; - - constructor(app: App, vaultReader: VaultReader) { - super(app); - this.vaultReader = vaultReader; - } - - async onOpen(): Promise { - const { contentEl } = this; - contentEl.addClass("clawvault-blocked-modal"); - - contentEl.createEl("h2", { text: "⊘ Blocked tasks" }); - - // Show loading state - const loadingEl = contentEl.createDiv({ cls: "clawvault-loading" }); - loadingEl.setText("Loading blocked tasks..."); - - try { - const blockedTasks = await this.vaultReader.getBlockedTasks(); - loadingEl.remove(); - - if (blockedTasks.length === 0) { - this.renderEmptyState(); - } else { - this.renderBlockedTasks(blockedTasks); - } - } catch (error) { - loadingEl.remove(); - this.renderError(error); - } - } - - onClose(): void { - const { contentEl } = this; - contentEl.empty(); - } - - /** - * Render empty state when no blocked tasks - */ - private renderEmptyState(): void { - const { contentEl } = this; - const emptyEl = contentEl.createDiv({ cls: "clawvault-empty-state" }); - emptyEl.createEl("p", { text: "No blocked tasks found." }); - emptyEl.createEl("p", { - text: "Tasks with status: blocked will appear here.", - cls: "clawvault-empty-hint", - }); - } - - /** - * Render the list of blocked tasks - */ - private renderBlockedTasks(tasks: BlockedTask[]): void { - const { contentEl } = this; - - const summary = contentEl.createDiv({ cls: "clawvault-blocked-summary" }); - summary.setText(`${tasks.length} blocked task${tasks.length === 1 ? "" : "s"}`); - - const listEl = contentEl.createDiv({ cls: "clawvault-blocked-list" }); - - for (const task of tasks) { - const taskEl = listEl.createDiv({ cls: "clawvault-blocked-item" }); - - // Task title/name - const titleEl = taskEl.createDiv({ cls: "clawvault-blocked-title" }); - const titleLink = titleEl.createEl("a", { - text: task.frontmatter.title ?? task.file.basename, - cls: "clawvault-blocked-link", - }); - titleLink.addEventListener("click", (e) => { - e.preventDefault(); - this.close(); - void this.app.workspace.openLinkText(task.file.path, "", true); - }); - - // Task metadata - const metaEl = taskEl.createDiv({ cls: "clawvault-blocked-meta" }); - - if (task.frontmatter.project) { - metaEl.createSpan({ - text: `Project: ${task.frontmatter.project}`, - cls: "clawvault-blocked-project", - }); - } - - if (task.frontmatter.priority) { - const priorityClass = `clawvault-priority-${task.frontmatter.priority}`; - metaEl.createSpan({ - text: `Priority: ${task.frontmatter.priority}`, - cls: `clawvault-blocked-priority ${priorityClass}`, - }); - } - - // Blocked by info - if (task.frontmatter.blocked_by) { - const blockedBy = Array.isArray(task.frontmatter.blocked_by) - ? task.frontmatter.blocked_by.join(", ") - : task.frontmatter.blocked_by; - - const blockedByEl = taskEl.createDiv({ cls: "clawvault-blocked-by" }); - blockedByEl.createSpan({ text: "Blocked by: " }); - blockedByEl.createSpan({ - text: blockedBy, - cls: "clawvault-blocked-by-value", - }); - } - - // Due date if present - if (task.frontmatter.due) { - const dueEl = taskEl.createDiv({ cls: "clawvault-blocked-due" }); - dueEl.setText(`Due: ${task.frontmatter.due}`); - } - } - - // Close button - const buttonContainer = contentEl.createDiv({ cls: "clawvault-modal-buttons" }); - const closeBtn = buttonContainer.createEl("button", { - text: "Close", - cls: "mod-cta", - }); - closeBtn.addEventListener("click", () => this.close()); - } - - /** - * Render error state - */ - private renderError(error: unknown): void { - const { contentEl } = this; - const errorEl = contentEl.createDiv({ cls: "clawvault-error" }); - errorEl.createEl("p", { text: "Failed to load blocked tasks." }); - errorEl.createEl("p", { - text: error instanceof Error ? error.message : "Unknown error", - cls: "clawvault-error-details", - }); - } -} diff --git a/src/modals/index.ts b/src/modals/index.ts index 3b9fbd8..530535d 100644 --- a/src/modals/index.ts +++ b/src/modals/index.ts @@ -4,7 +4,3 @@ */ export { CaptureModal } from "./capture-modal"; -export { TaskModal } from "./task-modal"; -export { BlockedModal } from "./blocked-modal"; -// Template modal removed — canvas dashboards deprecated in favor of Kanban -export { OpenLoopsModal } from "./open-loops-modal"; diff --git a/src/modals/open-loops-modal.ts b/src/modals/open-loops-modal.ts deleted file mode 100644 index 9502784..0000000 --- a/src/modals/open-loops-modal.ts +++ /dev/null @@ -1,93 +0,0 @@ -/** - * Open Loops Modal - * Lists aging non-completed tasks (> 7 days old) - */ - -import { App, Modal } from "obsidian"; -import type { ParsedTask, VaultReader } from "../vault-reader"; - -export class OpenLoopsModal extends Modal { - private vaultReader: VaultReader; - - constructor(app: App, vaultReader: VaultReader) { - super(app); - this.vaultReader = vaultReader; - } - - async onOpen(): Promise { - const { contentEl } = this; - contentEl.empty(); - contentEl.addClass("clawvault-open-loops-modal"); - contentEl.createEl("h2", { text: "Open loops" }); - - const loadingEl = contentEl.createDiv({ cls: "clawvault-loading" }); - loadingEl.setText("Loading open loops..."); - - try { - const openLoops = await this.vaultReader.getOpenLoops(7); - loadingEl.remove(); - this.renderOpenLoops(openLoops); - } catch (error) { - loadingEl.remove(); - contentEl.createEl("p", { - text: - error instanceof Error - ? `Failed to load open loops: ${error.message}` - : "Failed to load open loops.", - cls: "clawvault-error-details", - }); - } - } - - onClose(): void { - this.contentEl.empty(); - } - - private renderOpenLoops(tasks: ParsedTask[]): void { - const { contentEl } = this; - - if (tasks.length === 0) { - const emptyState = contentEl.createDiv({ cls: "clawvault-empty-state" }); - emptyState.createEl("p", { text: "No open loops older than 7 days." }); - } else { - const summary = contentEl.createDiv({ cls: "clawvault-blocked-summary" }); - summary.setText( - `${tasks.length} open task${tasks.length === 1 ? "" : "s"} older than 7 days` - ); - - const listEl = contentEl.createDiv({ cls: "clawvault-blocked-list" }); - for (const task of tasks) { - const itemEl = listEl.createDiv({ cls: "clawvault-blocked-item clawvault-open-loop-item" }); - const ageDays = this.getAgeInDays(task.createdAt ?? new Date(task.file.stat.ctime)); - - const titleLink = itemEl.createEl("a", { - text: task.frontmatter.title ?? task.file.basename, - cls: "clawvault-blocked-link", - }); - titleLink.addEventListener("click", (event) => { - event.preventDefault(); - this.close(); - void this.app.workspace.openLinkText(task.file.path, "", "tab"); - }); - - const meta = itemEl.createDiv({ cls: "clawvault-blocked-meta" }); - meta.createSpan({ text: `Status: ${task.status}` }); - if (task.frontmatter.project) { - meta.createSpan({ text: `Project: ${task.frontmatter.project}` }); - } - meta.createSpan({ text: `${ageDays}d open`, cls: "clawvault-open-loop-age" }); - } - } - - const actionsEl = contentEl.createDiv({ cls: "clawvault-modal-buttons" }); - actionsEl.createEl("button", { - text: "Close", - cls: "mod-cta", - }).addEventListener("click", () => this.close()); - } - - private getAgeInDays(createdAt: Date): number { - const diff = Date.now() - createdAt.getTime(); - return Math.max(0, Math.floor(diff / (24 * 60 * 60 * 1000))); - } -} diff --git a/src/modals/task-modal.ts b/src/modals/task-modal.ts deleted file mode 100644 index d3ac16c..0000000 --- a/src/modals/task-modal.ts +++ /dev/null @@ -1,178 +0,0 @@ -/** - * Add Task Modal - * Modal for creating new tasks - */ - -import { App, Modal, Notice, Setting } from "obsidian"; -import { DEFAULT_FOLDERS, TASK_PRIORITY, TASK_STATUS, TaskPriority } from "../constants"; - -/** - * Modal for adding new tasks - */ -export class TaskModal extends Modal { - private title = ""; - private project = ""; - private priority: TaskPriority = TASK_PRIORITY.MEDIUM; - private description = ""; - - constructor(app: App) { - super(app); - } - - onOpen(): void { - const { contentEl } = this; - contentEl.addClass("clawvault-task-modal"); - - contentEl.createEl("h2", { text: "Add task" }); - - // Title input (required) - new Setting(contentEl) - .setName("Title") - .setDesc("Task title (required)") - .addText((text) => - text - .setPlaceholder("Enter task title") - .onChange((value) => { - this.title = value; - }) - ); - - // Project input - new Setting(contentEl) - .setName("Project") - .setDesc("Associated project (optional)") - .addText((text) => - text - .setPlaceholder("e.g., my-project") - .onChange((value) => { - this.project = value; - }) - ); - - // Priority dropdown - new Setting(contentEl) - .setName("Priority") - .setDesc("Task priority level") - .addDropdown((dropdown) => - dropdown - .addOption(TASK_PRIORITY.CRITICAL, "Critical") - .addOption(TASK_PRIORITY.HIGH, "High") - .addOption(TASK_PRIORITY.MEDIUM, "Medium") - .addOption(TASK_PRIORITY.LOW, "Low") - .setValue(this.priority) - .onChange((value) => { - this.priority = value as TaskPriority; - }) - ); - - // Description textarea - const descSetting = new Setting(contentEl) - .setName("Description") - .setDesc("Task description (optional)"); - - const textareaContainer = descSetting.controlEl.createDiv(); - const textarea = textareaContainer.createEl("textarea", { - cls: "clawvault-task-textarea", - attr: { - placeholder: "Enter task description...", - rows: "4", - }, - }); - textarea.addEventListener("input", (e) => { - this.description = (e.target as HTMLTextAreaElement).value; - }); - - // Buttons - const buttonContainer = contentEl.createDiv({ cls: "clawvault-modal-buttons" }); - - const cancelBtn = buttonContainer.createEl("button", { text: "Cancel" }); - cancelBtn.addEventListener("click", () => this.close()); - - const saveBtn = buttonContainer.createEl("button", { - text: "Create task", - cls: "mod-cta", - }); - saveBtn.addEventListener("click", () => { - void this.save(); - }); - - // Handle Enter key (Ctrl/Cmd + Enter to save) - contentEl.addEventListener("keydown", (e) => { - if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) { - e.preventDefault(); - void this.save(); - } - if (e.key === "Escape") { - this.close(); - } - }); - } - - onClose(): void { - const { contentEl } = this; - contentEl.empty(); - } - - /** - * Save the task - */ - private async save(): Promise { - if (!this.title.trim()) { - new Notice("Please enter a task title."); - return; - } - - try { - // Generate filename from title - const sanitizedTitle = this.title - .trim() - .toLowerCase() - .replace(/[\\/:*?"<>|]/g, "-") - .replace(/\s+/g, "-") - .slice(0, 50); - const timestamp = Date.now(); - const filename = `${sanitizedTitle}-${timestamp}.md`; - const filepath = `${DEFAULT_FOLDERS.TASKS}/${filename}`; - - // Create frontmatter - const frontmatterLines = [ - "---", - `title: "${this.title}"`, - `status: ${TASK_STATUS.OPEN}`, - `priority: ${this.priority}`, - `created: ${new Date().toISOString()}`, - ]; - - if (this.project.trim()) { - frontmatterLines.push(`project: "${this.project.trim()}"`); - } - - frontmatterLines.push("---"); - const frontmatter = frontmatterLines.join("\n"); - - // Create file content - let fileContent = `${frontmatter}\n\n# ${this.title}\n`; - if (this.description.trim()) { - fileContent += `\n${this.description}\n`; - } - - // Ensure tasks folder exists - const tasksFolder = this.app.vault.getAbstractFileByPath(DEFAULT_FOLDERS.TASKS); - if (!tasksFolder) { - await this.app.vault.createFolder(DEFAULT_FOLDERS.TASKS); - } - - // Create the file - const file = await this.app.vault.create(filepath, fileContent); - - new Notice(`Task created: ${this.title}`); - this.close(); - - // Open the new task file - await this.app.workspace.openLinkText(file.path, "", true); - } catch (error) { - console.error("ClawVault: Failed to create task:", error); - new Notice("Failed to create task. Check console for details."); - } - } -} diff --git a/src/settings.ts b/src/settings.ts index 1a291b4..546a4e3 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -96,7 +96,7 @@ export class ClawVaultSettingTab extends PluginSettingTab { new Setting(containerEl) .setName("Show status bar") - .setDesc("Show node count and task count in the status bar") + .setDesc("Show graph node and edge counts in the status bar") .addToggle((toggle) => toggle .setValue(this.plugin.settings.showStatusBar) @@ -109,7 +109,7 @@ export class ClawVaultSettingTab extends PluginSettingTab { new Setting(containerEl) .setName("Show file decorations") - .setDesc("Show status icons on task files in the file explorer") + .setDesc("Show inbox indicators in the file explorer") .addToggle((toggle) => toggle .setValue(this.plugin.settings.showFileDecorations) @@ -178,7 +178,7 @@ export class ClawVaultSettingTab extends PluginSettingTab { new Setting(containerEl).setName("About").setHeading(); containerEl.createEl("p", { - text: "ClawVault is a visual memory management plugin for Obsidian. It provides colored graph nodes, task tracking, and vault statistics.", + text: "ClawVault is a visual memory health plugin for Obsidian. It provides graph insights, quick capture, and vault statistics.", }); containerEl.createEl("p", { text: "For more information, visit the ClawVault documentation.", diff --git a/src/status-view.ts b/src/status-view.ts index 60a4f11..e249794 100644 --- a/src/status-view.ts +++ b/src/status-view.ts @@ -3,16 +3,13 @@ * Sidebar panel showing vault statistics */ -import { ItemView, TFile, WorkspaceLeaf } from "obsidian"; +import { ItemView, WorkspaceLeaf } from "obsidian"; import type ClawVaultPlugin from "./main"; import { COMMAND_IDS, STATUS_VIEW_TYPE } from "./constants"; -import type { ObservationSession, ParsedTask, VaultStats } from "./vault-reader"; +import type { VaultStats } from "./vault-reader"; interface StatusViewData { stats: VaultStats; - backlogItems: ParsedTask[]; - recentSessions: ObservationSession[]; - openLoops: ParsedTask[]; graphTypes: Record; todayObs: { count: number; categories: string[] }; } @@ -65,19 +62,13 @@ export class ClawVaultStatusView extends ItemView { this.statusContentEl.empty(); try { - const [stats, backlogItems, recentSessions, openLoops, graphTypes, todayObs] = await Promise.all([ + const [stats, graphTypes, todayObs] = await Promise.all([ this.plugin.vaultReader.getVaultStats(), - this.plugin.vaultReader.getBacklogTasks(5), - this.plugin.vaultReader.getRecentObservationSessions(5), - this.plugin.vaultReader.getOpenLoops(7), this.plugin.vaultReader.getGraphTypeSummary(), this.plugin.vaultReader.getTodayObservations(), ]); this.renderStats({ stats, - backlogItems, - recentSessions, - openLoops, graphTypes, todayObs, }); @@ -91,7 +82,7 @@ export class ClawVaultStatusView extends ItemView { */ private renderStats(data: StatusViewData): void { if (!this.statusContentEl) return; - const { stats, backlogItems, recentSessions, openLoops, graphTypes, todayObs } = data; + const { stats, graphTypes, todayObs } = data; // Header const header = this.statusContentEl.createDiv({ cls: "clawvault-status-header" }); @@ -105,131 +96,45 @@ export class ClawVaultStatusView extends ItemView { cls: "clawvault-status-vault-name", }); vaultInfo.createEl("div", { - text: `Files: ${this.formatNumber(stats.fileCount)} | Nodes: ${this.formatNumber(stats.nodeCount)} | Edges: ${this.formatNumber(stats.edgeCount)}`, + text: `Files: ${this.formatNumber(stats.fileCount)}`, cls: "clawvault-status-counts", }); // Memory Graph section - if (stats.nodeCount > 0 || Object.keys(graphTypes).length > 0) { - const graphSection = this.statusContentEl.createDiv({ cls: "clawvault-status-section" }); - graphSection.createEl("h4", { text: "Memory Graph" }); + const graphSection = this.statusContentEl.createDiv({ cls: "clawvault-status-section" }); + graphSection.createEl("h4", { text: "Memory Graph" }); - const graphStats = graphSection.createDiv({ cls: "clawvault-graph-stats" }); - graphStats.createDiv({ - text: `🔗 ${this.formatNumber(stats.nodeCount)} nodes · ${this.formatNumber(stats.edgeCount)} edges`, - cls: "clawvault-graph-totals", - }); + const graphStats = graphSection.createDiv({ cls: "clawvault-graph-stats" }); + graphStats.createDiv({ + text: `🔗 ${this.formatNumber(stats.nodeCount)} nodes · ${this.formatNumber(stats.edgeCount)} edges`, + cls: "clawvault-graph-totals", + }); - // Node type breakdown (top 5) - const sortedTypes = Object.entries(graphTypes) - .sort((a, b) => b[1] - a[1]) - .slice(0, 6); - if (sortedTypes.length > 0) { - const typeGrid = graphSection.createDiv({ cls: "clawvault-graph-type-grid" }); - for (const [type, count] of sortedTypes) { - const typeEl = typeGrid.createDiv({ cls: "clawvault-graph-type-item" }); - typeEl.createSpan({ text: `${count}`, cls: "clawvault-graph-type-count" }); - typeEl.createSpan({ text: ` ${type}`, cls: "clawvault-graph-type-label" }); - } + // Node type breakdown + const sortedTypes = Object.entries(graphTypes).sort((a, b) => b[1] - a[1]); + if (sortedTypes.length > 0) { + const typeGrid = graphSection.createDiv({ cls: "clawvault-graph-type-grid" }); + for (const [type, count] of sortedTypes) { + const typeEl = typeGrid.createDiv({ cls: "clawvault-graph-type-item" }); + typeEl.createSpan({ text: `${count}`, cls: "clawvault-graph-type-count" }); + typeEl.createSpan({ text: ` ${type}`, cls: "clawvault-graph-type-label" }); } } // Today's observations - if (todayObs.count > 0) { - const obsSection = this.statusContentEl.createDiv({ cls: "clawvault-status-section" }); - obsSection.createDiv({ - text: `🔭 ${todayObs.count} observation${todayObs.count === 1 ? "" : "s"} today`, - cls: "clawvault-obs-today", - }); - if (todayObs.categories.length > 0) { - obsSection.createDiv({ - text: `→ ${todayObs.categories.join(", ")}`, - cls: "clawvault-obs-categories", - }); - } - } - - // Kanban board link - const boardFile = this.app.vault.getAbstractFileByPath("Board.md"); - if (boardFile instanceof TFile) { - const kanbanSection = this.statusContentEl.createDiv({ cls: "clawvault-status-section" }); - const kanbanLink = kanbanSection.createEl("a", { - text: "📋 Open Kanban Board", - cls: "clawvault-kanban-link", - }); - kanbanLink.addEventListener("click", (event) => { - event.preventDefault(); - void this.app.workspace.openLinkText("Board.md", "", "tab"); - }); - } - - // Tasks section - if (stats.tasks.total > 0) { - const tasksSection = this.statusContentEl.createDiv({ cls: "clawvault-status-section" }); - tasksSection.createEl("h4", { text: "Tasks" }); - - const taskStats = tasksSection.createDiv({ cls: "clawvault-task-stats" }); - - // Active, open, blocked - const statusLine = taskStats.createDiv({ cls: "clawvault-task-status-line" }); - statusLine.createSpan({ text: `● ${stats.tasks.active} active`, cls: "clawvault-task-active" }); - statusLine.createSpan({ text: " | " }); - statusLine.createSpan({ text: `○ ${stats.tasks.open} open`, cls: "clawvault-task-open" }); - statusLine.createSpan({ text: " | " }); - statusLine.createSpan({ text: `⊘ ${stats.tasks.blocked} blocked`, cls: "clawvault-task-blocked" }); - - // Completed with percentage - const completedPct = stats.tasks.total > 0 - ? Math.round((stats.tasks.completed / stats.tasks.total) * 100) - : 0; - taskStats.createDiv({ - text: `✓ ${stats.tasks.completed} completed (${completedPct}%)`, - cls: "clawvault-task-completed", - }); - - // Progress bar - const progressBar = taskStats.createDiv({ cls: "clawvault-progress-bar" }); - const progressFill = progressBar.createDiv({ cls: "clawvault-progress-fill" }); - progressFill.style.width = `${completedPct}%`; - } - - // Backlog section - const backlogSection = this.statusContentEl.createDiv({ cls: "clawvault-status-section" }); - backlogSection.createEl("h4", { - text: `Backlog (${stats.tasks.open})`, + const obsSection = this.statusContentEl.createDiv({ cls: "clawvault-status-section" }); + obsSection.createEl("h4", { text: "Observations today" }); + obsSection.createDiv({ + text: `${todayObs.count} observation${todayObs.count === 1 ? "" : "s"}`, + cls: "clawvault-obs-today", + }); + obsSection.createDiv({ + text: + todayObs.categories.length > 0 + ? `Categories: ${todayObs.categories.join(", ")}` + : "Categories: none", + cls: "clawvault-obs-categories", }); - - if (backlogItems.length === 0) { - backlogSection.createDiv({ - text: "No backlog tasks", - cls: "clawvault-empty-hint", - }); - } else { - const backlogList = backlogSection.createDiv({ cls: "clawvault-status-list" }); - for (const task of backlogItems) { - const item = backlogList.createDiv({ cls: "clawvault-status-list-item" }); - const link = item.createEl("a", { - text: task.frontmatter.title ?? task.file.basename, - cls: "clawvault-blocked-link", - }); - link.addEventListener("click", (event) => { - event.preventDefault(); - void this.app.workspace.openLinkText(task.file.path, "", "tab"); - }); - - const meta = item.createDiv({ cls: "clawvault-status-list-meta" }); - if (task.frontmatter.project) { - meta.createSpan({ text: task.frontmatter.project }); - } - if (task.frontmatter.priority) { - if (meta.childElementCount > 0) meta.createSpan({ text: " · " }); - meta.createSpan({ - text: `${task.frontmatter.priority}`, - cls: `clawvault-priority-${task.frontmatter.priority}`, - }); - } - } - } // Inbox section if (stats.inboxCount > 0) { @@ -240,98 +145,38 @@ export class ClawVaultStatusView extends ItemView { }); } - // Last activity section - const activitySection = this.statusContentEl.createDiv({ cls: "clawvault-status-section clawvault-activity" }); - - if (stats.lastObservation) { - activitySection.createDiv({ - text: `Last observation: ${this.formatTimeAgo(stats.lastObservation)}`, - }); - } - - if (stats.lastReflection) { - activitySection.createDiv({ - text: `Last reflection: ${stats.lastReflection}`, - }); - } - - // Recent observation sessions - const recentActivitySection = this.statusContentEl.createDiv({ - cls: "clawvault-status-section", + // Last observation section + const activitySection = this.statusContentEl.createDiv({ + cls: "clawvault-status-section clawvault-activity", }); - recentActivitySection.createEl("h4", { text: "Recent activity" }); - if (recentSessions.length === 0) { - recentActivitySection.createDiv({ - text: "No observed sessions found.", - cls: "clawvault-empty-hint", - }); - } else { - const sessionsList = recentActivitySection.createDiv({ cls: "clawvault-status-list" }); - for (const session of recentSessions) { - const row = sessionsList.createDiv({ cls: "clawvault-status-list-item" }); - const link = row.createEl("a", { - text: session.file.basename, - cls: "clawvault-blocked-link", - }); - link.addEventListener("click", (event) => { - event.preventDefault(); - void this.app.workspace.openLinkText(session.file.path, "", "tab"); - }); - row.createDiv({ - text: session.timestamp.toLocaleString(), - cls: "clawvault-status-list-meta", - }); - } - } - - // Open loops section - const openLoopsSection = this.statusContentEl.createDiv({ cls: "clawvault-status-section" }); - openLoopsSection.createEl("h4", { - text: `Open loops (${openLoops.length})`, + activitySection.createDiv({ + text: `Last observation: ${ + stats.lastObservation ? stats.lastObservation.toLocaleString() : "none" + }`, }); - if (openLoops.length === 0) { - openLoopsSection.createDiv({ - text: "No open loops older than 7 days.", - cls: "clawvault-empty-hint", - }); - } else { - const loopList = openLoopsSection.createDiv({ cls: "clawvault-status-list" }); - for (const task of openLoops.slice(0, 5)) { - const row = loopList.createDiv({ - cls: "clawvault-status-list-item clawvault-open-loop-warning", - }); - const ageDays = this.getAgeInDays(task.createdAt ?? new Date(task.file.stat.ctime)); - const link = row.createEl("a", { - text: task.frontmatter.title ?? task.file.basename, - cls: "clawvault-blocked-link", - }); - link.addEventListener("click", (event) => { - event.preventDefault(); - void this.app.workspace.openLinkText(task.file.path, "", "tab"); - }); - row.createDiv({ - text: `${ageDays}d open`, - cls: "clawvault-status-list-meta", - }); - } - } + // Kanban board link + const kanbanSection = this.statusContentEl.createDiv({ cls: "clawvault-status-section" }); + const kanbanLink = kanbanSection.createEl("a", { + text: "📋 Open Kanban Board", + cls: "clawvault-kanban-link", + }); + kanbanLink.addEventListener("click", (event) => { + event.preventDefault(); + void this.app.workspace.openLinkText("Board.md", "", "tab"); + }); const quickActionsSection = this.statusContentEl.createDiv({ cls: "clawvault-status-section clawvault-status-quick-actions", }); quickActionsSection.createEl("h4", { text: "Quick actions" }); const actionsRow = quickActionsSection.createDiv({ cls: "clawvault-quick-action-row" }); - this.renderQuickActionButton( - actionsRow, - "Add Task", - COMMAND_IDS.ADD_TASK - ); this.renderQuickActionButton( actionsRow, "Quick Capture", COMMAND_IDS.QUICK_CAPTURE ); + this.renderQuickActionButton(actionsRow, "Refresh", COMMAND_IDS.REFRESH_STATS); // Refresh button const footer = this.statusContentEl.createDiv({ cls: "clawvault-status-footer" }); @@ -340,7 +185,7 @@ export class ClawVaultStatusView extends ItemView { cls: "clawvault-refresh-btn", }); refreshBtn.addEventListener("click", () => { - void this.refresh(); + void this.plugin.refreshAll(); }); } @@ -366,7 +211,7 @@ export class ClawVaultStatusView extends ItemView { cls: "clawvault-refresh-btn", }); refreshBtn.addEventListener("click", () => { - void this.refresh(); + void this.plugin.refreshAll(); }); } @@ -377,28 +222,6 @@ export class ClawVaultStatusView extends ItemView { return num.toLocaleString(); } - /** - * Format a date as time ago - */ - private formatTimeAgo(date: Date): string { - const now = new Date(); - const diffMs = now.getTime() - date.getTime(); - const diffMins = Math.floor(diffMs / 60000); - const diffHours = Math.floor(diffMins / 60); - const diffDays = Math.floor(diffHours / 24); - - if (diffMins < 1) return "just now"; - if (diffMins < 60) return `${diffMins}m ago`; - if (diffHours < 24) return `${diffHours}h ago`; - if (diffDays < 7) return `${diffDays}d ago`; - return date.toLocaleDateString(); - } - - private getAgeInDays(date: Date): number { - const diffMs = Date.now() - date.getTime(); - return Math.max(0, Math.floor(diffMs / (1000 * 60 * 60 * 24))); - } - private renderQuickActionButton( parent: HTMLElement, label: string, diff --git a/src/vault-reader.ts b/src/vault-reader.ts index bc5f39f..f1914e7 100644 --- a/src/vault-reader.ts +++ b/src/vault-reader.ts @@ -1,17 +1,10 @@ /** * ClawVault Vault Reader - * Reads .clawvault.json, graph-index.json, and task files + * Reads .clawvault.json and graph-index.json for vault health metrics */ -import { App, TFile, TFolder, parseYaml } from "obsidian"; -import { - CLAWVAULT_CONFIG_FILE, - CLAWVAULT_GRAPH_INDEX, - DEFAULT_FOLDERS, - TaskPriority, - TaskStatus, - TASK_STATUS, -} from "./constants"; +import { App, TFile, TFolder } from "obsidian"; +import { CLAWVAULT_CONFIG_FILE, CLAWVAULT_GRAPH_INDEX, DEFAULT_FOLDERS } from "./constants"; // Graph node structure from graph-index.json export interface GraphNode { @@ -51,28 +44,6 @@ export interface ClawVaultConfig { version?: string; } -// Task frontmatter structure -export interface TaskFrontmatter { - status?: TaskStatus; - priority?: TaskPriority | string; - project?: string; - owner?: string; - blocked_by?: string | string[]; - due?: string; - title?: string; - tags?: string[] | string; - created?: string; - completed?: string | null; - source?: string; -} - -export interface ParsedTask { - file: TFile; - frontmatter: TaskFrontmatter; - status: TaskStatus; - createdAt: Date | null; -} - export interface ObservationSession { file: TFile; timestamp: Date; @@ -84,16 +55,8 @@ export interface VaultStats { fileCount: number; nodeCount: number; edgeCount: number; - tasks: { - active: number; - open: number; - blocked: number; - completed: number; - total: number; - }; inboxCount: number; lastObservation?: Date; - lastReflection?: string; categories: string[]; } @@ -192,10 +155,12 @@ export class VaultReader { */ async getTodayObservations(): Promise<{ count: number; categories: string[] }> { const todayStr = new Date().toISOString().split("T")[0] ?? ""; - if (!todayStr) return { count: 0, categories: [] }; + if (!todayStr) { + return { count: 0, categories: [] }; + } + const seenPaths = new Set(); const categories = new Set(); - let count = 0; const todayStart = new Date(todayStr).getTime(); // Check ledger/observations for today's files @@ -205,23 +170,29 @@ export class VaultReader { if (folder instanceof TFolder) { for (const child of folder.children) { if (child instanceof TFile && child.basename.startsWith(todayStr)) { - count++; + seenPaths.add(child.path); } } } - } catch { /* no ledger dir */ } + } catch { + // Folder does not exist in every vault layout. + } // Check observations folder for today's files - const obsFiles = this.getFilesInFolder("observations"); - for (const f of obsFiles) { - if (f.basename.startsWith(todayStr) || f.stat.mtime >= todayStart) { - count++; - const parts = f.path.split("/"); - if (parts.length > 2 && parts[1]) categories.add(parts[1]); + const observationFiles = this.getFilesInFolder("observations"); + for (const file of observationFiles) { + if (file.basename.startsWith(todayStr) || file.stat.mtime >= todayStart) { + seenPaths.add(file.path); + const parts = file.path.split("/"); + if (parts.length > 2 && parts[1]) { + categories.add(parts[1]); + } else if (parts[0]) { + categories.add(parts[0]); + } } } - return { count, categories: Array.from(categories) }; + return { count: seenPaths.size, categories: Array.from(categories) }; } /** @@ -229,7 +200,9 @@ export class VaultReader { */ async getGraphTypeSummary(): Promise> { const graph = await this.readGraphIndex(); - if (!graph) return {}; + if (!graph) { + return {}; + } const typeCounts: Record = {}; for (const node of graph.nodes) { @@ -249,8 +222,8 @@ export class VaultReader { } const files: TFile[] = []; - const collectFiles = (folder: TFolder) => { - for (const child of folder.children) { + const collectFiles = (currentFolder: TFolder) => { + for (const child of currentFolder.children) { if (child instanceof TFile && child.extension === "md") { files.push(child); } else if (child instanceof TFolder) { @@ -263,145 +236,6 @@ export class VaultReader { return files; } - /** - * Parse frontmatter from a file - */ - async parseFrontmatter(file: TFile): Promise { - try { - const content = await this.app.vault.read(file); - const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/); - if (frontmatterMatch?.[1]) { - return parseYaml(frontmatterMatch[1]) as TaskFrontmatter; - } - } catch (error) { - console.warn(`ClawVault: Could not parse frontmatter for ${file.path}:`, error); - } - return null; - } - - /** - * Parse a frontmatter date field into a Date value - */ - private parseDate(value: unknown): Date | null { - if (value instanceof Date) { - return Number.isNaN(value.getTime()) ? null : value; - } - if (typeof value === "number") { - const date = new Date(value); - return Number.isNaN(date.getTime()) ? null : date; - } - if (typeof value === "string" && value.trim().length > 0) { - const date = new Date(value); - return Number.isNaN(date.getTime()) ? null : date; - } - return null; - } - - /** - * Get all tasks from the tasks folder - */ - async getAllTasks(): Promise { - const taskFiles = this.getFilesInFolder(DEFAULT_FOLDERS.TASKS); - const tasks: ParsedTask[] = []; - - for (const file of taskFiles) { - const frontmatter = (await this.parseFrontmatter(file)) ?? {}; - const status = frontmatter.status ?? TASK_STATUS.OPEN; - const createdAt = this.parseDate(frontmatter.created) ?? new Date(file.stat.ctime); - tasks.push({ - file, - frontmatter, - status, - createdAt, - }); - } - - return tasks; - } - - /** - * Get task statistics from task files - */ - async getTaskStats(): Promise { - const stats = { - active: 0, - open: 0, - blocked: 0, - completed: 0, - total: 0, - }; - - const tasks = await this.getAllTasks(); - - for (const task of tasks) { - stats.total++; - - switch (task.status) { - case TASK_STATUS.IN_PROGRESS: - stats.active++; - break; - case TASK_STATUS.OPEN: - stats.open++; - break; - case TASK_STATUS.BLOCKED: - stats.blocked++; - break; - case TASK_STATUS.DONE: - stats.completed++; - break; - } - } - - return stats; - } - - /** - * Get all blocked tasks with their details - */ - async getBlockedTasks(): Promise> { - const blockedTasks: Array<{ file: TFile; frontmatter: TaskFrontmatter }> = []; - - for (const task of await this.getAllTasks()) { - if (task.status === TASK_STATUS.BLOCKED) { - blockedTasks.push({ file: task.file, frontmatter: task.frontmatter }); - } - } - - return blockedTasks; - } - - /** - * Get backlog tasks (open status), newest first - */ - async getBacklogTasks(limit = 5): Promise { - const openTasks = (await this.getAllTasks()) - .filter((task) => task.status === TASK_STATUS.OPEN) - .sort((a, b) => b.file.stat.mtime - a.file.stat.mtime); - - return openTasks.slice(0, limit); - } - - /** - * Get open loops: non-completed tasks older than a threshold - */ - async getOpenLoops(daysOpen = 7): Promise { - const ageThreshold = Date.now() - daysOpen * 24 * 60 * 60 * 1000; - const tasks = await this.getAllTasks(); - return tasks - .filter((task) => { - if (task.status === TASK_STATUS.DONE) { - return false; - } - const createdAt = task.createdAt ?? new Date(task.file.stat.ctime); - return createdAt.getTime() < ageThreshold; - }) - .sort((a, b) => { - const aTime = a.createdAt?.getTime() ?? a.file.stat.ctime; - const bTime = b.createdAt?.getTime() ?? b.file.stat.ctime; - return aTime - bTime; - }); - } - /** * Get recent observation sessions by file modified time */ @@ -416,49 +250,20 @@ export class VaultReader { })); } - /** - * Get recent decisions updated within a date window - */ - getRecentDecisionFiles(days = 7, limit = 10): TFile[] { - const cutoff = Date.now() - days * 24 * 60 * 60 * 1000; - return this.getFilesInFolder("decisions") - .filter((file) => file.stat.mtime >= cutoff) - .sort((a, b) => b.stat.mtime - a.stat.mtime) - .slice(0, limit); - } - /** * Get complete vault statistics */ async getVaultStats(): Promise { const config = await this.readConfig(); const graphIndex = await this.readGraphIndex(); - const taskStats = await this.getTaskStats(); const inboxFiles = this.getFilesInFolder(DEFAULT_FOLDERS.INBOX); const allFiles = this.app.vault.getMarkdownFiles(); - // Try to find last observation and reflection let lastObservation: Date | undefined; - let lastReflection: string | undefined; - - // Look for observations folder - const observationFiles = this.getFilesInFolder("observations"); - if (observationFiles.length > 0) { - const sorted = observationFiles.sort((a, b) => b.stat.mtime - a.stat.mtime); - if (sorted[0]) { - lastObservation = new Date(sorted[0].stat.mtime); - } - } - - // Look for reflections folder - const reflectionFiles = this.getFilesInFolder("reflections"); - if (reflectionFiles.length > 0) { - const sorted = reflectionFiles.sort((a, b) => b.stat.mtime - a.stat.mtime); - if (sorted[0]) { - // Extract week number from filename if possible - const weekMatch = sorted[0].basename.match(/week[_-]?(\d+)/i); - lastReflection = weekMatch ? `Week ${weekMatch[1]}` : sorted[0].basename; - } + const observationFiles = this.getFilesInFolder("observations") + .sort((a, b) => b.stat.mtime - a.stat.mtime); + if (observationFiles[0]) { + lastObservation = new Date(observationFiles[0].stat.mtime); } return { @@ -466,10 +271,8 @@ export class VaultReader { fileCount: allFiles.length, nodeCount: graphIndex?.nodes.length ?? 0, edgeCount: graphIndex?.edges.length ?? 0, - tasks: taskStats, inboxCount: inboxFiles.length, lastObservation, - lastReflection, categories: config?.categories ?? [], }; } @@ -481,22 +284,4 @@ export class VaultReader { const config = await this.readConfig(); return config?.name ?? this.app.vault.getName(); } - - /** - * Check if a file is in a specific folder - */ - isFileInFolder(file: TFile, folderPath: string): boolean { - return file.path.startsWith(folderPath + "/") || file.parent?.path === folderPath; - } - - /** - * Get task status for a file - */ - async getTaskStatus(file: TFile): Promise { - if (!this.isFileInFolder(file, DEFAULT_FOLDERS.TASKS)) { - return null; - } - const frontmatter = await this.parseFrontmatter(file); - return frontmatter?.status ?? TASK_STATUS.OPEN; - } }