feat(filesource): implement file-based task recognition system

Add comprehensive FileSource feature that enables files to serve as tasks
based on metadata properties, tags, templates, and path patterns. This
creates a unified task management experience where files can represent
both projects and actionable items.

Key features:
- Multiple recognition strategies (metadata, tags, templates, paths)
- Flexible task property extraction from file metadata
- Dual project-task file capabilities
- Settings UI with live preview and validation
- Integration with existing dataflow architecture
- Configurable task content sources (filename, title, custom fields)
- Support for custom status characters and priority levels

The implementation follows established patterns from ObsidianSource and
IcsSource, ensuring consistency with the existing codebase architecture.
This commit is contained in:
Quorafind 2025-08-20 22:38:16 +08:00
parent b600490a1c
commit 691952a6b3
10 changed files with 2595 additions and 97 deletions

View file

@ -4,6 +4,7 @@ import { BaseHabitData } from "../types/habit-card";
import type { RootFilterState } from "../components/task-filter/ViewTaskFilter";
import { IcsManagerConfig } from "../types/ics";
import { TimeParsingConfig } from "../services/time-parsing-service";
import type { FileSourceConfiguration } from "../types/file-source";
// Interface for individual project review settings (If still needed, otherwise remove)
// Keep it for now, in case it's used elsewhere, but it's not part of TaskProgressBarSettings anymore
@ -682,7 +683,7 @@ export interface TaskProgressBarSettings {
// Enhanced Project Configuration
projectConfig: ProjectConfiguration;
// File Parsing Configuration
// File Parsing Configuration (DEPRECATED - use fileSource instead)
fileParsingConfig: FileParsingConfiguration;
// Date Settings
@ -694,8 +695,9 @@ export interface TaskProgressBarSettings {
// Focus all tasks behind heading
focusHeading: string;
// View Settings (Updated Structure)
enableView: boolean;
// Indexer and View Settings
enableIndexer: boolean; // Enable Task Genius indexer for whole vault scanning
enableView: boolean; // Enable Task Genius sidebar views
enableInlineEditor: boolean; // Enable inline editing in task views
enableDynamicMetadataPositioning: boolean; // Enable intelligent metadata positioning based on content length
defaultViewMode: "list" | "tree"; // Global default view mode for all views
@ -760,6 +762,9 @@ export interface TaskProgressBarSettings {
skipOnboarding?: boolean;
completedAt?: string;
};
// FileSource Settings
fileSource: FileSourceConfiguration;
}
/** Define the default settings */
@ -990,7 +995,6 @@ export const DEFAULT_SETTINGS: TaskProgressBarSettings = {
inheritFromFrontmatterForSubtasks: false,
},
// Enhanced Project Configuration
projectConfig: {
enableEnhancedProject: false,
pathMappings: [],
@ -1051,8 +1055,9 @@ export const DEFAULT_SETTINGS: TaskProgressBarSettings = {
// Focus all tasks behind heading
focusHeading: "",
// View Defaults (Updated Structure)
enableView: true,
// Indexer and View Defaults
enableIndexer: true, // Enable indexer by default
enableView: true, // Enable view by default
enableInlineEditor: true, // Enable inline editing by default
enableDynamicMetadataPositioning: true, // Enable intelligent metadata positioning by default
defaultViewMode: "list", // Global default view mode for all views
@ -1484,6 +1489,102 @@ export const DEFAULT_SETTINGS: TaskProgressBarSettings = {
configMode: 'beginner',
skipOnboarding: false,
completedAt: ""
},
// FileSource Defaults - Import from FileSourceConfig
fileSource: {
enabled: false,
recognitionStrategies: {
metadata: {
enabled: true,
taskFields: ["dueDate", "status", "priority", "assigned"],
requireAllFields: false
},
tags: {
enabled: true,
taskTags: ["#task", "#actionable", "#todo"],
matchMode: "exact"
},
templates: {
enabled: false,
templatePaths: ["Templates/Task Template.md"],
checkTemplateMetadata: true
},
paths: {
enabled: false,
taskPaths: ["Projects/", "Tasks/"],
matchMode: "prefix"
}
},
fileTaskProperties: {
contentSource: "filename",
stripExtension: true,
defaultStatus: " ",
defaultPriority: undefined
},
relationships: {
enableChildRelationships: true,
enableMetadataInheritance: true,
inheritanceFields: ["project", "priority", "context"]
},
performance: {
enableWorkerProcessing: true,
enableCaching: true,
cacheTTL: 300000
},
advanced: {
excludePatterns: ["**/.obsidian/**", "**/node_modules/**"],
maxFileSize: 1048576
},
statusMapping: {
enabled: true,
metadataToSymbol: {
// Completed variants
'completed': 'x',
'done': 'x',
'finished': 'x',
'complete': 'x',
'checked': 'x',
'x': 'x',
// In Progress variants
'in-progress': '/',
'in progress': '/',
'inprogress': '/',
'doing': '/',
'working': '/',
'/': '/',
// Planned variants
'planned': '?',
'todo': '?',
'pending': '?',
'?': '?',
// Abandoned variants
'cancelled': '-',
'canceled': '-',
'abandoned': '-',
'-': '-',
// Not Started variants
'not-started': ' ',
'not started': ' ',
'new': ' ',
' ': ' '
},
symbolToMetadata: {
'x': 'completed',
'X': 'completed',
'/': 'in-progress',
'>': 'in-progress',
'?': 'planned',
'-': 'cancelled',
' ': 'not-started'
},
autoDetect: true,
caseSensitive: false
}
}
};

View file

@ -0,0 +1,610 @@
/**
* FileSourceSettings - UI component for File as Task configuration
*
* Provides a settings interface for configuring how files can be recognized
* and treated as tasks with various strategies and options.
*/
import { Setting } from "obsidian";
import type TaskProgressBarPlugin from "../../index";
import type { FileSourceConfiguration } from "../../types/file-source";
import { t } from "../../translations/helper";
/**
* Create File as Task settings UI
*/
export function createFileSourceSettings(
containerEl: HTMLElement,
plugin: TaskProgressBarPlugin,
): void {
const config = plugin.settings.fileSource;
// Main FileSource enable/disable toggle
createEnableToggle(containerEl, plugin, config);
if (config.enabled) {
// Recognition strategies section
createRecognitionStrategiesSection(containerEl, plugin, config);
// File task properties section
createFileTaskPropertiesSection(containerEl, plugin, config);
// Status mapping section
createStatusMappingSection(containerEl, plugin, config);
// Performance section
createPerformanceSection(containerEl, plugin, config);
// Advanced section
createAdvancedSection(containerEl, plugin, config);
}
}
/**
* Create the main enable/disable toggle
*/
function createEnableToggle(
containerEl: HTMLElement,
plugin: TaskProgressBarPlugin,
config: FileSourceConfiguration,
): void {
// Don't create duplicate header since we're now embedded in IndexSettingsTab
const desc = containerEl.createEl("p");
desc.innerHTML =
t(
"Enable files to be recognized as tasks based on their metadata properties. ",
) +
`<strong>${t("Note:")}</strong> ` +
t(
"This is an advanced feature that extends file metadata parsing with multiple recognition strategies.",
);
new Setting(containerEl)
.setName(t("Enable File as Task"))
.setDesc(
t(
"Allow files to be recognized and treated as tasks based on their properties",
),
)
.addToggle((toggle) =>
toggle.setValue(config.enabled).onChange(async (value) => {
plugin.settings.fileSource.enabled = value;
await plugin.saveSettings();
// Refresh the settings display
containerEl.empty();
createFileSourceSettings(containerEl, plugin);
}),
);
}
/**
* Create recognition strategies section
*/
function createRecognitionStrategiesSection(
containerEl: HTMLElement,
plugin: TaskProgressBarPlugin,
config: FileSourceConfiguration,
): void {
new Setting(containerEl)
.setHeading()
.setName(t("Recognition Strategies"))
.setDesc(
t(
"Configure how files are recognized as tasks. At least one strategy must be enabled.",
),
);
// Metadata strategy
const metadataContainer = containerEl.createDiv(
"file-source-strategy-container",
);
new Setting(metadataContainer)
.setName(t("Metadata-based Recognition"))
.setDesc(
t(
"Recognize files as tasks if they have specific frontmatter fields",
),
)
.addToggle((toggle) =>
toggle
.setValue(config.recognitionStrategies.metadata.enabled)
.onChange(async (value) => {
plugin.settings.fileSource.recognitionStrategies.metadata.enabled =
value;
await plugin.saveSettings();
}),
);
if (config.recognitionStrategies.metadata.enabled) {
new Setting(metadataContainer)
.setName(t("Task Fields"))
.setDesc(
t(
"Comma-separated list of metadata fields that indicate a task",
),
)
.addText((text) =>
text
.setPlaceholder("dueDate, status, priority, assigned")
.setValue(
config.recognitionStrategies.metadata.taskFields.join(
", ",
),
)
.onChange(async (value) => {
plugin.settings.fileSource.recognitionStrategies.metadata.taskFields =
value
.split(",")
.map((s) => s.trim())
.filter((s) => s.length > 0);
await plugin.saveSettings();
}),
);
new Setting(metadataContainer)
.setName(t("Require All Fields"))
.setDesc(
t(
"Require all specified fields to be present (otherwise any field is sufficient)",
),
)
.addToggle((toggle) =>
toggle
.setValue(
config.recognitionStrategies.metadata.requireAllFields,
)
.onChange(async (value) => {
plugin.settings.fileSource.recognitionStrategies.metadata.requireAllFields =
value;
await plugin.saveSettings();
}),
);
}
// Tag strategy
const tagContainer = containerEl.createDiv(
"file-source-strategy-container",
);
new Setting(tagContainer)
.setName(t("Tag-based Recognition"))
.setDesc(t("Recognize files as tasks if they have specific tags"))
.addToggle((toggle) =>
toggle
.setValue(config.recognitionStrategies.tags.enabled)
.onChange(async (value) => {
plugin.settings.fileSource.recognitionStrategies.tags.enabled =
value;
await plugin.saveSettings();
}),
);
if (config.recognitionStrategies.tags.enabled) {
new Setting(tagContainer)
.setName(t("Task Tags"))
.setDesc(t("Comma-separated list of tags that indicate a task"))
.addText((text) =>
text
.setPlaceholder("#task, #actionable, #todo")
.setValue(
config.recognitionStrategies.tags.taskTags.join(", "),
)
.onChange(async (value) => {
plugin.settings.fileSource.recognitionStrategies.tags.taskTags =
value
.split(",")
.map((s) => s.trim())
.filter((s) => s.length > 0);
await plugin.saveSettings();
}),
);
new Setting(tagContainer)
.setName(t("Tag Matching Mode"))
.setDesc(t("How tags should be matched against file tags"))
.addDropdown((dropdown) =>
dropdown
.addOption("exact", t("Exact match"))
.addOption("prefix", t("Prefix match"))
.addOption("contains", t("Contains match"))
.setValue(config.recognitionStrategies.tags.matchMode)
.onChange(
async (value: "exact" | "prefix" | "contains") => {
plugin.settings.fileSource.recognitionStrategies.tags.matchMode =
value;
await plugin.saveSettings();
},
),
);
}
// Template and Path strategies (Phase 2 placeholders)
const futureContainer = containerEl.createDiv(
"file-source-future-strategies",
);
futureContainer.createEl("p", {
text: t(
"Template-based and Path-based recognition strategies will be available in a future update.",
),
attr: { style: "color: var(--text-muted); font-style: italic;" },
});
}
/**
* Create file task properties section
*/
function createFileTaskPropertiesSection(
containerEl: HTMLElement,
plugin: TaskProgressBarPlugin,
config: FileSourceConfiguration,
): void {
new Setting(containerEl)
.setHeading()
.setName(t("Task Properties for Files"));
new Setting(containerEl)
.setName(t("Task Title Source"))
.setDesc(
t(
"What should be used as the task title when a file becomes a task",
),
)
.addDropdown((dropdown) =>
dropdown
.addOption("filename", t("Filename"))
.addOption("title", t("Frontmatter title"))
.addOption("h1", t("First H1 heading"))
.addOption("custom", t("Custom metadata field"))
.setValue(config.fileTaskProperties.contentSource)
.onChange(
async (value: "filename" | "title" | "h1" | "custom") => {
plugin.settings.fileSource.fileTaskProperties.contentSource =
value;
await plugin.saveSettings();
// Refresh to show/hide custom field input
containerEl.empty();
createFileSourceSettings(containerEl, plugin);
},
),
);
if (config.fileTaskProperties.contentSource === "custom") {
new Setting(containerEl)
.setName(t("Custom Content Field"))
.setDesc(t("Name of the metadata field to use as task content"))
.addText((text) =>
text
.setPlaceholder("taskContent")
.setValue(
config.fileTaskProperties.customContentField || "",
)
.onChange(async (value) => {
plugin.settings.fileSource.fileTaskProperties.customContentField =
value;
await plugin.saveSettings();
}),
);
}
if (config.fileTaskProperties.contentSource === "filename") {
new Setting(containerEl)
.setName(t("Strip File Extension"))
.setDesc(
t(
"Remove the .md extension from filename when using as task content",
),
)
.addToggle((toggle) =>
toggle
.setValue(config.fileTaskProperties.stripExtension)
.onChange(async (value) => {
plugin.settings.fileSource.fileTaskProperties.stripExtension =
value;
await plugin.saveSettings();
}),
);
}
new Setting(containerEl)
.setName(t("Default Task Status"))
.setDesc(t("Default status for newly created file tasks"))
.addText((text) =>
text
.setPlaceholder(" ")
.setValue(config.fileTaskProperties.defaultStatus)
.onChange(async (value) => {
plugin.settings.fileSource.fileTaskProperties.defaultStatus =
value;
await plugin.saveSettings();
}),
);
}
/**
* Create status mapping section
*/
function createStatusMappingSection(
containerEl: HTMLElement,
plugin: TaskProgressBarPlugin,
config: FileSourceConfiguration,
): void {
new Setting(containerEl)
.setName(t("Status Mapping"))
.setDesc(
t(
"Map between human-readable metadata values (e.g., 'completed') and task symbols (e.g., 'x').",
),
);
new Setting(containerEl)
.setName(t("Enable Status Mapping"))
.setDesc(
t(
"Automatically convert between metadata status values and task symbols",
),
)
.addToggle((toggle) =>
toggle
.setValue(config.statusMapping.enabled)
.onChange(async (value) => {
plugin.settings.fileSource.statusMapping.enabled = value;
await plugin.saveSettings();
// Refresh to show/hide mapping options
containerEl.empty();
createFileSourceSettings(containerEl, plugin);
}),
);
if (config.statusMapping.enabled) {
new Setting(containerEl)
.setName(t("Case Sensitive Matching"))
.setDesc(t("Enable case-sensitive matching for status values"))
.addToggle((toggle) =>
toggle
.setValue(config.statusMapping.caseSensitive)
.onChange(async (value) => {
plugin.settings.fileSource.statusMapping.caseSensitive =
value;
await plugin.saveSettings();
}),
);
new Setting(containerEl)
.setName(t("Auto-detect Status Mappings"))
.setDesc(t("Automatically sync with task status configuration"))
.addToggle((toggle) =>
toggle
.setValue(config.statusMapping.autoDetect)
.onChange(async (value) => {
plugin.settings.fileSource.statusMapping.autoDetect =
value;
await plugin.saveSettings();
}),
);
// Common status mappings display
const mappingsContainer = containerEl.createDiv(
"file-source-status-mappings",
);
mappingsContainer.createEl("h5", { text: t("Common Mappings") });
const mappingsList = mappingsContainer.createEl("div", {
cls: "status-mapping-list",
});
// Show some example mappings
const examples = [
{ metadata: "completed", symbol: "x" },
{ metadata: "in-progress", symbol: "/" },
{ metadata: "planned", symbol: "?" },
{ metadata: "cancelled", symbol: "-" },
{ metadata: "not-started", symbol: " " },
];
const table = mappingsList.createEl("table", {
cls: "status-mapping-table",
});
const thead = table.createEl("thead");
const headerRow = thead.createEl("tr");
headerRow.createEl("th", { text: t("Metadata Value") });
headerRow.createEl("th", { text: "→" });
headerRow.createEl("th", { text: t("Task Symbol") });
const tbody = table.createEl("tbody");
examples.forEach((example) => {
const row = tbody.createEl("tr");
row.createEl("td", { text: example.metadata });
row.createEl("td", { text: "→" });
row.createEl("td", {
text: example.symbol === " " ? "(space)" : example.symbol,
});
});
// Add custom mapping management UI
containerEl.createEl("h5", { text: t("Custom Mappings") });
const customMappingDesc = containerEl.createEl("p");
customMappingDesc.textContent = t(
"Add custom status mappings for your workflow.",
);
// Add mapping input
new Setting(containerEl)
.setName(t("Add Custom Mapping"))
.setDesc(t("Enter metadata value and symbol (e.g., 'done:x')"))
.addText((text) =>
text.setPlaceholder("done:x").onChange(async (value) => {
if (value.includes(":")) {
const [metadata, symbol] = value.split(":", 2);
if (metadata && symbol) {
plugin.settings.fileSource.statusMapping.metadataToSymbol[
metadata
] = symbol;
// Also update reverse mapping if not exists
if (
!plugin.settings.fileSource.statusMapping
.symbolToMetadata[symbol]
) {
plugin.settings.fileSource.statusMapping.symbolToMetadata[
symbol
] = metadata;
}
await plugin.saveSettings();
text.setValue("");
}
}
}),
)
.addButton((button) =>
button
.setButtonText(t("Add"))
.setCta()
.onClick(() => {
// Trigger the text change event with the current value
const textInput = containerEl.querySelector(
".setting-item:last-child input[type='text']",
) as HTMLInputElement;
if (textInput) {
textInput.dispatchEvent(new Event("change"));
}
}),
);
// Note about Task Status Settings integration
const integrationNote = containerEl.createDiv(
"setting-item-description",
);
integrationNote.innerHTML =
`<strong>${t("Note:")}</strong> ` +
t("Status mappings work with your Task Status Settings. ") +
t(
"The symbols defined here should match those in your checkbox status configuration.",
);
}
}
/**
* Create performance section
*/
function createPerformanceSection(
containerEl: HTMLElement,
plugin: TaskProgressBarPlugin,
config: FileSourceConfiguration,
): void {
new Setting(containerEl).setHeading().setName(t("Performance"));
new Setting(containerEl)
.setName(t("Enable Caching"))
.setDesc(t("Cache file task results to improve performance"))
.addToggle((toggle) =>
toggle
.setValue(config.performance.enableCaching)
.onChange(async (value) => {
plugin.settings.fileSource.performance.enableCaching =
value;
await plugin.saveSettings();
}),
);
new Setting(containerEl)
.setName(t("Enable Worker Processing"))
.setDesc(
t(
"Use background workers for better performance (will be implemented in Phase 4)",
),
)
.addToggle((toggle) =>
toggle
.setValue(config.performance.enableWorkerProcessing)
.setDisabled(true) // Disabled until Phase 4
.onChange(async (value) => {
plugin.settings.fileSource.performance.enableWorkerProcessing =
value;
await plugin.saveSettings();
}),
);
}
/**
* Create advanced section
*/
function createAdvancedSection(
containerEl: HTMLElement,
plugin: TaskProgressBarPlugin,
config: FileSourceConfiguration,
): void {
new Setting(containerEl).setHeading().setName(t("Advanced"));
new Setting(containerEl)
.setName(t("Exclude Patterns"))
.setDesc(
t(
"Comma-separated patterns for files to exclude from being recognized as tasks",
),
)
.addText((text) =>
text
.setPlaceholder("**/.obsidian/**, **/node_modules/**")
.setValue(config.advanced.excludePatterns.join(", "))
.onChange(async (value) => {
plugin.settings.fileSource.advanced.excludePatterns = value
.split(",")
.map((s) => s.trim())
.filter((s) => s.length > 0);
await plugin.saveSettings();
}),
);
new Setting(containerEl)
.setName(t("Maximum File Size"))
.setDesc(t("Maximum file size in bytes to process (default: 1MB)"))
.addText((text) =>
text
.setPlaceholder("1048576")
.setValue(config.advanced.maxFileSize.toString())
.onChange(async (value) => {
const size = parseInt(value) || 1048576;
plugin.settings.fileSource.advanced.maxFileSize = size;
await plugin.saveSettings();
}),
);
// Statistics section
const statsContainer = containerEl.createDiv("file-source-stats");
statsContainer.createEl("h5", { text: t("File as Task Status") });
const statusText = config.enabled
? t("File as Task is enabled and monitoring files")
: t("File as Task is disabled");
statsContainer.createEl("p", { text: statusText });
if (config.enabled) {
const strategiesText = getEnabledStrategiesText(config);
statsContainer.createEl("p", {
text: t("Active strategies: ") + strategiesText,
});
}
}
/**
* Get text description of enabled strategies
*/
function getEnabledStrategiesText(config: FileSourceConfiguration): string {
const enabled: string[] = [];
if (config.recognitionStrategies.metadata.enabled)
enabled.push(t("Metadata"));
if (config.recognitionStrategies.tags.enabled) enabled.push(t("Tags"));
if (config.recognitionStrategies.templates.enabled)
enabled.push(t("Templates"));
if (config.recognitionStrategies.paths.enabled) enabled.push(t("Paths"));
return enabled.length > 0 ? enabled.join(", ") : t("None");
}

View file

@ -0,0 +1,825 @@
/**
* FileSource - Main implementation for FileSource feature
*
* This source integrates files as tasks into the dataflow architecture.
* It follows the same patterns as ObsidianSource and IcsSource.
*/
import type { App, TFile, EventRef, CachedMetadata } from "obsidian";
import type { Task } from "../../types/task";
import type {
FileSourceConfiguration,
FileSourceTaskMetadata,
FileSourceStats,
UpdateDecision,
FileTaskCache,
RecognitionStrategy
} from "../../types/file-source";
import { Events, emit, Seq, on } from "../events/Events";
import { FileSourceConfig } from "./FileSourceConfig";
/**
* FileSource - Independent event source for file-based tasks
*
* Subscribes to file events and transforms qualifying files into tasks
* following the established dataflow patterns.
*/
export class FileSource {
private config: FileSourceConfig;
private isInitialized = false;
private lastUpdateSeq = 0;
// Event references for cleanup
private eventRefs: EventRef[] = [];
// Cache for tracking file task state
private fileTaskCache = new Map<string, FileTaskCache>();
// Debouncing for rapid changes
private pendingUpdates = new Map<string, NodeJS.Timeout>();
private readonly DEBOUNCE_DELAY = 300; // ms
// Statistics tracking
private stats: FileSourceStats = {
initialized: false,
trackedFileCount: 0,
recognitionBreakdown: {
metadata: 0,
tag: 0,
template: 0,
path: 0
},
lastUpdate: 0,
lastUpdateSeq: 0
};
constructor(
private app: App,
initialConfig?: Partial<FileSourceConfiguration>
) {
this.config = new FileSourceConfig(initialConfig);
}
/**
* Initialize FileSource and start listening for events
*/
initialize(): void {
if (this.isInitialized) return;
if (!this.config.isEnabled()) return;
console.log("[FileSource] Initializing FileSource...");
// Subscribe to configuration changes
this.config.onChange((newConfig) => {
this.handleConfigChange(newConfig);
});
// Subscribe to file events
this.subscribeToFileEvents();
// Perform initial scan of existing files
this.performInitialScan();
this.isInitialized = true;
this.stats.initialized = true;
console.log(`[FileSource] Initialized with strategies: ${this.config.getEnabledStrategies().join(', ')}`);
}
/**
* Subscribe to relevant file events
*/
private subscribeToFileEvents(): void {
// Subscribe to FILE_UPDATED events from ObsidianSource
this.eventRefs.push(
on(this.app, Events.FILE_UPDATED, (payload) => {
if (payload?.path) {
this.handleFileUpdate(payload.path, payload.reason);
}
})
);
// Subscribe to more granular events if they exist
// These would be added to Events.ts later in Phase 2
this.eventRefs.push(
on(this.app, "task-genius:file-metadata-changed" as any, (payload) => {
if (payload?.path) {
this.handleFileMetadataChange(payload.path);
}
})
);
this.eventRefs.push(
on(this.app, "task-genius:file-content-changed" as any, (payload) => {
if (payload?.path) {
this.handleFileContentChange(payload.path);
}
})
);
}
/**
* Handle file update events with debouncing
*/
private handleFileUpdate(filePath: string, reason: string): void {
if (!this.isInitialized || !this.config.isEnabled()) return;
if (!this.isRelevantFile(filePath)) return;
// Clear existing timeout for this file
const existingTimeout = this.pendingUpdates.get(filePath);
if (existingTimeout) {
clearTimeout(existingTimeout);
}
// Set new debounced timeout
const timeout = setTimeout(async () => {
this.pendingUpdates.delete(filePath);
try {
await this.processFileUpdate(filePath, reason);
} catch (error) {
console.error(`[FileSource] Error processing file update for ${filePath}:`, error);
}
}, this.DEBOUNCE_DELAY);
this.pendingUpdates.set(filePath, timeout);
}
/**
* Handle granular metadata changes (Phase 2 enhancement)
*/
private handleFileMetadataChange(filePath: string): void {
if (!this.shouldUpdateFileTask(filePath, 'metadata')) return;
this.handleFileUpdate(filePath, 'frontmatter');
}
/**
* Handle granular content changes (Phase 2 enhancement)
*/
private handleFileContentChange(filePath: string): void {
if (!this.shouldUpdateFileTask(filePath, 'content')) return;
this.handleFileUpdate(filePath, 'modify');
}
/**
* Process a file update and determine if it should be a file task
*/
private async processFileUpdate(filePath: string, reason: string): Promise<void> {
if (reason === 'delete') {
await this.removeFileTask(filePath);
return;
}
const shouldBeTask = await this.shouldCreateFileTask(filePath);
const existingCache = this.fileTaskCache.get(filePath);
const wasTask = existingCache?.fileTaskExists ?? false;
if (shouldBeTask && !wasTask) {
// File should become a task
await this.createFileTask(filePath);
} else if (shouldBeTask && wasTask) {
// File is already a task, check if it needs updating
await this.updateFileTask(filePath);
} else if (!shouldBeTask && wasTask) {
// File should no longer be a task
await this.removeFileTask(filePath);
}
// else: File is not and should not be a task, do nothing
}
/**
* Check if a file should be treated as a task
*/
async shouldCreateFileTask(filePath: string): Promise<boolean> {
if (!this.isRelevantFile(filePath)) return false;
const file = this.app.vault.getAbstractFileByPath(filePath) as TFile;
if (!file) return false;
try {
const fileContent = await this.app.vault.cachedRead(file);
const fileCache = this.app.metadataCache.getFileCache(file);
return this.evaluateRecognitionStrategies(filePath, fileContent, fileCache);
} catch (error) {
console.error(`[FileSource] Error reading file ${filePath}:`, error);
return false;
}
}
/**
* Evaluate all enabled recognition strategies
*/
private evaluateRecognitionStrategies(
filePath: string,
fileContent: string,
fileCache: CachedMetadata | null
): boolean {
const config = this.config.getConfig();
const { recognitionStrategies } = config;
// Check metadata strategy
if (recognitionStrategies.metadata.enabled) {
if (this.matchesMetadataStrategy(filePath, fileContent, fileCache, recognitionStrategies.metadata)) {
return true;
}
}
// Check tag strategy
if (recognitionStrategies.tags.enabled) {
if (this.matchesTagStrategy(filePath, fileContent, fileCache, recognitionStrategies.tags)) {
return true;
}
}
// Check template strategy (Phase 2)
if (recognitionStrategies.templates.enabled) {
if (this.matchesTemplateStrategy(filePath, fileContent, fileCache, recognitionStrategies.templates)) {
return true;
}
}
// Check path strategy (Phase 2)
if (recognitionStrategies.paths.enabled) {
if (this.matchesPathStrategy(filePath, fileContent, fileCache, recognitionStrategies.paths)) {
return true;
}
}
return false;
}
/**
* Check if file matches metadata strategy
*/
private matchesMetadataStrategy(
filePath: string,
fileContent: string,
fileCache: CachedMetadata | null,
config: any
): boolean {
if (!fileCache?.frontmatter) return false;
const { taskFields, requireAllFields } = config;
const frontmatter = fileCache.frontmatter;
const matchingFields = taskFields.filter((field: string) =>
frontmatter.hasOwnProperty(field) && frontmatter[field] !== undefined
);
if (requireAllFields) {
return matchingFields.length === taskFields.length;
} else {
return matchingFields.length > 0;
}
}
/**
* Check if file matches tag strategy
*/
private matchesTagStrategy(
filePath: string,
fileContent: string,
fileCache: CachedMetadata | null,
config: any
): boolean {
if (!fileCache?.tags) return false;
const { taskTags, matchMode } = config;
const fileTags = fileCache.tags.map(tag => tag.tag);
return taskTags.some((taskTag: string) => {
return fileTags.some(fileTag => {
switch (matchMode) {
case 'exact':
return fileTag === taskTag;
case 'prefix':
return fileTag.startsWith(taskTag);
case 'contains':
return fileTag.includes(taskTag);
default:
return fileTag === taskTag;
}
});
});
}
/**
* Check if file matches template strategy (stub for Phase 2)
*/
private matchesTemplateStrategy(
filePath: string,
fileContent: string,
fileCache: CachedMetadata | null,
config: any
): boolean {
// TODO: Implement in Phase 2
return false;
}
/**
* Check if file matches path strategy (stub for Phase 2)
*/
private matchesPathStrategy(
filePath: string,
fileContent: string,
fileCache: CachedMetadata | null,
config: any
): boolean {
// TODO: Implement in Phase 2
return false;
}
/**
* Create a new file task
*/
async createFileTask(filePath: string): Promise<Task<FileSourceTaskMetadata> | null> {
const file = this.app.vault.getAbstractFileByPath(filePath) as TFile;
if (!file) return null;
try {
const fileContent = await this.app.vault.cachedRead(file);
const fileCache = this.app.metadataCache.getFileCache(file);
const fileTask = await this.buildFileTask(filePath, fileContent, fileCache);
if (!fileTask) return null;
// Update cache
this.updateFileTaskCache(filePath, fileTask);
// Update statistics
this.updateStatistics(fileTask.metadata.recognitionStrategy, 1);
// Emit file task event
this.emitFileTaskUpdate('created', fileTask);
return fileTask;
} catch (error) {
console.error(`[FileSource] Error creating file task for ${filePath}:`, error);
return null;
}
}
/**
* Update an existing file task
*/
async updateFileTask(filePath: string): Promise<Task<FileSourceTaskMetadata> | null> {
// For Phase 1, just recreate the task
// Phase 2 will add smart update detection
return await this.createFileTask(filePath);
}
/**
* Remove a file task
*/
async removeFileTask(filePath: string): Promise<void> {
const existingCache = this.fileTaskCache.get(filePath);
if (!existingCache?.fileTaskExists) return;
// Remove from cache
this.fileTaskCache.delete(filePath);
// Update statistics
this.stats.trackedFileCount = Math.max(0, this.stats.trackedFileCount - 1);
// Emit removal event
const seq = Seq.next();
this.lastUpdateSeq = seq;
emit(this.app, "task-genius:file-task-removed" as any, {
filePath,
timestamp: Date.now(),
seq
});
console.log(`[FileSource] Removed file task: ${filePath}`);
}
/**
* Build a file task from file data
*/
private async buildFileTask(
filePath: string,
fileContent: string,
fileCache: CachedMetadata | null
): Promise<Task<FileSourceTaskMetadata> | null> {
const config = this.config.getConfig();
const file = this.app.vault.getAbstractFileByPath(filePath) as TFile;
if (!file) return null;
// Determine which strategy matched
const strategy = this.getMatchingStrategy(filePath, fileContent, fileCache);
if (!strategy) return null;
// Generate task content based on configuration
const content = this.generateTaskContent(filePath, fileContent, fileCache);
// Extract metadata from frontmatter
const metadata = this.extractTaskMetadata(filePath, fileContent, fileCache, strategy);
// Create the file task
const fileTask: Task<FileSourceTaskMetadata> = {
id: `file-source:${filePath}`,
content,
filePath,
line: 0, // File tasks are at line 0
completed: metadata.status === 'x' || metadata.status === 'X',
status: metadata.status || config.fileTaskProperties.defaultStatus,
originalMarkdown: `[${content}](${filePath})`,
metadata: {
...metadata,
source: "file-source",
recognitionStrategy: strategy.name,
recognitionCriteria: strategy.criteria,
fileTimestamps: {
created: file.stat.ctime,
modified: file.stat.mtime
},
childTasks: [], // Will be populated in Phase 3
tags: metadata.tags || [],
children: [] // Required by StandardTaskMetadata
}
};
return fileTask;
}
/**
* Get the matching recognition strategy for a file
*/
private getMatchingStrategy(
filePath: string,
fileContent: string,
fileCache: CachedMetadata | null
): { name: RecognitionStrategy; criteria: string } | null {
const config = this.config.getConfig();
if (config.recognitionStrategies.metadata.enabled &&
this.matchesMetadataStrategy(filePath, fileContent, fileCache, config.recognitionStrategies.metadata)) {
return { name: "metadata", criteria: "frontmatter" };
}
if (config.recognitionStrategies.tags.enabled &&
this.matchesTagStrategy(filePath, fileContent, fileCache, config.recognitionStrategies.tags)) {
return { name: "tag", criteria: "file-tags" };
}
// TODO: Add template and path strategies in Phase 2
return null;
}
/**
* Generate task content based on configuration
*/
private generateTaskContent(
filePath: string,
fileContent: string,
fileCache: CachedMetadata | null
): string {
const config = this.config.getConfig().fileTaskProperties;
const fileName = filePath.split('/').pop() || filePath;
switch (config.contentSource) {
case 'filename':
return config.stripExtension ?
fileName.replace(/\.[^/.]+$/, "") : fileName;
case 'title':
return fileCache?.frontmatter?.title || fileName;
case 'h1':
const h1 = fileCache?.headings?.find(h => h.level === 1);
return h1?.heading || fileName;
case 'custom':
if (config.customContentField && fileCache?.frontmatter) {
return fileCache.frontmatter[config.customContentField] || fileName;
}
return fileName;
default:
return fileName;
}
}
/**
* Extract task metadata from file
*/
private extractTaskMetadata(
filePath: string,
fileContent: string,
fileCache: CachedMetadata | null,
strategy: { name: RecognitionStrategy; criteria: string }
): Partial<FileSourceTaskMetadata> {
const config = this.config.getConfig();
const frontmatter = fileCache?.frontmatter || {};
// Get status from frontmatter and map it to symbol if needed
let status = frontmatter.status || config.fileTaskProperties.defaultStatus;
// Apply status mapping if enabled
if (config.statusMapping.enabled && frontmatter.status) {
// Try to map the metadata value to a symbol
const mappedStatus = this.config.mapMetadataToSymbol(frontmatter.status);
if (mappedStatus !== frontmatter.status) {
// Mapping was successful
status = mappedStatus;
console.log(`[FileSource] Mapped status '${frontmatter.status}' to '${mappedStatus}' for ${filePath}`);
}
}
// Extract standard task metadata
const metadata: Partial<FileSourceTaskMetadata> = {
dueDate: this.parseDate(frontmatter.dueDate || frontmatter.due),
startDate: this.parseDate(frontmatter.startDate || frontmatter.start),
scheduledDate: this.parseDate(frontmatter.scheduledDate || frontmatter.scheduled),
priority: frontmatter.priority || config.fileTaskProperties.defaultPriority,
project: frontmatter.project,
context: frontmatter.context,
area: frontmatter.area,
tags: fileCache?.tags?.map(tag => tag.tag) || [],
status: status,
children: []
};
return metadata;
}
/**
* Convert a task symbol back to metadata value for file updates
* This will be used in Phase 3 when implementing file task updates
*/
private mapSymbolToFileMetadata(symbol: string): string {
const config = this.config.getConfig();
if (!config.statusMapping.enabled) {
return symbol;
}
// Map symbol back to preferred metadata value
return this.config.mapSymbolToMetadata(symbol);
}
/**
* Parse date from various formats
*/
private parseDate(dateValue: any): number | undefined {
if (!dateValue) return undefined;
if (typeof dateValue === 'number') {
return dateValue;
}
if (typeof dateValue === 'string') {
const parsed = Date.parse(dateValue);
return isNaN(parsed) ? undefined : parsed;
}
if (dateValue instanceof Date) {
return dateValue.getTime();
}
return undefined;
}
/**
* Update file task cache
*/
private updateFileTaskCache(filePath: string, task: Task<FileSourceTaskMetadata>): void {
const frontmatterHash = this.generateFrontmatterHash(filePath);
this.fileTaskCache.set(filePath, {
fileTaskExists: true,
frontmatterHash,
childTaskIds: new Set(task.metadata.childTasks || []),
lastUpdated: Date.now()
});
}
/**
* Generate hash for frontmatter for change detection
*/
private generateFrontmatterHash(filePath: string): string {
const file = this.app.vault.getAbstractFileByPath(filePath) as TFile;
if (!file) return '';
const fileCache = this.app.metadataCache.getFileCache(file);
if (!fileCache?.frontmatter) return '';
// Simple hash of frontmatter JSON
const frontmatterStr = JSON.stringify(fileCache.frontmatter, Object.keys(fileCache.frontmatter).sort());
return this.simpleHash(frontmatterStr);
}
/**
* Simple hash function
*/
private simpleHash(str: string): string {
let hash = 0;
for (let i = 0; i < str.length; i++) {
const char = str.charCodeAt(i);
hash = ((hash << 5) - hash) + char;
hash = hash & hash; // Convert to 32-bit integer
}
return hash.toString(36);
}
/**
* Check if file needs updating (stub for Phase 2)
*/
private shouldUpdateFileTask(filePath: string, changeType: 'metadata' | 'content'): boolean {
// Simple check for Phase 1 - always update if file is tracked
return this.fileTaskCache.has(filePath);
}
/**
* Update statistics
*/
private updateStatistics(strategy: RecognitionStrategy, delta: number): void {
this.stats.recognitionBreakdown[strategy] += delta;
this.stats.trackedFileCount += delta;
this.stats.lastUpdate = Date.now();
this.stats.lastUpdateSeq = this.lastUpdateSeq;
}
/**
* Emit file task update event
*/
private emitFileTaskUpdate(
action: 'created' | 'updated' | 'removed',
task: Task<FileSourceTaskMetadata>
): void {
const seq = Seq.next();
this.lastUpdateSeq = seq;
emit(this.app, "task-genius:file-task-updated" as any, {
action,
task,
timestamp: Date.now(),
seq
});
console.log(`[FileSource] File task ${action}: ${task.filePath}`);
}
/**
* Check if file is relevant for processing
*/
private isRelevantFile(filePath: string): boolean {
const config = this.config.getConfig().advanced;
// Check exclude patterns
for (const pattern of config.excludePatterns) {
// Simple pattern matching (could be enhanced with glob patterns in Phase 2)
if (filePath.includes(pattern.replace('**/', '').replace('/**', ''))) {
return false;
}
}
// Only process markdown files for now
if (!filePath.endsWith('.md')) {
return false;
}
// Skip system/hidden files
if (filePath.startsWith('.') || filePath.includes('/.')) {
return false;
}
return true;
}
/**
* Perform initial scan of existing files
*/
private async performInitialScan(): Promise<void> {
console.log("[FileSource] Performing initial scan...");
const mdFiles = this.app.vault.getMarkdownFiles();
let scannedCount = 0;
let taskCount = 0;
for (const file of mdFiles) {
if (this.isRelevantFile(file.path)) {
try {
const shouldBeTask = await this.shouldCreateFileTask(file.path);
if (shouldBeTask) {
await this.createFileTask(file.path);
taskCount++;
}
scannedCount++;
} catch (error) {
console.error(`[FileSource] Error scanning ${file.path}:`, error);
}
}
}
console.log(`[FileSource] Initial scan complete: ${scannedCount} files scanned, ${taskCount} file tasks created`);
}
/**
* Handle configuration changes
*/
private handleConfigChange(newConfig: FileSourceConfiguration): void {
if (!newConfig.enabled && this.isInitialized) {
// FileSource is being disabled
this.destroy();
return;
}
if (newConfig.enabled && !this.isInitialized) {
// FileSource is being enabled
this.initialize();
return;
}
// Configuration changed while active - might need to rescan
// This is a Phase 2 enhancement
console.log("[FileSource] Configuration updated");
}
/**
* Get current statistics
*/
getStats(): FileSourceStats {
return { ...this.stats };
}
/**
* Get all file tasks (stub for Phase 2)
*/
async getAllFileTasks(): Promise<Task<FileSourceTaskMetadata>[]> {
// This will be implemented properly in Phase 3 with Repository integration
return [];
}
/**
* Update configuration
*/
updateConfiguration(config: Partial<FileSourceConfiguration>): void {
this.config.updateConfig(config);
}
/**
* Force refresh of all file tasks
*/
async refresh(): Promise<void> {
if (!this.isInitialized || !this.config.isEnabled()) return;
console.log("[FileSource] Manual refresh triggered");
// Clear cache and re-scan
this.fileTaskCache.clear();
this.stats.trackedFileCount = 0;
this.stats.recognitionBreakdown = { metadata: 0, tag: 0, template: 0, path: 0 };
await this.performInitialScan();
}
/**
* Cleanup and destroy FileSource
*/
destroy(): void {
if (!this.isInitialized) return;
console.log("[FileSource] Destroying FileSource...");
// Clear all debouncing timeouts
for (const timeout of this.pendingUpdates.values()) {
clearTimeout(timeout);
}
this.pendingUpdates.clear();
// Clear event listeners
for (const ref of this.eventRefs) {
this.app.vault.offref(ref);
}
this.eventRefs = [];
// Clear cache
this.fileTaskCache.clear();
// Reset statistics
this.stats = {
initialized: false,
trackedFileCount: 0,
recognitionBreakdown: { metadata: 0, tag: 0, template: 0, path: 0 },
lastUpdate: 0,
lastUpdateSeq: 0
};
// Emit cleanup event
emit(this.app, "task-genius:file-task-removed" as any, {
filePath: null, // Indicates all file tasks removed
timestamp: Date.now(),
seq: Seq.next(),
destroyed: true
});
this.isInitialized = false;
console.log("[FileSource] Cleanup complete");
}
}

View file

@ -0,0 +1,560 @@
/**
* FileSourceConfig - Configuration management for FileSource
*
* Handles configuration validation, defaults, and update management
* for the FileSource feature.
*/
import type {
FileSourceConfiguration,
MetadataRecognitionConfig,
TagRecognitionConfig,
TemplateRecognitionConfig,
PathRecognitionConfig,
FileTaskPropertiesConfig,
RelationshipsConfig,
PerformanceConfig,
AdvancedConfig,
StatusMappingConfig
} from "../../types/file-source";
/** Default configuration for metadata-based recognition */
export const DEFAULT_METADATA_CONFIG: MetadataRecognitionConfig = {
enabled: true,
taskFields: ["dueDate", "status", "priority", "assigned"],
requireAllFields: false
};
/** Default configuration for tag-based recognition */
export const DEFAULT_TAG_CONFIG: TagRecognitionConfig = {
enabled: true,
taskTags: ["#task", "#actionable", "#todo"],
matchMode: "exact"
};
/** Default configuration for template-based recognition */
export const DEFAULT_TEMPLATE_CONFIG: TemplateRecognitionConfig = {
enabled: false,
templatePaths: ["Templates/Task Template.md"],
checkTemplateMetadata: true
};
/** Default configuration for path-based recognition */
export const DEFAULT_PATH_CONFIG: PathRecognitionConfig = {
enabled: false,
taskPaths: ["Projects/", "Tasks/"],
matchMode: "prefix"
};
/** Default configuration for file task properties */
export const DEFAULT_FILE_TASK_PROPERTIES: FileTaskPropertiesConfig = {
contentSource: "filename",
stripExtension: true,
defaultStatus: " ",
defaultPriority: undefined
};
/** Default configuration for relationships */
export const DEFAULT_RELATIONSHIPS_CONFIG: RelationshipsConfig = {
enableChildRelationships: true,
enableMetadataInheritance: true,
inheritanceFields: ["project", "priority", "context"]
};
/** Default configuration for performance */
export const DEFAULT_PERFORMANCE_CONFIG: PerformanceConfig = {
enableWorkerProcessing: true,
enableCaching: true,
cacheTTL: 300000 // 5 minutes
};
/** Default configuration for advanced features */
export const DEFAULT_ADVANCED_CONFIG: AdvancedConfig = {
excludePatterns: ["**/.obsidian/**", "**/node_modules/**"],
maxFileSize: 1048576 // 1MB
};
/** Default status mapping configuration */
export const DEFAULT_STATUS_MAPPING_CONFIG: StatusMappingConfig = {
enabled: true,
metadataToSymbol: {
// Completed variants
'completed': 'x',
'done': 'x',
'finished': 'x',
'complete': 'x',
'checked': 'x',
'resolved': 'x',
'closed': 'x',
'x': 'x',
'X': 'x',
// In Progress variants
'in-progress': '/',
'in progress': '/',
'inprogress': '/',
'doing': '/',
'working': '/',
'active': '/',
'started': '/',
'ongoing': '/',
'/': '/',
'>': '/',
// Planned variants
'planned': '?',
'todo': '?',
'pending': '?',
'scheduled': '?',
'queued': '?',
'waiting': '?',
'later': '?',
'?': '?',
// Abandoned variants
'cancelled': '-',
'canceled': '-',
'abandoned': '-',
'dropped': '-',
'skipped': '-',
'deferred': '-',
'wontfix': '-',
"won't fix": '-',
'-': '-',
// Not Started variants
'not-started': ' ',
'not started': ' ',
'notstarted': ' ',
'new': ' ',
'open': ' ',
'created': ' ',
'unstarted': ' ',
' ': ' '
},
symbolToMetadata: {
'x': 'completed',
'X': 'completed',
'/': 'in-progress',
'>': 'in-progress',
'?': 'planned',
'-': 'cancelled',
' ': 'not-started'
},
autoDetect: true,
caseSensitive: false
};
/** Complete default FileSource configuration */
export const DEFAULT_FILE_SOURCE_CONFIG: FileSourceConfiguration = {
enabled: false, // Disabled by default for backward compatibility
recognitionStrategies: {
metadata: DEFAULT_METADATA_CONFIG,
tags: DEFAULT_TAG_CONFIG,
templates: DEFAULT_TEMPLATE_CONFIG,
paths: DEFAULT_PATH_CONFIG
},
fileTaskProperties: DEFAULT_FILE_TASK_PROPERTIES,
relationships: DEFAULT_RELATIONSHIPS_CONFIG,
performance: DEFAULT_PERFORMANCE_CONFIG,
advanced: DEFAULT_ADVANCED_CONFIG,
statusMapping: DEFAULT_STATUS_MAPPING_CONFIG
};
/**
* FileSourceConfig - Manages FileSource configuration
*/
export class FileSourceConfig {
private config: FileSourceConfiguration;
private listeners: Array<(config: FileSourceConfiguration) => void> = [];
constructor(initialConfig?: Partial<FileSourceConfiguration>) {
this.config = this.mergeWithDefaults(initialConfig || {});
}
/**
* Get current configuration
*/
getConfig(): FileSourceConfiguration {
return { ...this.config };
}
/**
* Update configuration with partial updates
*/
updateConfig(updates: Partial<FileSourceConfiguration>): void {
const newConfig = this.mergeWithDefaults(updates);
const hasChanged = JSON.stringify(newConfig) !== JSON.stringify(this.config);
if (hasChanged) {
this.config = newConfig;
this.notifyListeners();
}
}
/**
* Subscribe to configuration changes
*/
onChange(listener: (config: FileSourceConfiguration) => void): () => void {
this.listeners.push(listener);
return () => {
const index = this.listeners.indexOf(listener);
if (index > -1) {
this.listeners.splice(index, 1);
}
};
}
/**
* Check if FileSource is enabled
*/
isEnabled(): boolean {
return this.config.enabled;
}
/**
* Get recognition strategies that are enabled
*/
getEnabledStrategies(): string[] {
const strategies: string[] = [];
const { recognitionStrategies } = this.config;
if (recognitionStrategies.metadata.enabled) strategies.push("metadata");
if (recognitionStrategies.tags.enabled) strategies.push("tags");
if (recognitionStrategies.templates.enabled) strategies.push("templates");
if (recognitionStrategies.paths.enabled) strategies.push("paths");
return strategies;
}
/**
* Validate configuration and return any errors
*/
validateConfig(config: Partial<FileSourceConfiguration>): string[] {
const errors: string[] = [];
// Validate recognition strategies
if (config.recognitionStrategies) {
const strategies = config.recognitionStrategies;
// Check if at least one strategy is enabled when FileSource is enabled
if (config.enabled !== false) {
const hasEnabledStrategy = Object.values(strategies).some(strategy =>
strategy && strategy.enabled
);
if (!hasEnabledStrategy) {
errors.push("At least one recognition strategy must be enabled");
}
}
// Validate metadata strategy
if (strategies.metadata?.taskFields?.length === 0) {
errors.push("Metadata strategy requires at least one task field");
}
// Validate tag strategy
if (strategies.tags?.taskTags?.length === 0) {
errors.push("Tag strategy requires at least one task tag");
}
// Validate template strategy
if (strategies.templates?.templatePaths?.length === 0) {
errors.push("Template strategy requires at least one template path");
}
// Validate path strategy
if (strategies.paths?.taskPaths?.length === 0) {
errors.push("Path strategy requires at least one task path");
}
}
// Validate file task properties
if (config.fileTaskProperties) {
const props = config.fileTaskProperties;
if (props.contentSource === "custom" && !props.customContentField) {
errors.push("Custom content source requires customContentField to be specified");
}
}
// Validate performance config
if (config.performance) {
const perf = config.performance;
if (perf.cacheTTL && perf.cacheTTL < 0) {
errors.push("Cache TTL must be a positive number");
}
}
// Validate advanced config
if (config.advanced) {
const advanced = config.advanced;
if (advanced.maxFileSize && advanced.maxFileSize < 1024) {
errors.push("Maximum file size must be at least 1KB");
}
if (advanced.customRecognitionFunction) {
// Basic validation for custom function
try {
// Check if it's valid JavaScript function syntax
new Function('filePath', 'fileContent', 'fileCache', advanced.customRecognitionFunction);
} catch (error) {
errors.push("Custom recognition function has invalid syntax");
}
}
}
// Validate status mapping config
if (config.statusMapping?.enabled) {
const mapping = config.statusMapping;
if (Object.keys(mapping.metadataToSymbol || {}).length === 0) {
errors.push("Status mapping requires at least one metadata to symbol mapping");
}
if (Object.keys(mapping.symbolToMetadata || {}).length === 0) {
errors.push("Status mapping requires at least one symbol to metadata mapping");
}
}
return errors;
}
/**
* Merge partial configuration with defaults
*/
private mergeWithDefaults(partial: Partial<FileSourceConfiguration>): FileSourceConfiguration {
return {
enabled: partial.enabled ?? DEFAULT_FILE_SOURCE_CONFIG.enabled,
recognitionStrategies: {
metadata: {
...DEFAULT_METADATA_CONFIG,
...partial.recognitionStrategies?.metadata
},
tags: {
...DEFAULT_TAG_CONFIG,
...partial.recognitionStrategies?.tags
},
templates: {
...DEFAULT_TEMPLATE_CONFIG,
...partial.recognitionStrategies?.templates
},
paths: {
...DEFAULT_PATH_CONFIG,
...partial.recognitionStrategies?.paths
}
},
fileTaskProperties: {
...DEFAULT_FILE_TASK_PROPERTIES,
...partial.fileTaskProperties
},
relationships: {
...DEFAULT_RELATIONSHIPS_CONFIG,
...partial.relationships
},
performance: {
...DEFAULT_PERFORMANCE_CONFIG,
...partial.performance
},
advanced: {
...DEFAULT_ADVANCED_CONFIG,
...partial.advanced
},
statusMapping: {
...DEFAULT_STATUS_MAPPING_CONFIG,
...partial.statusMapping
}
};
}
/**
* Notify all listeners of configuration changes
*/
private notifyListeners(): void {
this.listeners.forEach(listener => {
try {
listener(this.config);
} catch (error) {
console.error('FileSourceConfig: Error in change listener:', error);
}
});
}
/**
* Map a metadata status value to a task symbol
* @param metadataValue The metadata value (e.g., "completed", "in-progress")
* @returns The corresponding task symbol (e.g., "x", "/") or the original value if no mapping exists
*/
mapMetadataToSymbol(metadataValue: string): string {
const { statusMapping } = this.config;
if (!statusMapping.enabled) {
return metadataValue;
}
// Handle case sensitivity
const lookupValue = statusMapping.caseSensitive
? metadataValue
: metadataValue.toLowerCase();
// Find matching key in the mapping (case-insensitive search if needed)
for (const [key, symbol] of Object.entries(statusMapping.metadataToSymbol)) {
const compareKey = statusMapping.caseSensitive ? key : key.toLowerCase();
if (compareKey === lookupValue) {
return symbol;
}
}
// Return original value if no mapping found
return metadataValue;
}
/**
* Map a task symbol to a metadata status value
* @param symbol The task symbol (e.g., "x", "/")
* @returns The corresponding metadata value (e.g., "completed", "in-progress") or the original symbol if no mapping exists
*/
mapSymbolToMetadata(symbol: string): string {
const { statusMapping } = this.config;
if (!statusMapping.enabled) {
return symbol;
}
// Direct lookup for symbols (usually case-sensitive)
return statusMapping.symbolToMetadata[symbol] || symbol;
}
/**
* Check if a value is a recognized status (either metadata value or symbol)
* @param value The value to check
* @returns True if the value is recognized as a status
*/
isRecognizedStatus(value: string): boolean {
const { statusMapping } = this.config;
if (!statusMapping.enabled) {
return false;
}
const lookupValue = statusMapping.caseSensitive
? value
: value.toLowerCase();
// Check if it's a known metadata value
for (const key of Object.keys(statusMapping.metadataToSymbol)) {
const compareKey = statusMapping.caseSensitive ? key : key.toLowerCase();
if (compareKey === lookupValue) {
return true;
}
}
// Check if it's a known symbol
return value in statusMapping.symbolToMetadata;
}
/**
* Sync status mappings with current task status configuration
* @param taskStatuses The current task status configuration from settings
*/
syncWithTaskStatuses(taskStatuses: Record<string, string>): void {
if (!this.config.statusMapping.autoDetect) {
return;
}
// Extract symbols from task status configuration
const symbolToType: Record<string, string> = {};
for (const [type, symbols] of Object.entries(taskStatuses)) {
const symbolList = symbols.split('|').filter(s => s);
for (const symbol of symbolList) {
// Handle potential pattern like '/>' being split into '/' and '>'
if (symbol.length === 1 || symbol === '/>') {
if (symbol === '/>') {
symbolToType['/'] = type;
symbolToType['>'] = type;
} else {
symbolToType[symbol] = type;
}
} else {
// For multi-character symbols, add each character separately
for (const char of symbol) {
symbolToType[char] = type;
}
}
}
}
// Update symbol to metadata mappings based on type
const typeToMetadata: Record<string, string> = {
'completed': 'completed',
'inProgress': 'in-progress',
'planned': 'planned',
'abandoned': 'cancelled',
'notStarted': 'not-started'
};
for (const [symbol, type] of Object.entries(symbolToType)) {
if (typeToMetadata[type]) {
this.config.statusMapping.symbolToMetadata[symbol] = typeToMetadata[type];
}
}
this.notifyListeners();
}
/**
* Create a configuration preset for common use cases
*/
static createPreset(presetName: 'basic' | 'metadata-only' | 'tag-only' | 'full'): Partial<FileSourceConfiguration> {
switch (presetName) {
case 'basic':
return {
enabled: true,
recognitionStrategies: {
metadata: { ...DEFAULT_METADATA_CONFIG, enabled: true },
tags: { ...DEFAULT_TAG_CONFIG, enabled: false },
templates: { ...DEFAULT_TEMPLATE_CONFIG, enabled: false },
paths: { ...DEFAULT_PATH_CONFIG, enabled: false }
}
};
case 'metadata-only':
return {
enabled: true,
recognitionStrategies: {
metadata: { ...DEFAULT_METADATA_CONFIG, enabled: true },
tags: { ...DEFAULT_TAG_CONFIG, enabled: false },
templates: { ...DEFAULT_TEMPLATE_CONFIG, enabled: false },
paths: { ...DEFAULT_PATH_CONFIG, enabled: false }
}
};
case 'tag-only':
return {
enabled: true,
recognitionStrategies: {
metadata: { ...DEFAULT_METADATA_CONFIG, enabled: false },
tags: { ...DEFAULT_TAG_CONFIG, enabled: true },
templates: { ...DEFAULT_TEMPLATE_CONFIG, enabled: false },
paths: { ...DEFAULT_PATH_CONFIG, enabled: false }
}
};
case 'full':
return {
enabled: true,
recognitionStrategies: {
metadata: { ...DEFAULT_METADATA_CONFIG, enabled: true },
tags: { ...DEFAULT_TAG_CONFIG, enabled: true },
templates: { ...DEFAULT_TEMPLATE_CONFIG, enabled: false },
paths: { ...DEFAULT_PATH_CONFIG, enabled: false }
}
};
default:
return {};
}
}
}

View file

@ -267,8 +267,11 @@ export default class TaskProgressBarPlugin extends Plugin {
this.rebuildProgressManager = new RebuildProgressManager();
// Initialize task management systems
if (this.settings.enableView) {
this.loadViews();
if (this.settings.enableIndexer) {
// Initialize indexer-dependent features
if (this.settings.enableView) {
this.loadViews();
}
addIcon("task-genius", getTaskGeniusIcon());
addIcon("completed", getStatusIcon("completed"));
@ -474,7 +477,7 @@ export default class TaskProgressBarPlugin extends Plugin {
}
});
if (this.settings.enableView) {
if (this.settings.enableIndexer) {
// Check for version changes and handle rebuild if needed
this.initializeTaskManagerWithVersionCheck().catch((error) => {
console.error(
@ -711,7 +714,7 @@ export default class TaskProgressBarPlugin extends Plugin {
},
});
if (this.settings.enableView) {
if (this.settings.enableIndexer) {
// Add command to refresh the task index
this.addCommand({
id: "refresh-task-index",

View file

@ -13,6 +13,7 @@ import "./styles/setting.css";
import "./styles/setting-v2.css";
import "./styles/beta-warning.css";
import "./styles/settings-search.css";
import "./styles/settings-migration.css";
import {
renderAboutSettingsTab,
renderBetaTestSettingsTab,
@ -28,6 +29,7 @@ import {
renderProjectSettingsTab,
renderRewardSettingsTab,
renderTimelineSidebarSettingsTab,
renderIndexSettingsTab,
IcsSettingsComponent,
} from "./components/settings";
import { renderFileFilterSettingsTab } from "./components/settings/FileFilterSettingsTab";
@ -55,9 +57,15 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
icon: "settings",
category: "core",
},
{
id: "index",
name: t("Index & Sources"),
icon: "database",
category: "core",
},
{
id: "view-settings",
name: t("Views & Index"),
name: t("Views"),
icon: "layout",
category: "core",
},
@ -100,7 +108,7 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
id: "project",
name: t("Projects"),
icon: "folder-open",
category: "management",
category: "core",
},
// Workflow & Automation
@ -209,7 +217,10 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
if (this.searchComponent) {
this.searchComponent.destroy();
}
this.searchComponent = new SettingsSearchComponent(this, this.containerEl);
this.searchComponent = new SettingsSearchComponent(
this,
this.containerEl,
);
}
// Tabs management with categories
@ -301,7 +312,7 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
tab.name +
(tab.id === "about"
? " v" + this.plugin.manifest.version
: "")
: ""),
);
// Add click handler
@ -334,7 +345,7 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
// Show active section, hide others
const sections = this.containerEl.querySelectorAll(
".settings-tab-section"
".settings-tab-section",
);
sections.forEach((section) => {
if (section.getAttribute("data-tab-id") === tabId) {
@ -348,10 +359,10 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
// Handle tab container and header visibility based on selected tab
const tabsContainer = this.containerEl.querySelector(
".settings-tabs-categorized-container"
".settings-tabs-categorized-container",
);
const settingsHeader = this.containerEl.querySelector(
".task-genius-settings-header"
".task-genius-settings-header",
);
if (tabId === "general") {
@ -377,7 +388,7 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
tabId,
"Active sections:",
this.containerEl.querySelectorAll(".settings-tab-section-active")
.length
.length,
);
}
@ -389,7 +400,7 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
private createTabSection(tabId: string): HTMLElement {
// Get the sections container
const sectionsContainer = this.containerEl.querySelector(
".settings-tab-sections"
".settings-tab-sections",
);
if (!sectionsContainer) return this.containerEl;
@ -497,6 +508,10 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
const projectSection = this.createTabSection("project");
this.displayProjectSettings(projectSection);
// Index Settings Tab
const indexSection = this.createTabSection("index");
this.displayIndexSettings(indexSection);
// View Settings Tab
const viewSettingsSection = this.createTabSection("view-settings");
this.displayViewSettings(viewSettingsSection);
@ -585,6 +600,10 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
renderViewSettingsTab(this, containerEl);
}
private displayIndexSettings(containerEl: HTMLElement): void {
renderIndexSettingsTab(this, containerEl);
}
private displayProjectSettings(containerEl: HTMLElement): void {
renderProjectSettingsTab(this, containerEl);
}
@ -596,16 +615,14 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
() => {
this.currentTab = "general";
this.display();
}
},
);
icsSettingsComponent.display();
}
private displayMcpSettings(containerEl: HTMLElement): void {
renderMcpIntegrationSettingsTab(
containerEl,
this.plugin,
() => this.applySettingsUpdate()
renderMcpIntegrationSettingsTab(containerEl, this.plugin, () =>
this.applySettingsUpdate(),
);
}
@ -638,7 +655,9 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
// Main enable/disable setting
new Setting(timerSection)
.setName("Enable Task Timer")
.setDesc("Enable task timer functionality for tracking time spent on tasks")
.setDesc(
"Enable task timer functionality for tracking time spent on tasks",
)
.addToggle((toggle) => {
toggle
.setValue(this.plugin.settings.taskTimer?.enabled || false)
@ -649,10 +668,10 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
metadataDetection: {
frontmatter: "task-timer",
folders: [],
tags: []
tags: [],
},
timeFormat: "{h}hrs{m}mins",
blockRefPrefix: "timer"
blockRefPrefix: "timer",
};
}
this.plugin.settings.taskTimer.enabled = value;
@ -668,7 +687,7 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
// Metadata detection section
const metadataSection = timerSection.createDiv();
metadataSection.addClass("task-timer-metadata-section");
const metadataHeading = metadataSection.createEl("h3");
metadataHeading.setText("Metadata Detection");
metadataHeading.addClass("task-timer-section-heading");
@ -676,29 +695,45 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
// Frontmatter field setting
new Setting(metadataSection)
.setName("Frontmatter field")
.setDesc("Field name in frontmatter to check for enabling task timer (e.g., 'task-timer: true')")
.setDesc(
"Field name in frontmatter to check for enabling task timer (e.g., 'task-timer: true')",
)
.addText((text) => {
text
.setValue(this.plugin.settings.taskTimer?.metadataDetection?.frontmatter || "task-timer")
.onChange(async (value) => {
if (this.plugin.settings.taskTimer?.metadataDetection) {
this.plugin.settings.taskTimer.metadataDetection.frontmatter = value;
this.applySettingsUpdate();
}
});
text.setValue(
this.plugin.settings.taskTimer?.metadataDetection
?.frontmatter || "task-timer",
).onChange(async (value) => {
if (this.plugin.settings.taskTimer?.metadataDetection) {
this.plugin.settings.taskTimer.metadataDetection.frontmatter =
value;
this.applySettingsUpdate();
}
});
});
// Folder paths setting
new Setting(metadataSection)
.setName("Folder paths")
.setDesc("Comma-separated list of folder paths where task timer should be enabled")
.setDesc(
"Comma-separated list of folder paths where task timer should be enabled",
)
.addTextArea((textArea) => {
textArea
.setValue(this.plugin.settings.taskTimer?.metadataDetection?.folders?.join(", ") || "")
.setValue(
this.plugin.settings.taskTimer?.metadataDetection?.folders?.join(
", ",
) || "",
)
.onChange(async (value) => {
if (this.plugin.settings.taskTimer?.metadataDetection) {
this.plugin.settings.taskTimer.metadataDetection.folders =
value.split(",").map(f => f.trim()).filter(f => f);
if (
this.plugin.settings.taskTimer
?.metadataDetection
) {
this.plugin.settings.taskTimer.metadataDetection.folders =
value
.split(",")
.map((f) => f.trim())
.filter((f) => f);
this.applySettingsUpdate();
}
});
@ -711,11 +746,21 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
.setDesc("Comma-separated list of tags that enable task timer")
.addTextArea((textArea) => {
textArea
.setValue(this.plugin.settings.taskTimer?.metadataDetection?.tags?.join(", ") || "")
.setValue(
this.plugin.settings.taskTimer?.metadataDetection?.tags?.join(
", ",
) || "",
)
.onChange(async (value) => {
if (this.plugin.settings.taskTimer?.metadataDetection) {
this.plugin.settings.taskTimer.metadataDetection.tags =
value.split(",").map(t => t.trim()).filter(t => t);
if (
this.plugin.settings.taskTimer
?.metadataDetection
) {
this.plugin.settings.taskTimer.metadataDetection.tags =
value
.split(",")
.map((t) => t.trim())
.filter((t) => t);
this.applySettingsUpdate();
}
});
@ -725,7 +770,7 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
// Time format section
const formatSection = timerSection.createDiv();
formatSection.addClass("task-timer-format-section");
const formatHeading = formatSection.createEl("h3");
formatHeading.setText("Time Format");
formatHeading.addClass("task-timer-section-heading");
@ -733,36 +778,39 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
// Time format template setting
new Setting(formatSection)
.setName("Time format template")
.setDesc("Template for displaying completed task time. Use {h} for hours, {m} for minutes, {s} for seconds")
.setDesc(
"Template for displaying completed task time. Use {h} for hours, {m} for minutes, {s} for seconds",
)
.addText((text) => {
text
.setValue(this.plugin.settings.taskTimer?.timeFormat || "{h}hrs{m}mins")
.onChange(async (value) => {
if (this.plugin.settings.taskTimer) {
this.plugin.settings.taskTimer.timeFormat = value;
this.applySettingsUpdate();
}
});
text.setValue(
this.plugin.settings.taskTimer?.timeFormat ||
"{h}hrs{m}mins",
).onChange(async (value) => {
if (this.plugin.settings.taskTimer) {
this.plugin.settings.taskTimer.timeFormat = value;
this.applySettingsUpdate();
}
});
});
// Format examples
const examplesDiv = formatSection.createDiv();
examplesDiv.addClass("task-timer-examples");
const examplesTitle = examplesDiv.createDiv();
examplesTitle.addClass("task-timer-examples-title");
examplesTitle.setText("Format Examples:");
const examplesList = examplesDiv.createEl("ul");
const examples = [
{ format: "{h}hrs{m}mins", result: "2hrs30mins" },
{ format: "{h}h {m}m {s}s", result: "2h 30m 45s" },
{ format: "{h}:{m}:{s}", result: "2:30:45" },
{ format: "({m}mins)", result: "(150mins)" }
{ format: "({m}mins)", result: "(150mins)" },
];
examples.forEach(example => {
examples.forEach((example) => {
const listItem = examplesList.createEl("li");
const codeEl = listItem.createEl("code");
codeEl.setText(example.format);
@ -772,7 +820,7 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
// Block reference section
const blockRefSection = timerSection.createDiv();
blockRefSection.addClass("task-timer-blockref-section");
const blockRefHeading = blockRefSection.createEl("h3");
blockRefHeading.setText("Block References");
blockRefHeading.addClass("task-timer-section-heading");
@ -780,43 +828,64 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
// Block reference prefix setting
new Setting(blockRefSection)
.setName("Block reference prefix")
.setDesc("Prefix for generated block reference IDs (e.g., 'timer' creates ^timer-123456-7890)")
.setDesc(
"Prefix for generated block reference IDs (e.g., 'timer' creates ^timer-123456-7890)",
)
.addText((text) => {
text
.setValue(this.plugin.settings.taskTimer?.blockRefPrefix || "timer")
.onChange(async (value) => {
if (this.plugin.settings.taskTimer) {
this.plugin.settings.taskTimer.blockRefPrefix = value;
this.applySettingsUpdate();
}
});
text.setValue(
this.plugin.settings.taskTimer?.blockRefPrefix ||
"timer",
).onChange(async (value) => {
if (this.plugin.settings.taskTimer) {
this.plugin.settings.taskTimer.blockRefPrefix =
value;
this.applySettingsUpdate();
}
});
});
// Commands section
const commandsSection = timerSection.createDiv();
commandsSection.addClass("task-timer-commands-section");
const commandsHeading = commandsSection.createEl("h3");
commandsHeading.setText("Data Management");
commandsHeading.addClass("task-timer-section-heading");
const commandsDesc = commandsSection.createDiv();
commandsDesc.addClass("task-timer-commands-desc");
const descParagraph = commandsDesc.createEl("p");
descParagraph.setText("Use the command palette to access timer data management:");
descParagraph.setText(
"Use the command palette to access timer data management:",
);
const commandsList = commandsDesc.createEl("ul");
const commands = [
{ name: "Export task timer data", desc: "Export all timer data to JSON" },
{ name: "Import task timer data", desc: "Import timer data from JSON file" },
{ name: "Export task timer data (YAML)", desc: "Export to YAML format" },
{ name: "Create task timer backup", desc: "Create a backup of active timers" },
{ name: "Show task timer statistics", desc: "Display timer usage statistics" }
{
name: "Export task timer data",
desc: "Export all timer data to JSON",
},
{
name: "Import task timer data",
desc: "Import timer data from JSON file",
},
{
name: "Export task timer data (YAML)",
desc: "Export to YAML format",
},
{
name: "Create task timer backup",
desc: "Create a backup of active timers",
},
{
name: "Show task timer statistics",
desc: "Display timer usage statistics",
},
];
commands.forEach(command => {
commands.forEach((command) => {
const listItem = commandsList.createEl("li");
const strongEl = listItem.createEl("strong");
strongEl.setText(command.name);
@ -839,12 +908,14 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
const warningEl = experimentalSection.createDiv();
warningEl.addClass("experimental-warning");
warningEl.createEl("strong").setText("⚠️ Warning: ");
warningEl.appendText("These features are experimental and may not be stable. Use at your own risk.");
warningEl.appendText(
"These features are experimental and may not be stable. Use at your own risk.",
);
// Dataflow Settings
const dataflowSection = experimentalSection.createDiv();
dataflowSection.addClass("experimental-dataflow-section");
const dataflowHeading = dataflowSection.createEl("h3");
dataflowHeading.setText("New Dataflow Architecture");
dataflowHeading.addClass("experimental-subsection-heading");
@ -852,24 +923,34 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
// Enable dataflow setting
new Setting(dataflowSection)
.setName("Enable Dataflow Architecture")
.setDesc("Enable the new dataflow-based task processing system. This is an experimental feature that replaces the current TaskManager.")
.setDesc(
"Enable the new dataflow-based task processing system. This is an experimental feature that replaces the current TaskManager.",
)
.addToggle((toggle) => {
toggle
.setValue(this.plugin.settings.experimental?.dataflowEnabled || false)
.setValue(
this.plugin.settings.experimental?.dataflowEnabled ||
false,
)
.onChange(async (value) => {
if (!this.plugin.settings.experimental) {
this.plugin.settings.experimental = {
dataflowEnabled: false
dataflowEnabled: false,
};
}
this.plugin.settings.experimental.dataflowEnabled = value;
this.plugin.settings.experimental.dataflowEnabled =
value;
await this.plugin.saveSettings();
// Show restart notice
if (value) {
const restartNotice = dataflowSection.createDiv();
restartNotice.addClass("experimental-restart-notice");
restartNotice.setText("⚠️ A restart may be required for the dataflow architecture to take full effect.");
restartNotice.addClass(
"experimental-restart-notice",
);
restartNotice.setText(
"⚠️ A restart may be required for the dataflow architecture to take full effect.",
);
}
});
});
@ -877,6 +958,8 @@ export class TaskProgressBarSettingTab extends PluginSettingTab {
// Dataflow info
const infoEl = dataflowSection.createDiv();
infoEl.addClass("experimental-info");
infoEl.setText("The new dataflow architecture provides improved performance and scalability for task processing. It includes better caching, worker-based processing, and more efficient data structures.");
infoEl.setText(
"The new dataflow architecture provides improved performance and scalability for task processing. It includes better caching, worker-based processing, and more efficient data structures.",
);
}
}

View file

@ -1535,7 +1535,6 @@
}
.task-genius-add-format-btn:hover {
background-color: var(--interactive-accent-hover);
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
transform: translateY(-1px);
}

View file

@ -1969,7 +1969,24 @@ const translations = {
"Quick capture configured": "Quick capture configured",
"Workflow settings enabled": "Workflow settings enabled",
"Advanced features enabled": "Advanced features enabled",
"File parsing customized": "File parsing customized"
"File parsing customized": "File parsing customized",
// Settings Migration
"Settings Migration Required": "Settings Migration Required",
"Task Genius has detected duplicate settings that can cause confusion. ": "Task Genius has detected duplicate settings that can cause confusion. ",
"We recommend migrating to the new unified FileSource system for better organization.": "We recommend migrating to the new unified FileSource system for better organization.",
"Auto-Migrate Settings": "Auto-Migrate Settings",
"Settings migrated successfully! ": "Settings migrated successfully! ",
" changes applied.": " changes applied.",
"Migration failed. Please check console for details.": "Migration failed. Please check console for details.",
"Learn More": "Learn More",
// FileSource
"FileSource": "FileSource",
"FileSource Configuration": "FileSource Configuration",
"Go to FileSource Settings": "Go to FileSource Settings",
"Note: This setting will be deprecated in favor of the unified FileSource system.": "Note: This setting will be deprecated in favor of the unified FileSource system.",
"Note: FileSource settings have been moved to a dedicated tab for better organization and to avoid duplication with file metadata parsing.": "Note: FileSource settings have been moved to a dedicated tab for better organization and to avoid duplication with file metadata parsing."
};
export default translations;

226
src/types/file-source.d.ts vendored Normal file
View file

@ -0,0 +1,226 @@
/**
* FileSource Type Definitions
*
* Core types for the FileSource feature that enables files to be recognized
* as tasks based on their metadata properties.
*/
import type { StandardTaskMetadata } from "./task";
/** Recognition strategies for identifying files as tasks */
export type RecognitionStrategy = "metadata" | "tag" | "template" | "path";
/** Extended metadata for file source tasks */
export interface FileSourceTaskMetadata extends StandardTaskMetadata {
/** Task source identifier */
source: "file-source";
/** Recognition strategy that identified this file as a task */
recognitionStrategy: RecognitionStrategy;
/** Recognition criteria that matched */
recognitionCriteria: string;
/** File creation/modification timestamps */
fileTimestamps: {
created: number;
modified: number;
};
/** Child task relationships */
childTasks: string[]; // IDs of tasks within this file
/** Project relationship (if file is also a project) */
projectData?: {
isProject: boolean;
projectName?: string;
projectType?: string;
};
}
/** Configuration for metadata-based recognition */
export interface MetadataRecognitionConfig {
enabled: boolean;
/** Metadata fields that make a file a task */
taskFields: string[];
/** Require all fields or any field */
requireAllFields: boolean;
}
/** Configuration for tag-based recognition */
export interface TagRecognitionConfig {
enabled: boolean;
/** Tags that make a file a task */
taskTags: string[];
/** Tag matching mode */
matchMode: "exact" | "prefix" | "contains";
}
/** Configuration for template-based recognition */
export interface TemplateRecognitionConfig {
enabled: boolean;
/** Template files or patterns */
templatePaths: string[];
/** Check template metadata */
checkTemplateMetadata: boolean;
}
/** Configuration for path-based recognition */
export interface PathRecognitionConfig {
enabled: boolean;
/** Path patterns that contain file tasks */
taskPaths: string[];
/** Pattern matching mode */
matchMode: "prefix" | "regex" | "glob";
}
/** Recognition strategies configuration */
export interface RecognitionStrategiesConfig {
metadata: MetadataRecognitionConfig;
tags: TagRecognitionConfig;
templates: TemplateRecognitionConfig;
paths: PathRecognitionConfig;
}
/** File task properties configuration */
export interface FileTaskPropertiesConfig {
/** Default task content source */
contentSource: "filename" | "title" | "h1" | "custom";
/** Custom content field (if contentSource is "custom") */
customContentField?: string;
/** Strip file extension from content */
stripExtension: boolean;
/** Default status for new file tasks */
defaultStatus: string;
/** Default priority for new file tasks */
defaultPriority?: number;
}
/** Relationship configuration */
export interface RelationshipsConfig {
/** Enable file-task to child-task relationships */
enableChildRelationships: boolean;
/** Inherit metadata from file to child tasks */
enableMetadataInheritance: boolean;
/** Metadata fields to inherit */
inheritanceFields: string[];
}
/** Performance configuration */
export interface PerformanceConfig {
/** Enable worker processing for file tasks */
enableWorkerProcessing: boolean;
/** Cache file task results */
enableCaching: boolean;
/** Cache TTL in milliseconds */
cacheTTL: number;
}
/** Advanced configuration */
export interface AdvancedConfig {
/** Custom recognition function */
customRecognitionFunction?: string;
/** Exclude patterns */
excludePatterns: string[];
/** Maximum file size to process (bytes) */
maxFileSize: number;
}
/**
* Configuration for mapping between file metadata status values and task status symbols
*/
export interface StatusMappingConfig {
/** Enable status mapping between metadata values and symbols */
enabled: boolean;
/** Map from human-readable metadata values to task status symbols
* e.g., "completed" "x", "in-progress" "/" */
metadataToSymbol: Record<string, string>;
/** Map from task status symbols to preferred metadata values
* e.g., "x" "completed", "/" "in-progress" */
symbolToMetadata: Record<string, string>;
/** Auto-detect common status patterns */
autoDetect: boolean;
/** Case sensitivity for status value matching */
caseSensitive: boolean;
}
/** Main FileSource configuration interface */
export interface FileSourceConfiguration {
/** Enable FileSource feature */
enabled: boolean;
/** Recognition strategies */
recognitionStrategies: RecognitionStrategiesConfig;
/** File task properties */
fileTaskProperties: FileTaskPropertiesConfig;
/** Relationship configuration */
relationships: RelationshipsConfig;
/** Performance configuration */
performance: PerformanceConfig;
/** Advanced configuration */
advanced: AdvancedConfig;
/** Status mapping configuration */
statusMapping: StatusMappingConfig;
}
/** Statistics interface for FileSource */
export interface FileSourceStats {
/** Whether FileSource is initialized */
initialized: boolean;
/** Number of files being tracked as tasks */
trackedFileCount: number;
/** Breakdown by recognition strategy */
recognitionBreakdown: Record<RecognitionStrategy, number>;
/** Last update timestamp */
lastUpdate: number;
/** Last update sequence number */
lastUpdateSeq: number;
}
/** Interface for recognition strategy implementations */
export interface RecognitionStrategyInterface {
/** Strategy name */
name: RecognitionStrategy;
/** Check if file matches this strategy */
matches(filePath: string, fileContent: string, fileCache: any): boolean;
/** Extract task metadata from file */
extractMetadata(filePath: string, fileContent: string, fileCache: any): Partial<FileSourceTaskMetadata>;
/** Get strategy configuration */
getConfig(): any;
/** Update strategy configuration */
updateConfig(config: any): void;
}
/** Update decision for change detection */
export interface UpdateDecision {
update: boolean;
reason: 'not-a-file-task' | 'task-status-changed' |
'task-properties-changed' | 'children-structure-changed' |
'no-relevant-changes';
details?: any;
}
/** File task cache structure for change detection */
export interface FileTaskCache {
/** Whether file is tracked as a task */
fileTaskExists: boolean;
/** Hash of frontmatter for quick property change detection */
frontmatterHash: string;
/** Set of child task IDs for structure tracking */
childTaskIds: Set<string>;
/** Last updated timestamp */
lastUpdated: number;
}

File diff suppressed because one or more lines are too long