feat(dataflow): enhance project configuration and metadata handling

- Add project configuration support to Augmentor and WorkerOrchestrator
- Implement enhanced metadata inheritance with project configs
- Add TgProject support for config-based project definitions
- Improve project metadata resolution and caching
- Add CSS styles for project info popover display
- Refactor ConfigurableTaskParser for better project handling
This commit is contained in:
Quorafind 2025-09-15 17:27:32 +08:00
parent fd6df16b39
commit ab5b945ae2
7 changed files with 1024 additions and 683 deletions

View file

@ -89,7 +89,11 @@ export class DataflowOrchestrator {
// Initial sync of settings to Augmentor to ensure correct inheritance behavior on startup
try {
const initFmi = this.plugin?.settings?.fileMetadataInheritance;
this.augmentor.updateSettings({ fileMetadataInheritance: initFmi });
const initProjectConfig = this.plugin?.settings?.projectConfig;
this.augmentor.updateSettings({
fileMetadataInheritance: initFmi,
projectConfig: initProjectConfig,
});
} catch (e) {
console.warn(
"[DataflowOrchestrator][init] Failed to sync settings to Augmentor",
@ -164,6 +168,12 @@ export class DataflowOrchestrator {
areaTagPrefix: this.plugin.settings.areaTagPrefix,
},
});
// Ensure worker parser receives enhanced project config at init
taskWorkerManager.updateSettings({
projectConfig: this.plugin.settings.projectConfig,
});
const projectWorkerManager = new ProjectDataWorkerManager({
vault,
metadataCache,

View file

@ -1,6 +1,7 @@
import type { Task, TgProject } from "../../types/task";
import { DateInheritanceAugmentor } from "./DateInheritanceAugmentor";
import { App, Vault, MetadataCache } from "obsidian";
import { ProjectConfiguration } from "../../common/setting-definition";
export interface AugmentContext {
filePath: string;
@ -41,6 +42,7 @@ export interface InheritanceStrategy {
export class Augmentor {
private strategy: InheritanceStrategy;
private dateInheritanceAugmentor?: DateInheritanceAugmentor;
private projectConfig?: ProjectConfiguration;
// Respect plugin setting: file frontmatter inheritance toggle
private fileFrontmatterInheritanceEnabled: boolean = true;
@ -68,9 +70,9 @@ export class Augmentor {
completed: false,
status: false,
recurrence: false,
onCompletion: false
onCompletion: false,
},
...options?.strategy
...options?.strategy,
};
// Initialize date inheritance augmentor if Obsidian context is available
@ -86,12 +88,23 @@ export class Augmentor {
/**
* Update settings from plugin to control inheritance behavior
*/
public updateSettings(settings: Partial<{
fileMetadataInheritance?: { enabled?: boolean; inheritFromFrontmatter?: boolean }
}>): void {
public updateSettings(
settings: Partial<{
fileMetadataInheritance?: {
enabled?: boolean;
inheritFromFrontmatter?: boolean;
};
projectConfig?: ProjectConfiguration;
}>
): void {
const f = settings.fileMetadataInheritance;
if (f) {
this.fileFrontmatterInheritanceEnabled = !!(f.enabled && f.inheritFromFrontmatter);
this.fileFrontmatterInheritanceEnabled = !!(
f.enabled && f.inheritFromFrontmatter
);
}
if (settings.projectConfig) {
this.projectConfig = settings.projectConfig;
}
}
@ -100,17 +113,23 @@ export class Augmentor {
*/
async merge(ctx: AugmentContext): Promise<Task[]> {
// First apply standard augmentation
let augmentedTasks = ctx.tasks.map(task => this.augmentTask(task, ctx));
let augmentedTasks = ctx.tasks.map((task) =>
this.augmentTask(task, ctx)
);
// Then apply date inheritance for time-only expressions if available
if (this.dateInheritanceAugmentor) {
try {
augmentedTasks = await this.dateInheritanceAugmentor.augmentTasksWithDateInheritance(
augmentedTasks,
ctx.filePath
);
augmentedTasks =
await this.dateInheritanceAugmentor.augmentTasksWithDateInheritance(
augmentedTasks,
ctx.filePath
);
} catch (error) {
console.warn('[Augmentor] Date inheritance augmentation failed:', error);
console.warn(
"[Augmentor] Date inheritance augmentation failed:",
error
);
// Continue with standard augmentation if date inheritance fails
}
}
@ -127,10 +146,10 @@ export class Augmentor {
fileMeta: ctx.fileMeta,
projectName: ctx.project?.name,
projectMeta: ctx.project?.data,
tasks
tasks,
};
return tasks.map(task => this.augmentTask(task, augmentCtx));
return tasks.map((task) => this.augmentTask(task, augmentCtx));
}
/**
@ -138,25 +157,21 @@ export class Augmentor {
*/
private augmentTask(task: Task, ctx: AugmentContext): Task {
const originalMetadata = task.metadata || {};
const enhancedMetadata = {...originalMetadata};
const enhancedMetadata = { ...originalMetadata };
// Special handling for priority: check both task.priority and metadata.priority
// Priority might be at task root level (from parser) or in metadata
// IMPORTANT: Once priority is set, it should NOT be overridden by inheritance
// Debug logging for priority processing
const debug = process.env.NODE_ENV === 'development';
if (debug && (enhancedMetadata.priority !== undefined || (task as any).priority !== undefined)) {
console.log('[Augmentor] Priority processing:', {
metadataPriority: enhancedMetadata.priority,
taskPriority: (task as any).priority,
filePath: ctx.filePath
});
}
// First, ensure we have the priority from task-level if it exists
if ((enhancedMetadata.priority === undefined || enhancedMetadata.priority === null) &&
(task as any).priority !== undefined && (task as any).priority !== null) {
if (
(enhancedMetadata.priority === undefined ||
enhancedMetadata.priority === null) &&
(task as any).priority !== undefined &&
(task as any).priority !== null
) {
enhancedMetadata.priority = (task as any).priority;
}
@ -166,15 +181,9 @@ export class Augmentor {
enhancedMetadata.priority = undefined;
} else if (enhancedMetadata.priority !== undefined) {
const originalPriority = enhancedMetadata.priority;
enhancedMetadata.priority = this.convertPriorityValue(enhancedMetadata.priority);
if (debug) {
console.log('[Augmentor] Priority conversion:', {
original: originalPriority,
converted: enhancedMetadata.priority,
filePath: ctx.filePath
});
}
enhancedMetadata.priority = this.convertPriorityValue(
enhancedMetadata.priority
);
}
// Apply inheritance for each metadata field
@ -184,23 +193,34 @@ export class Augmentor {
this.applyProjectReference(enhancedMetadata, ctx);
// Handle subtask inheritance if this is a parent task
if (originalMetadata.children && Array.isArray(originalMetadata.children)) {
if (
originalMetadata.children &&
Array.isArray(originalMetadata.children)
) {
this.applySubtaskInheritance(task, enhancedMetadata, ctx);
}
return {
...task,
metadata: enhancedMetadata
metadata: enhancedMetadata,
} as Task;
}
/**
* Apply scalar field inheritance: task > file > project > default
*/
private applyScalarInheritance(metadata: Record<string, any>, ctx: AugmentContext): void {
private applyScalarInheritance(
metadata: Record<string, any>,
ctx: AugmentContext
): void {
const scalarFields = [
'priority', 'context', 'area', 'estimatedTime', 'actualTime',
'useAsDateType', 'heading'
"priority",
"context",
"area",
"estimatedTime",
"actualTime",
"useAsDateType",
"heading",
];
for (const field of scalarFields) {
@ -211,14 +231,14 @@ export class Augmentor {
// Special handling for priority - NEVER apply default value
// Priority should only come from task itself, file, or project
if (field === 'priority') {
if (field === "priority") {
// Only check file and project sources, skip default
for (const source of ['file', 'project']) {
for (const source of ["file", "project"]) {
let value: any;
if (source === 'file') {
if (source === "file") {
value = ctx.fileMeta?.[field];
} else if (source === 'project') {
} else if (source === "project") {
value = ctx.projectMeta?.[field];
}
@ -233,20 +253,21 @@ export class Augmentor {
}
// Apply inheritance priority for other fields: file > project > default
for (const source of this.strategy.scalarPriority.slice(1)) { // Skip 'task' since we checked above
for (const source of this.strategy.scalarPriority.slice(1)) {
// Skip 'task' since we checked above
let value: any;
switch (source) {
case 'file':
case "file":
if (!this.fileFrontmatterInheritanceEnabled) {
continue; // Skip applying file-level values when inheritance is disabled
}
value = ctx.fileMeta?.[field];
break;
case 'project':
case "project":
value = ctx.projectMeta?.[field];
break;
case 'default':
case "default":
value = this.getDefaultValue(field);
break;
}
@ -262,31 +283,39 @@ export class Augmentor {
/**
* Apply array field inheritance with merge and deduplication
*/
private applyArrayInheritance(metadata: Record<string, any>, ctx: AugmentContext): void {
const arrayFields = ['tags', 'dependsOn'];
private applyArrayInheritance(
metadata: Record<string, any>,
ctx: AugmentContext
): void {
const arrayFields = ["tags", "dependsOn"];
for (const field of arrayFields) {
const taskArray = Array.isArray(metadata[field]) ? metadata[field] : [];
const fileArrayRaw = ctx.fileMeta && Array.isArray((ctx.fileMeta as any)[field]) ? (ctx.fileMeta as any)[field] : [];
const fileArray = this.fileFrontmatterInheritanceEnabled ? fileArrayRaw : [];
const projectArray = ctx.projectMeta && Array.isArray((ctx.projectMeta as any)[field]) ? (ctx.projectMeta as any)[field] : [];
const taskArray = Array.isArray(metadata[field])
? metadata[field]
: [];
const fileArrayRaw =
ctx.fileMeta && Array.isArray((ctx.fileMeta as any)[field])
? (ctx.fileMeta as any)[field]
: [];
const fileArray = this.fileFrontmatterInheritanceEnabled
? fileArrayRaw
: [];
const projectArray =
ctx.projectMeta &&
Array.isArray((ctx.projectMeta as any)[field])
? (ctx.projectMeta as any)[field]
: [];
// Normalize tags consistently (ensure leading #) before merging/dedup
const normalizeIfTags = (arr: any[]) => {
if (field !== 'tags') return arr;
if (field !== "tags") return arr;
return arr
.filter((t) => typeof t === 'string' && t.trim().length > 0)
.filter((t) => typeof t === "string" && t.trim().length > 0)
.map((t: string) => this.normalizeTag(t));
};
// If user disabled file frontmatter inheritance, do not inherit tags from file or project
if (field === 'tags' && !this.fileFrontmatterInheritanceEnabled) {
try {
console.debug('[Augmentor][Tags] Inheritance disabled. Keeping only task tags.', {
taskTags: normalizeIfTags(taskArray)
});
} catch {
}
if (field === "tags" && !this.fileFrontmatterInheritanceEnabled) {
metadata[field] = normalizeIfTags(taskArray);
continue;
}
@ -299,17 +328,33 @@ export class Augmentor {
// Merge based on strategy
switch (this.strategy.arrayMergeStrategy) {
case 'task-first':
mergedArray = [...taskArrNorm, ...fileArrNorm, ...projectArrNorm];
case "task-first":
mergedArray = [
...taskArrNorm,
...fileArrNorm,
...projectArrNorm,
];
break;
case 'file-first':
mergedArray = [...fileArrNorm, ...taskArrNorm, ...projectArrNorm];
case "file-first":
mergedArray = [
...fileArrNorm,
...taskArrNorm,
...projectArrNorm,
];
break;
case 'project-first':
mergedArray = [...projectArrNorm, ...taskArrNorm, ...fileArrNorm];
case "project-first":
mergedArray = [
...projectArrNorm,
...taskArrNorm,
...fileArrNorm,
];
break;
default:
mergedArray = [...taskArrNorm, ...fileArrNorm, ...projectArrNorm];
mergedArray = [
...taskArrNorm,
...fileArrNorm,
...projectArrNorm,
];
}
// Deduplicate while preserving order
@ -320,24 +365,27 @@ export class Augmentor {
// Normalize a tag to include leading # and trim whitespace
private normalizeTag(tag: any): string {
if (typeof tag !== 'string') return tag;
if (typeof tag !== "string") return tag;
const trimmed = tag.trim();
if (!trimmed) return trimmed;
return trimmed.startsWith('#') ? trimmed : `#${trimmed}`;
return trimmed.startsWith("#") ? trimmed : `#${trimmed}`;
}
/**
* Apply special field rules for status/completion and recurrence
*/
private applySpecialFieldRules(metadata: Record<string, any>, ctx: AugmentContext): void {
private applySpecialFieldRules(
metadata: Record<string, any>,
ctx: AugmentContext
): void {
// Status and completion: only from task level (never inherit)
if (this.strategy.statusCompletionSource === 'task-only') {
if (this.strategy.statusCompletionSource === "task-only") {
// These fields should only come from the task itself, never inherit
// (No action needed as we don't override existing task values)
}
// Recurrence: task explicit priority
if (this.strategy.recurrenceSource === 'task-explicit') {
if (this.strategy.recurrenceSource === "task-explicit") {
// Only use recurrence if explicitly set on task
if (!metadata.recurrence) {
// Don't inherit recurrence from file or project
@ -346,7 +394,12 @@ export class Augmentor {
}
// Date fields: inherit only if not already set
const dateFields = ['dueDate', 'startDate', 'scheduledDate', 'createdDate'];
const dateFields = [
"dueDate",
"startDate",
"scheduledDate",
"createdDate",
];
for (const field of dateFields) {
if (metadata[field] === undefined || metadata[field] === null) {
// Try file first, then project
@ -355,7 +408,10 @@ export class Augmentor {
if (fileValue !== undefined && fileValue !== null) {
metadata[field] = fileValue;
} else if (projectValue !== undefined && projectValue !== null) {
} else if (
projectValue !== undefined &&
projectValue !== null
) {
metadata[field] = projectValue;
}
}
@ -365,37 +421,109 @@ export class Augmentor {
/**
* Apply TgProject reference
*/
private applyProjectReference(metadata: Record<string, any>, ctx: AugmentContext): void {
private applyProjectReference(
metadata: Record<string, any>,
ctx: AugmentContext
): void {
// Derive project name from multiple sources with priority:
// 1) ctx.projectName (resolver-provided tgProject name)
// 2) ctx.projectMeta.project
// 3) ctx.fileMeta.project (frontmatter) - new fallback to avoid losing projects
const projectFromMeta =
typeof ctx.projectMeta?.project === "string" &&
ctx.projectMeta.project.trim()
? ctx.projectMeta.project.trim()
: undefined;
const projectFromFrontmatter =
typeof ctx.fileMeta?.project === "string" &&
ctx.fileMeta.project.trim()
? ctx.fileMeta.project.trim()
: undefined;
// Also consider configured metadataKey in frontmatter (e.g., projectName)
const metadataKeyFromConfig =
this.projectConfig?.metadataConfig?.metadataKey;
const projectFromMetadataKey =
metadataKeyFromConfig &&
typeof ctx.fileMeta?.[metadataKeyFromConfig] === "string" &&
String(ctx.fileMeta?.[metadataKeyFromConfig]).trim().length > 0
? String(ctx.fileMeta?.[metadataKeyFromConfig]).trim()
: undefined;
const effectiveProjectName =
ctx.projectName ||
projectFromMeta ||
projectFromMetadataKey ||
projectFromFrontmatter;
// Set project name if not already set
if (!metadata.project && ctx.projectName) {
metadata.project = ctx.projectName;
if (!metadata.project && effectiveProjectName) {
metadata.project = effectiveProjectName;
}
// If tgProject missing but metadataKey-derived value exists, synthesize tgProject now
if (!metadata.tgProject && projectFromMetadataKey) {
metadata.tgProject = {
type: "metadata",
name: projectFromMetadataKey,
source: metadataKeyFromConfig || "metadata",
readonly: true,
} as TgProject;
}
// Set TgProject if project metadata is available
// The tgProject should come from ProjectResolver which includes all necessary fields
// Prefer resolver-provided tgProject; otherwise synthesize from available context
if (ctx.projectMeta) {
// Check if we have a tgProject object in the project metadata
if (ctx.projectMeta.tgProject) {
// Only set from ctx.projectMeta when task doesn't already have a tgProject
if (ctx.projectMeta.tgProject && !metadata.tgProject) {
metadata.tgProject = ctx.projectMeta.tgProject;
} else if (ctx.projectName) {
// Fallback: construct from available data
} else if (effectiveProjectName && !metadata.tgProject) {
// Infer type/source when resolver didn't provide tgProject
const inferredType: TgProject["type"] = ctx.projectMeta
.configSource
? "config"
: "metadata";
metadata.tgProject = {
type: ctx.projectMeta.type || 'metadata',
name: ctx.projectName,
source: ctx.projectMeta.source || ctx.projectMeta.configSource || 'unknown',
readonly: ctx.projectMeta.readonly || false
type: inferredType,
name: effectiveProjectName,
source:
(ctx.projectMeta as any).source ||
ctx.projectMeta.configSource ||
"unknown",
readonly: (ctx.projectMeta as any).readonly || true,
} as TgProject;
}
}
// 2) If neither project nor tgProject are set, but we do have frontmatter project
// then set both from frontmatter to restore project recognition/counting
if (
!metadata.project &&
!metadata.tgProject &&
projectFromFrontmatter
) {
metadata.project = projectFromFrontmatter;
metadata.tgProject = {
type: "metadata",
name: projectFromFrontmatter,
source: "frontmatter",
readonly: true,
} as TgProject;
}
}
/**
* Apply subtask inheritance based on per-key control
*/
private applySubtaskInheritance(parentTask: Task, parentMetadata: Record<string, any>, ctx: AugmentContext): void {
private applySubtaskInheritance(
parentTask: Task,
parentMetadata: Record<string, any>,
ctx: AugmentContext
): void {
// This would typically involve finding child tasks and applying inheritance
// For now, we'll store the inheritance rules on the parent for child processing
parentMetadata._subtaskInheritanceRules = this.strategy.subtaskInheritance;
parentMetadata._subtaskInheritanceRules =
this.strategy.subtaskInheritance;
}
/**
@ -445,7 +573,8 @@ export class Augmentor {
}
// Try priority mapping (including emojis)
const mappedPriority = priorityMap[strValue.toLowerCase()] || priorityMap[strValue];
const mappedPriority =
priorityMap[strValue.toLowerCase()] || priorityMap[strValue];
if (mappedPriority !== undefined) {
return mappedPriority;
}
@ -465,7 +594,7 @@ export class Augmentor {
dependsOn: [],
estimatedTime: undefined,
actualTime: undefined,
useAsDateType: 'due'
useAsDateType: "due",
};
return defaults[field];
@ -475,14 +604,14 @@ export class Augmentor {
* Update inheritance strategy
*/
updateStrategy(strategy: Partial<InheritanceStrategy>): void {
this.strategy = {...this.strategy, ...strategy};
this.strategy = { ...this.strategy, ...strategy };
}
/**
* Get current inheritance strategy
*/
getStrategy(): InheritanceStrategy {
return {...this.strategy};
return { ...this.strategy };
}
/**
@ -495,20 +624,26 @@ export class Augmentor {
projectValue: any
): any {
// Handle arrays specially
if (Array.isArray(taskValue) || Array.isArray(fileValue) || Array.isArray(projectValue)) {
if (
Array.isArray(taskValue) ||
Array.isArray(fileValue) ||
Array.isArray(projectValue)
) {
const taskArray = Array.isArray(taskValue) ? taskValue : [];
const fileArray = Array.isArray(fileValue) ? fileValue : [];
const projectArray = Array.isArray(projectValue) ? projectValue : [];
const projectArray = Array.isArray(projectValue)
? projectValue
: [];
let merged: any[];
switch (this.strategy.arrayMergeStrategy) {
case 'task-first':
case "task-first":
merged = [...taskArray, ...fileArray, ...projectArray];
break;
case 'file-first':
case "file-first":
merged = [...fileArray, ...taskArray, ...projectArray];
break;
case 'project-first':
case "project-first":
merged = [...projectArray, ...taskArray, ...fileArray];
break;
default:
@ -517,21 +652,21 @@ export class Augmentor {
return Array.from(new Set(merged));
}
// Handle scalars with priority order
for (const source of this.strategy.scalarPriority) {
let value: any;
switch (source) {
case 'task':
case "task":
value = taskValue;
break;
case 'file':
case "file":
value = fileValue;
break;
case 'project':
case "project":
value = projectValue;
break;
case 'default':
case "default":
value = this.getDefaultValue(field);
break;
}
@ -544,4 +679,3 @@ export class Augmentor {
return undefined;
}
}

View file

@ -59,7 +59,7 @@ export class MarkdownTaskParser {
statusMapping: Record<string, string>,
timeParsingService?: TimeParsingService
): MarkdownTaskParser {
const newConfig = {...config, statusMapping};
const newConfig = { ...config, statusMapping };
return new MarkdownTaskParser(newConfig, timeParsingService);
}
@ -169,18 +169,18 @@ export class MarkdownTaskParser {
}
}
// Use provided tgProject or determine from config
// Prefer up-to-date detection for current file; fall back to provided tgProject
const taskTgProject =
tgProject || this.determineTgProject(filePath);
this.determineTgProject(filePath) || tgProject;
// Check for multiline comments
const [comment, linesToSkip] =
this.config.parseComments && i + 1 < lines.length
? this.extractMultilineComment(
lines,
i + 1,
actualSpaces
)
lines,
i + 1,
actualSpaces
)
: [undefined, 0];
i += linesToSkip;
@ -471,7 +471,7 @@ export class MarkdownTaskParser {
this.config.specialTagPrefixes[prefix] ??
this.config.specialTagPrefixes[
prefix.toLowerCase()
];
];
console.debug("[TPB] Tag parse", {
tag,
prefix,
@ -481,7 +481,7 @@ export class MarkdownTaskParser {
if (
metadataKey &&
this.config.metadataParseMode !==
MetadataParseMode.None
MetadataParseMode.None
) {
metadata[metadataKey] = value;
} else {
@ -760,7 +760,9 @@ export class MarkdownTaskParser {
// specialTagPrefixes format: { "prefixName": "metadataKey" }
// We need to reverse lookup: find prefix that maps to standard metadata keys
const lowerKey = key.toLowerCase();
for (const [prefix, metadataType] of Object.entries(this.config.specialTagPrefixes || {})) {
for (const [prefix, metadataType] of Object.entries(
this.config.specialTagPrefixes || {}
)) {
if (prefix.toLowerCase() === lowerKey) {
key = metadataType; // Map to the target metadata field (project, context, area)
break;
@ -770,17 +772,8 @@ export class MarkdownTaskParser {
if (key && value) {
// Debug: Log dataview metadata extraction for configured prefixes
const isConfiguredPrefix = Object.keys(this.config.specialTagPrefixes || {}).some(
prefix => prefix.toLowerCase() === parts[0].trim().toLowerCase()
);
if (isConfiguredPrefix) {
console.debug("[TPB] Dataview metadata with configured prefix", {
originalKey: parts[0].trim(),
mappedKey: key,
value,
specialTagPrefixes: Object.keys(this.config.specialTagPrefixes || {}),
});
}
const before = content.substring(0, start);
const after = content.substring(end + 1);
@ -801,7 +794,7 @@ export class MarkdownTaskParser {
const pos = content.indexOf(emoji);
if (pos !== -1) {
if (!earliestEmoji || pos < earliestEmoji.pos) {
earliestEmoji = {pos, emoji, key};
earliestEmoji = { pos, emoji, key };
}
}
}
@ -1211,11 +1204,11 @@ export class MarkdownTaskParser {
this.config.specialTagPrefixes[prefix] ??
this.config.specialTagPrefixes[
prefix.toLowerCase()
];
];
if (
metadataKey &&
this.config.metadataParseMode !==
MetadataParseMode.None
MetadataParseMode.None
) {
metadata[metadataKey] = value;
} else {
@ -1294,7 +1287,7 @@ export class MarkdownTaskParser {
);
}
this.indentStack.push({taskId, indentLevel, actualSpaces});
this.indentStack.push({ taskId, indentLevel, actualSpaces });
}
private getStatusFromMapping(rawStatus: string): string | undefined {
@ -1494,9 +1487,9 @@ export class MarkdownTaskParser {
id: enhancedTask.metadata.id,
dependsOn: enhancedTask.metadata.dependsOn
? enhancedTask.metadata.dependsOn
.split(",")
.map((id) => id.trim())
.filter((id) => id.length > 0)
.split(",")
.map((id) => id.trim())
.filter((id) => id.length > 0)
: undefined,
onCompletion: enhancedTask.metadata.onCompletion,
// Legacy compatibility fields that should remain in metadata
@ -1504,8 +1497,8 @@ export class MarkdownTaskParser {
heading: Array.isArray(enhancedTask.heading)
? enhancedTask.heading
: enhancedTask.heading
? [enhancedTask.heading]
: [],
? [enhancedTask.heading]
: [],
parent: enhancedTask.parentId,
tgProject: enhancedTask.tgProject,
},
@ -1762,7 +1755,7 @@ export class MarkdownTaskParser {
};
// Always convert priority values in task metadata, even if inheritance is disabled
const inherited = {...taskMetadata};
const inherited = { ...taskMetadata };
if (inherited.priority !== undefined) {
inherited.priority = convertPriorityValue(inherited.priority);
}
@ -1806,6 +1799,7 @@ export class MarkdownTaskParser {
"headingLevel",
"parent",
"parentId",
"children",
"childrenIds",
"indentLevel",
@ -1818,28 +1812,38 @@ export class MarkdownTaskParser {
// LEGACY: Inherit from file metadata (frontmatter) if available
if (this.fileMetadata) {
// Map configured frontmatter project key to standard 'project'
try {
const configuredProjectKey =
this.config.projectConfig?.metadataConfig?.metadataKey;
if (
configuredProjectKey &&
this.fileMetadata[configuredProjectKey] !== undefined &&
this.fileMetadata[configuredProjectKey] !== null &&
String(this.fileMetadata[configuredProjectKey]).trim() !==
""
) {
// When enhanced project + metadata detection are enabled,
// do NOT inject frontmatter project into metadata.project here.
// Let tgProject be determined via determineTgProject, and later
// Augmentor will mirror tgProject.name into metadata.project if needed.
const enhancedOn =
!!this.config.projectConfig?.enableEnhancedProject;
const metadataDetectOn =
!!this.config.projectConfig?.metadataConfig?.enabled;
if (!(enhancedOn && metadataDetectOn)) {
// Map configured frontmatter project key to standard 'project'
try {
const configuredProjectKey =
this.config.projectConfig?.metadataConfig?.metadataKey;
if (
inherited.project === undefined ||
inherited.project === null ||
inherited.project === ""
) {
inherited.project = String(
configuredProjectKey &&
this.fileMetadata[configuredProjectKey] !== undefined &&
this.fileMetadata[configuredProjectKey] !== null &&
String(
this.fileMetadata[configuredProjectKey]
).trim();
).trim() !== ""
) {
if (
inherited.project === undefined ||
inherited.project === null ||
inherited.project === ""
) {
inherited.project = String(
this.fileMetadata[configuredProjectKey]
).trim();
}
}
}
} catch {
} catch {}
}
for (const [key, value] of Object.entries(this.fileMetadata)) {
@ -1991,6 +1995,6 @@ export class ConfigurableTaskParser extends MarkdownTaskParser {
},
};
super({...defaultConfig, ...config}, timeParsingService);
super({ ...defaultConfig, ...config }, timeParsingService);
}
}

View file

@ -81,6 +81,7 @@ function parseTasksWithConfigurableParser(
// Raw parsing only - no project enhancement per dataflow architecture
// Project data will be handled by Augmentor in main thread
const tasks = parser.parseLegacy(
content,
filePath,

File diff suppressed because it is too large Load diff

View file

@ -293,3 +293,64 @@
.is-phone .projects-sidebar-header:has(.projects-sidebar-close) {
padding-right: var(--size-4-12);
}
/* Project info popover styling */
.tg-project-popover {
background: var(--background-secondary);
border: 1px solid var(--background-modifier-border);
border-radius: var(--radius-m);
box-shadow: var(--shadow-s);
padding: 10px 12px;
max-width: 520px;
max-height: 60vh;
overflow: auto;
}
.tg-project-props {
display: flex;
flex-direction: column;
gap: 6px;
}
/* Each row: key on the left, value on the right */
.tg-project-props > div {
display: grid;
grid-template-columns: minmax(90px, 160px) 1fr;
align-items: start;
column-gap: 10px;
padding: 2px 0;
}
/* Key cell */
.tg-project-props > div > div:first-child {
color: var(--text-muted);
font-size: 12px;
line-height: 1.3;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Value cell */
.tg-project-props > div > div:last-child {
color: var(--text-normal);
line-height: 1.4;
white-space: pre-wrap;
word-break: break-word;
}
/* Info button polish */
.projects-props-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: var(--icon-size, 24px);
height: var(--icon-size, 24px);
color: var(--text-muted);
cursor: pointer;
border-radius: var(--radius-s);
}
.projects-props-btn:hover {
background: var(--background-modifier-hover);
color: var(--text-normal);
}

File diff suppressed because one or more lines are too long