taskgenius_taskgenius-plugin/src/index.ts
Quorafind 12481c1798 feat(migration): version-keyed MigrationRegistry with tombstones (Phase 0 W1)
Replaces the ad-hoc settings migration calls in loadSettings with a
typed, atomic, version-keyed registry. Foundational infrastructure
for Phase 1's deprecation work — every v10 deprecation will register
a tombstone step here.

Capabilities
------------
- Atomic: clones settings, runs all applicable steps in-memory, commits
  only if every step succeeds. On any throw the original object is
  untouched.
- Dry-run: run({dryRun: true}) returns the diff without committing,
  so Phase 1's deprecation modals can show a preview before applying.
- Version-keyed: steps declare a targetVersion (semver). Registry runs
  every step where targetVersion ∈ (fromVersion, toVersion], in semver
  order. fromVersion is read from settings._meta.lastMigratedVersion,
  toVersion comes from manifest.version.
- Tombstone-aware: kind="tombstone" steps are first-class. Phase 1 will
  use these to retire deprecated fields, optionally salvaging into
  successor fields.
- Duplicate-id rejection: prevents Phase 1 PRs from accidentally
  shadowing existing tombstones.

Phase 0 scope
-------------
The legacy bundle step (v0.0.1-legacy-bundle) wraps the THREE existing
migration paths the plugin used before this commit:
  1. migrateSettings (multi-cycle status)
  2. migrateInheritanceSettings (projectConfig.metadataConfig → fileMetadataInheritance)
  3. fluentIntegration default backfill (inlined to avoid Component dep)

Bundle is byte-equivalent to the legacy direct calls — verified by
parameterized tests over 4 realistic data.json fixtures (legacy
multi-cycle, legacy inheritance, fresh install, partial fluent).

The sentinel tombstone (v0.0.2-sentinel-tombstone) targets a synthetic
field _meta._sentinelMarker that production never has, exercising the
tombstone code path without touching any real settings. The plan
originally suggested tombstoning taskStatusCycle/taskStatusMarks but
22+ files in the codebase still read those — Phase 1 audits readers
first, tombstone last. The sentinel doc includes that checklist.

Wiring
------
loadSettings now stashes the raw savedData on a transient field
(__transient_savedData__), runs the registry, then strips the field.
The legacy bundle step pulls savedData from there to detect old
projectConfig.metadataConfig.* keys that get dropped by the merge with
DEFAULT_SETTINGS. On registry failure (which shouldn't happen given
atomicity, but defensive) we fall back to the legacy direct calls so
the user is never left in a half-migrated state.

Discovery: SettingsMigrationManager exists in the codebase but is
never wired into index.ts — it's effectively dead code. Phase 1
audit will decide whether to delete it or wire it up. The plan
referenced it as an existing call site but reality says otherwise.

25/25 W1 tests pass (registry 14, legacy bundle 11). All other Phase 0
tests still green (73/73 mine).
2026-04-07 09:59:44 +08:00

2372 lines
64 KiB
TypeScript

import {
addIcon,
Editor,
editorInfoField,
MarkdownView,
Menu,
Notice,
Platform,
Plugin,
} from "obsidian";
import { taskProgressBarExtension } from "./editor-extensions/ui-widgets/progress-bar-widget";
import { taskTimerExtension } from "./editor-extensions/date-time/task-timer";
import { updateProgressBarInElement } from "./components/features/read-mode/ReadModeProgressBarWidget";
import { applyTaskTextMarks } from "./components/features/read-mode/ReadModeTextMark";
import {
DEFAULT_SETTINGS,
TaskProgressBarSettings,
} from "./common/setting-definition";
import { TaskProgressBarSettingTab } from "./setting";
import { EditorView } from "@codemirror/view";
import { autoCompleteParentExtension } from "./editor-extensions/autocomplete/parent-task-updater";
import { taskStatusSwitcherExtension } from "./editor-extensions/task-operations/status-switcher";
import { cycleCompleteStatusExtension } from "./editor-extensions/task-operations/status-cycler";
import {
updateWorkflowContextMenu,
workflowExtension,
} from "./editor-extensions/workflow/workflow-handler";
import { workflowDecoratorExtension } from "./editor-extensions/ui-widgets/workflow-decorator";
import { workflowRootEnterHandlerExtension } from "./editor-extensions/workflow/workflow-enter-handler";
import {
LETTER_PRIORITIES,
priorityPickerExtension,
TASK_PRIORITIES,
} from "./editor-extensions/ui-widgets/priority-picker";
import {
cycleTaskStatusBackward,
cycleTaskStatusForward,
} from "./commands/taskCycleCommands";
import { moveTaskCommand } from "./commands/taskMover";
import {
autoMoveCompletedTasksCommand,
moveCompletedTasksCommand,
moveIncompletedTasksCommand,
} from "./commands/completedTaskMover";
import {
convertTaskToWorkflowCommand,
convertToWorkflowRootCommand,
createQuickWorkflowCommand,
duplicateWorkflowCommand,
showWorkflowQuickActionsCommand,
startWorkflowHereCommand,
} from "./commands/workflowCommands";
import { datePickerExtension } from "./editor-extensions/date-time/date-picker";
import {
quickCaptureExtension,
quickCaptureState,
toggleQuickCapture,
} from "./editor-extensions/core/quick-capture-panel";
import {
migrateOldFilterOptions,
taskFilterExtension,
taskFilterState,
toggleTaskFilter,
} from "./editor-extensions/core/task-filter-panel";
import { Task } from "./types/task";
// Import the enhanced QuickCaptureModal and MinimalQuickCaptureModal
import { QuickCaptureModal } from "./components/features/quick-capture/modals/QuickCaptureModalWithSwitch";
import { MinimalQuickCaptureModal } from "./components/features/quick-capture/modals/MinimalQuickCaptureModalWithSwitch";
import { MinimalQuickCaptureSuggest } from "./components/features/quick-capture/suggest/MinimalQuickCaptureSuggest";
import { SuggestManager } from "@/components/ui/suggest";
import { t } from "./translations/helper";
import { TASK_VIEW_TYPE, TaskView } from "./pages/TaskView";
import { SettingsModal } from "./components/features/settings/SettingsModal";
import "./styles/global.scss";
import "./styles/setting.scss";
import "./styles/view.scss";
import "./styles/native-layout.scss";
import "./styles/view-config.scss";
import "./styles/task-status.scss";
import "./styles/task-selection.scss";
import "./styles/quadrant/quadrant.scss";
import "./styles/onboarding.scss";
import "./styles/universal-suggest.scss";
import "./styles/noise.scss";
import "./styles/changelog.scss";
import "./styles/widgets.scss";
import {
TASK_SPECIFIC_VIEW_TYPE,
TaskSpecificView,
} from "./pages/TaskSpecificView";
import {
TIMELINE_SIDEBAR_VIEW_TYPE,
TimelineSidebarView,
} from "./components/features/timeline-sidebar/TimelineSidebarView";
import { getStatusIcon, getTaskGeniusIcon } from "./icon";
import { RewardManager } from "./managers/reward-manager";
import { HabitManager } from "./managers/habit-manager";
import { TaskGeniusIconManager } from "./managers/icon-manager";
import { monitorTaskCompletedExtension } from "./editor-extensions/task-operations/completion-monitor";
import { sortTasksInDocument } from "./commands/sortTaskCommands";
import { taskGutterExtension } from "./editor-extensions/task-operations/gutter-marker";
import { autoDateManagerExtension } from "./editor-extensions/date-time/date-manager";
import { taskMarkCleanupExtension } from "./editor-extensions/task-operations/mark-cleanup";
import { IcsManager } from "./managers/ics-manager";
import { CalendarAuthManager } from "./managers/calendar-auth-manager";
import { FluentIntegration } from "./components/features/fluent/FluentIntegration";
import { ObsidianUriHandler } from "./utils/ObsidianUriHandler";
import {
migrateSettings,
repairStatusCycles,
} from "./utils/settings-migration";
import { createMigrationRegistry } from "./utils/migration";
import { VersionManager } from "./managers/version-manager";
import { RebuildProgressManager } from "./managers/rebuild-progress-manager";
import DesktopIntegrationManager from "./managers/desktop-integration-manager";
import { OnboardingConfigManager } from "./managers/onboarding-manager";
import { OnCompletionManager } from "./managers/completion-manager";
import { SettingsChangeDetector } from "./services/settings-change-detector";
import {
registerWidgetCommands,
registerWidgetViews,
} from "./widgets/registerWidgets";
import { registerWidgetCodeBlock } from "./widgets/codeblock/WidgetCodeBlockProcessor";
import {
ONBOARDING_VIEW_TYPE,
OnboardingView,
} from "./components/features/onboarding/OnboardingView";
import { registerTaskGeniusBasesViews } from "@/pages/bases/registerBasesViews";
import { TaskTimerExporter } from "./services/timer-export-service";
import { TaskTimerManager } from "./managers/timer-manager";
import { McpServerManager } from "./mcp/McpServerManager";
import { createDataflow } from "./dataflow/createDataflow";
import type { DataflowOrchestrator } from "./dataflow/Orchestrator";
import { WriteAPI } from "./dataflow/api/WriteAPI";
import { Events } from "./dataflow/events/Events";
import {
installWorkspaceDragMonitor,
registerRestrictedDnDViewTypes,
} from "./patches/workspace-dnd-patch";
import { FLUENT_TASK_VIEW } from "./pages/FluentTaskView";
import {
removePriorityAtCursor,
setPriorityAtCursor,
} from "./utils/task/curosr-priority-utils";
import { QuickCaptureSuggest } from "@/editor-extensions/autocomplete/task-metadata-suggest";
import { WorkspaceManager } from "@/components/features/fluent/managers/WorkspaceManager";
import {
CHANGELOG_VIEW_TYPE,
ChangelogView,
} from "./components/features/changelog/ChangelogView";
import { ChangelogManager } from "./managers/changelog-manager";
export default class TaskProgressBarPlugin extends Plugin {
settings: TaskProgressBarSettings;
// Dataflow orchestrator instance (primary architecture)
dataflowOrchestrator?: DataflowOrchestrator;
// Write API for dataflow architecture
writeAPI?: WriteAPI;
// Resolves once all async cleanup work scheduled by onunload() has settled.
// Obsidian's onunload() signature is sync (void), so async cleanup work has
// to be fired off without awaiting. Tests and any code that needs to know
// when the plugin is fully torn down should `await plugin.unloadComplete`.
// Reset on each onload(); see W2 in the v10 Phase 0 plan.
public unloadComplete: Promise<void> = Promise.resolve();
// Notification manager (desktop)
notificationManager?: DesktopIntegrationManager;
rewardManager: RewardManager;
habitManager: HabitManager;
// Task timer manager and exporter
taskTimerManager: TaskTimerManager;
taskTimerExporter: TaskTimerExporter;
// ICS manager instance
icsManager: IcsManager;
// Calendar auth manager instance (for OAuth providers)
calendarAuthManager?: CalendarAuthManager;
// Minimal quick capture suggest
minimalQuickCaptureSuggest: MinimalQuickCaptureSuggest;
// Regular quick capture suggest
quickCaptureSuggest: QuickCaptureSuggest;
// Global suggest manager
globalSuggestManager: SuggestManager;
// Version manager instance
versionManager: VersionManager;
// Changelog manager instance
changelogManager: ChangelogManager;
// Rebuild progress manager instance
rebuildProgressManager: RebuildProgressManager;
// Onboarding manager instance
onboardingConfigManager: OnboardingConfigManager;
settingsChangeDetector: SettingsChangeDetector;
// Preloaded tasks:
preloadedTasks: Task[] = [];
// Setting tab
settingTab: TaskProgressBarSettingTab;
// Workspace manager instance
workspaceManager?: WorkspaceManager;
// Task Genius Icon manager instance
taskGeniusIconManager: TaskGeniusIconManager;
// MCP Server manager instance (desktop only)
mcpServerManager?: McpServerManager;
// URI handler instance
uriHandler?: ObsidianUriHandler;
// OnCompletion manager instance
onCompletionManager?: OnCompletionManager;
// fluent Integration instance
fluentIntegration?: FluentIntegration;
// Deferred initialization guards
private coreCommandsRegistered = false;
private viewsRegistered = false;
private onboardingViewRegistered = false;
private viewCommandsRegistered = false;
private extendedCommandsScheduled = false;
private editorExtensionsRegistered = false;
private iconsDeferred = false;
async onload() {
console.time("[Task Genius] onload");
await this.loadSettings();
// Initialize version manager first
this.versionManager = new VersionManager(this.app, this);
this.addChild(this.versionManager);
this.changelogManager = new ChangelogManager(this);
this.registerView(
CHANGELOG_VIEW_TYPE,
(leaf) => new ChangelogView(leaf, this),
);
// Initialize onboarding config manager
this.onboardingConfigManager = new OnboardingConfigManager(this);
this.settingsChangeDetector = new SettingsChangeDetector(this);
// Initialize global suggest manager
this.globalSuggestManager = new SuggestManager(this.app, this);
this.workspaceManager = new WorkspaceManager(this);
this.workspaceManager.ensureDefaultWorkspaceInvariant();
// Initialize URI handler
this.uriHandler = new ObsidianUriHandler(this);
this.uriHandler.register();
// Initialize rebuild progress manager
this.rebuildProgressManager = new RebuildProgressManager();
if (this.settings.enableIndexer && this.settings.enableView) {
this.loadViews();
}
this.settingTab = new TaskProgressBarSettingTab(this.app, this);
this.addSettingTab(this.settingTab);
this.registerEvent(
this.app.workspace.on("editor-menu", (menu, editor) => {
if (this.settings.enablePriorityKeyboardShortcuts) {
menu.addItem((item) => {
item.setTitle(t("Set priority"));
item.setIcon("list-ordered");
// @ts-ignore
const submenu = item.setSubmenu() as Menu;
// Emoji priority commands
Object.entries(TASK_PRIORITIES).forEach(
([key, priority]) => {
if (key !== "none") {
submenu.addItem((item) => {
item.setTitle(
`${t("Set priority")}: ${
priority.text
}`,
);
item.setIcon("arrow-big-up-dash");
item.onClick(() => {
setPriorityAtCursor(
editor,
priority.emoji,
);
});
});
}
},
);
submenu.addSeparator();
// Letter priority commands
Object.entries(LETTER_PRIORITIES).forEach(
([key, priority]) => {
submenu.addItem((item) => {
item.setTitle(
`${t("Set priority")}: ${key}`,
);
item.setIcon("a-arrow-up");
item.onClick(() => {
setPriorityAtCursor(
editor,
`[#${key}]`,
);
});
});
},
);
// Remove priority command
submenu.addItem((item) => {
item.setTitle(t("Remove Priority"));
item.setIcon("list-x");
// @ts-ignore
item.setWarning(true);
item.onClick(() => {
removePriorityAtCursor(editor);
});
});
});
}
// Add workflow context menu
if (this.settings.workflow.enableWorkflow) {
updateWorkflowContextMenu(menu, editor, this);
}
}),
);
this.app.workspace.onLayoutReady(async () => {
console.time("[Task Genius] onLayoutReady");
await this.initializeDeferredStartup();
// Update workspace leaves when layout is ready
const deferWorkspaceLeaves =
this.app.workspace.getLeavesOfType(TASK_VIEW_TYPE);
const deferSpecificLeaves = this.app.workspace.getLeavesOfType(
TASK_SPECIFIC_VIEW_TYPE,
);
const deferTaskGeniusLeaves =
this.app.workspace.getLeavesOfType(FLUENT_TASK_VIEW);
[
...deferWorkspaceLeaves,
...deferSpecificLeaves,
...deferTaskGeniusLeaves,
].forEach((leaf) => {
leaf.loadIfDeferred();
});
// Initialize Task Genius Icon Manager
this.taskGeniusIconManager = new TaskGeniusIconManager(this);
this.addChild(this.taskGeniusIconManager);
// Initialize MCP Server Manager (desktop only)
if (Platform.isDesktopApp) {
this.mcpServerManager = new McpServerManager(this);
this.mcpServerManager.initialize();
// Initialize Notification Manager (desktop only)
this.notificationManager = new DesktopIntegrationManager(this);
this.addChild(this.notificationManager);
// Subscribe to task cache updates to inform notifications
this.registerEvent(
this.app.workspace.on(
Events.TASK_CACHE_UPDATED as any,
() => this.notificationManager?.onTaskCacheUpdated(),
),
);
}
// Check if user upgraded from intermediate version and show onboarding
await this.checkMigrationAndMaybeShowOnboarding();
// Check and show onboarding for first-time users
this.checkAndShowOnboarding();
if (this.settings.autoCompleteParent) {
this.registerEditorExtension([
autoCompleteParentExtension(this.app, this),
]);
}
this.registerMarkdownPostProcessor((el, ctx) => {
// Apply custom task text marks (replaces checkboxes with styled marks)
if (this.settings.enableTaskStatusSwitcher) {
applyTaskTextMarks({
plugin: this,
element: el,
ctx: ctx,
});
}
// Apply progress bars (existing functionality)
if (
this.settings.enableProgressbarInReadingMode &&
this.settings.progressBarDisplayMode !== "none"
) {
updateProgressBarInElement({
plugin: this,
element: el,
ctx: ctx,
});
}
});
if (this.settings.habit.enableHabits) {
this.habitManager = new HabitManager(this);
this.addChild(this.habitManager);
}
// Initialize Calendar Auth Manager for OAuth providers
this.calendarAuthManager = new CalendarAuthManager();
this.addChild(this.calendarAuthManager);
this.calendarAuthManager.registerProtocolHandler(this);
// Initialize ICS manager if sources are configured
if (this.settings.icsIntegration.sources.length > 0) {
this.icsManager = new IcsManager(
this.settings.icsIntegration,
this.settings,
this,
undefined, // timeParsingService
this.calendarAuthManager, // Pass auth manager for OAuth providers
);
this.addChild(this.icsManager);
// Initialize ICS manager
this.icsManager.initialize().catch((error) => {
console.error("Failed to initialize ICS manager:", error);
});
}
// Auto-open timeline sidebar if enabled
if (
this.settings.timelineSidebar.enableTimelineSidebar &&
this.settings.timelineSidebar.autoOpenOnStartup
) {
// Delay opening to ensure workspace is ready
setTimeout(() => {
this.activateTimelineSidebarView().catch((error) => {
console.error(
"Failed to auto-open timeline sidebar:",
error,
);
});
}, 1000);
}
this.maybeShowChangelog();
console.timeEnd("[Task Genius] onLayoutReady");
});
await this.migratePresetTaskFiltersIfNeeded();
this.registerCoreCommands();
console.timeEnd("[Task Genius] onload");
}
private async initializeDeferredStartup(): Promise<void> {
this.registerOnboardingView();
if (!this.settings.enableIndexer) {
this.scheduleExtendedCommands();
return;
}
console.time("[Task Genius] initializeIndexer");
await this.ensureFluentIntegration();
this.registerTaskViews();
this.installWorkspaceGuards();
this.registerViewCommands();
this.deferIconRegistration();
const dataflowInitialized = await this.initializeDataflowOrchestrator();
if (!dataflowInitialized) {
this.scheduleExtendedCommands();
console.timeEnd("[Task Genius] initializeIndexer");
return;
}
try {
await this.initializeDataflowWithVersionCheck();
} catch (error) {
console.error(
"[Plugin] Dataflow version check failed during startup:",
error,
);
}
this.initializeWriteAPI();
this.initializeOnCompletionManager();
if (this.settings.rewards.enableRewards) {
this.initializeRewardManager();
}
this.scheduleExtendedCommands();
console.timeEnd("[Task Genius] initializeIndexer");
}
private async ensureFluentIntegration(): Promise<void> {
if (this.fluentIntegration) {
return;
}
this.fluentIntegration = new FluentIntegration(this);
await this.fluentIntegration.migrateSettings();
this.fluentIntegration.register();
}
private registerTaskViews(): void {
if (this.viewsRegistered) {
return;
}
this.viewsRegistered = true;
this.registerOnboardingView();
// this.registerView(FLUENT_TASK_VIEW, (leaf) => new TaskView(leaf, this));
this.registerView(
TASK_SPECIFIC_VIEW_TYPE,
(leaf) => new TaskSpecificView(leaf, this),
);
this.registerView(
TIMELINE_SIDEBAR_VIEW_TYPE,
(leaf) => new TimelineSidebarView(leaf, this),
);
try {
registerTaskGeniusBasesViews(this);
} catch (error) {
console.log("Failed to register Bases views:", error);
}
try {
registerWidgetViews(this);
} catch (error) {
console.log("Failed to register Widget views:", error);
}
}
private registerOnboardingView(): void {
if (this.onboardingViewRegistered) {
return;
}
this.onboardingViewRegistered = true;
this.registerView(
ONBOARDING_VIEW_TYPE,
(leaf) =>
new OnboardingView(leaf, this, () => {
console.log("Onboarding completed successfully");
leaf.detach();
}),
);
}
private registerViewCommands(): void {
if (this.viewCommandsRegistered) {
return;
}
this.viewCommandsRegistered = true;
this.addCommand({
id: "open-task-genius-view",
name: t("Open Task Genius view"),
callback: () => {
this.activateTaskView();
},
});
this.addCommand({
id: "open-timeline-sidebar-view",
name: t("Open Timeline Sidebar"),
callback: () => {
this.activateTimelineSidebarView();
},
});
this.addCommand({
id: "open-task-genius-setup",
name: t("Open Task Genius Setup"),
callback: () => {
this.openOnboardingView();
},
});
this.addCommand({
id: "open-task-genius-changelog",
name: t("Open Task Genius changelog"),
callback: () => {
if (!this.changelogManager) {
return;
}
const targetVersion =
this.manifest?.version ||
this.settings.changelog.lastVersion;
if (!targetVersion) {
return;
}
const isBeta = targetVersion.toLowerCase().includes("beta");
this.changelogManager.openChangelog(targetVersion, isBeta);
},
});
this.addCommand({
id: "open-task-genius-settings-modal",
name: t("Open Task Genius settings"),
callback: () => {
this.openSettingsModal();
},
});
try {
registerWidgetCommands(this);
} catch (error) {
console.log("Failed to register Widget commands:", error);
}
try {
registerWidgetCodeBlock(this);
} catch (error) {
console.log(
"Failed to register Widget codeblock processor:",
error,
);
}
}
private deferIconRegistration(): void {
if (this.iconsDeferred) {
return;
}
this.iconsDeferred = true;
const registerIcons = () => {
addIcon("task-genius", getTaskGeniusIcon());
addIcon("completed", getStatusIcon("completed"));
addIcon("inProgress", getStatusIcon("inProgress"));
addIcon("planned", getStatusIcon("planned"));
addIcon("abandoned", getStatusIcon("abandoned"));
addIcon("notStarted", getStatusIcon("notStarted"));
this.addRibbonIcon(
"task-genius",
t("Open Task Genius view"),
() => {
this.activateTaskView();
},
);
};
const idle = (window as any)?.requestIdleCallback;
if (typeof idle === "function") {
idle(registerIcons);
} else {
setTimeout(registerIcons, 0);
}
}
private installWorkspaceGuards(): void {
installWorkspaceDragMonitor(this);
try {
registerRestrictedDnDViewTypes(FLUENT_TASK_VIEW);
} catch {}
}
private async initializeDataflowOrchestrator(): Promise<boolean> {
try {
this.dataflowOrchestrator = await createDataflow(
this.app,
this.app.vault,
this.app.metadataCache,
this,
{
configFileName:
this.settings.projectConfig?.configFile?.fileName ||
"project.md",
searchRecursively:
this.settings.projectConfig?.configFile
?.searchRecursively ?? true,
metadataKey:
this.settings.projectConfig?.metadataConfig
?.metadataKey || "project",
pathMappings:
this.settings.projectConfig?.pathMappings || [],
metadataMappings:
this.settings.projectConfig?.metadataMappings || [],
defaultProjectNaming: this.settings.projectConfig
?.defaultProjectNaming || {
strategy: "filename",
stripExtension: true,
enabled: false,
},
enhancedProjectEnabled:
this.settings.projectConfig?.enableEnhancedProject ??
false,
metadataConfigEnabled:
this.settings.projectConfig?.metadataConfig?.enabled ??
false,
configFileEnabled:
this.settings.projectConfig?.configFile?.enabled ??
false,
detectionMethods:
this.settings.projectConfig?.metadataConfig
?.detectionMethods || [],
},
);
return true;
} catch (error) {
console.error(
"[Plugin] Failed to initialize dataflow orchestrator:",
error,
);
new Notice(
t("Failed to initialize task system. Please restart Obsidian."),
);
this.dataflowOrchestrator = undefined;
return false;
}
}
private initializeWriteAPI(): void {
const dataflow = this.dataflowOrchestrator;
if (!dataflow) {
return;
}
const repository = dataflow.getRepository();
const getTaskById = async (id: string): Promise<Task | null> => {
try {
const task = await repository.getTaskById(id);
return task ?? null;
} catch (e) {
console.warn("Failed to get task from dataflow", e);
return null;
}
};
this.writeAPI = new WriteAPI(
this.app,
this.app.vault,
this.app.metadataCache,
this,
getTaskById,
);
}
private initializeOnCompletionManager(): void {
if (this.onCompletionManager) {
return;
}
this.onCompletionManager = new OnCompletionManager(this.app, this);
this.addChild(this.onCompletionManager);
console.log("[Plugin] OnCompletionManager initialized");
}
private initializeRewardManager(): void {
if (this.rewardManager) {
return;
}
this.rewardManager = new RewardManager(this);
this.addChild(this.rewardManager);
this.registerEditorExtension([
monitorTaskCompletedExtension(this.app, this),
]);
}
private scheduleExtendedCommands(): void {
if (this.extendedCommandsScheduled) {
return;
}
this.extendedCommandsScheduled = true;
setTimeout(() => {
try {
this.registerCommands();
this.ensureEditorExtensionsRegistered();
} catch (error) {
console.error(
"[Plugin] Failed registering deferred commands:",
error,
);
}
}, 100);
}
private ensureEditorExtensionsRegistered(): void {
if (this.editorExtensionsRegistered) {
return;
}
this.editorExtensionsRegistered = true;
this.registerEditorExt();
}
private registerCoreCommands(): void {
if (this.coreCommandsRegistered) {
return;
}
this.coreCommandsRegistered = true;
this.addCommand({
id: "quick-capture",
name: t("Quick Capture"),
callback: () => {
new QuickCaptureModal(this.app, this, undefined, true).open();
},
});
this.addCommand({
id: "minimal-quick-capture",
name: t("Minimal Quick Capture"),
callback: () => {
new MinimalQuickCaptureModal(this.app, this).open();
},
});
this.addCommand({
id: "quick-file-create",
name: t("Quick File Create"),
callback: () => {
const modal = new QuickCaptureModal(this.app, this, {
location: "file",
});
modal.open();
},
});
this.addCommand({
id: "toggle-task-filter",
name: t("Toggle task filter panel"),
editorCallback: (editor) => {
const view = editor.cm as EditorView;
if (!view) {
return;
}
view.dispatch({
effects: toggleTaskFilter.of(
!view.state.field(taskFilterState),
),
});
},
});
}
private async migratePresetTaskFiltersIfNeeded(): Promise<void> {
const presets = this.settings.taskFilter?.presetTaskFilters;
if (!presets) {
return;
}
console.time("[Task Genius] migratePresetTaskFilters");
this.settings.taskFilter.presetTaskFilters = presets.map(
(preset: any) => {
if (preset.options) {
preset.options = migrateOldFilterOptions(preset.options);
}
return preset;
},
);
await this.saveSettings();
console.timeEnd("[Task Genius] migratePresetTaskFilters");
}
registerCommands() {
if (this.settings.sortTasks) {
this.addCommand({
id: "sort-tasks-by-due-date",
name: t("Sort Tasks in Section"),
editorCallback: (editor: Editor, view: MarkdownView) => {
const editorView = (editor as any).cm as EditorView;
if (!editorView) return;
const changes = sortTasksInDocument(
editorView,
this,
false,
);
if (changes) {
new Notice(
t(
"Tasks sorted (using settings). Change application needs refinement.",
),
);
} else {
// Notice is already handled within sortTasksInDocument if no changes or sorting disabled
}
},
});
this.addCommand({
id: "sort-tasks-in-entire-document",
name: t("Sort Tasks in Entire Document"),
editorCallback: (editor: Editor, view: MarkdownView) => {
const editorView = (editor as any).cm as EditorView;
if (!editorView) return;
const changes = sortTasksInDocument(editorView, this, true);
if (changes) {
const info = editorView.state.field(editorInfoField);
if (!info || !info.file) return;
this.app.vault.process(info.file, (data) => {
return changes;
});
new Notice(
t("Entire document sorted (using settings)."),
);
} else {
new Notice(
t("Tasks already sorted or no tasks found."),
);
}
},
});
}
// Add command for cycling task status forward
this.addCommand({
id: "cycle-task-status-forward",
name: t("Cycle task status forward"),
editorCheckCallback: (checking, editor, ctx) => {
return cycleTaskStatusForward(checking, editor, ctx, this);
},
});
// Add command for cycling task status backward
this.addCommand({
id: "cycle-task-status-backward",
name: t("Cycle task status backward"),
editorCheckCallback: (checking, editor, ctx) => {
return cycleTaskStatusBackward(checking, editor, ctx, this);
},
});
if (this.settings.enableIndexer) {
// // Add command to refresh the task index
// this.addCommand({
// id: "refresh-task-index",
// name: t("Refresh task index"),
// callback: async () => {
// try {
// new Notice(t("Refreshing task index..."));
// // Check if dataflow is enabled
// if (
// this.settings?.enableIndexer &&
// this.dataflowOrchestrator
// ) {
// // Use dataflow orchestrator for refresh
// console.log(
// "[Command] Refreshing task index via dataflow",
// );
// // Re-scan all files to refresh the index
// const files = this.app.vault.getMarkdownFiles();
// const canvasFiles = this.app.vault
// .getFiles()
// .filter((f) => f.extension === "canvas");
// const allFiles = [...files, ...canvasFiles];
// // Process files in batches
// const batchSize = 50;
// for (
// let i = 0;
// i < allFiles.length;
// i += batchSize
// ) {
// const batch = allFiles.slice(i, i + batchSize);
// await Promise.all(
// batch.map((file) =>
// (
// this.dataflowOrchestrator as any
// ).processFileImmediate(file),
// ),
// );
// }
// // Refresh ICS events if available
// const icsSource = (this.dataflowOrchestrator as any)
// .icsSource;
// if (icsSource) {
// await icsSource.refresh();
// }
// }
// // else {
// // // Use legacy task manager
// // await this.taskManager.initialize();
// // }
// new Notice(t("Task index refreshed"));
// } catch (error) {
// console.error("Failed to refresh task index:", error);
// new Notice(t("Failed to refresh task index"));
// }
// },
// });
// Add command to force reindex all tasks by clearing cache
this.addCommand({
id: "force-reindex-tasks",
name: t("Force reindex all tasks"),
callback: async () => {
try {
// Check if dataflow is enabled
if (
this.settings?.enableIndexer &&
this.dataflowOrchestrator
) {
// Use dataflow orchestrator for force reindex
console.log(
"[Command] Force reindexing via dataflow",
);
new Notice(
t(
"Clearing task cache and rebuilding index...",
),
);
// Clear all caches and rebuild from scratch
await this.dataflowOrchestrator.rebuild();
// Refresh ICS events after rebuild
const icsSource = (
this
.dataflowOrchestrator as DataflowOrchestrator
).icsSource;
if (icsSource) {
await icsSource.refresh();
}
new Notice(t("Task index completely rebuilt"));
} else {
// No dataflow available
new Notice(t("Task system not initialized"));
}
} catch (error) {
console.error("Failed to force reindex tasks:", error);
new Notice(t("Failed to force reindex tasks"));
}
},
});
}
// Habit commands
this.addCommand({
id: "reindex-habits",
name: t("Reindex habits"),
callback: async () => {
try {
await this.habitManager?.initializeHabits();
new Notice(t("Habit index refreshed"));
} catch (e) {
console.error("Failed to reindex habits", e);
new Notice(t("Failed to refresh habit index"));
}
},
});
// Add priority keyboard shortcuts commands
if (this.settings.enablePriorityKeyboardShortcuts) {
// Emoji priority commands
Object.entries(TASK_PRIORITIES).forEach(([key, priority]) => {
if (key !== "none") {
this.addCommand({
id: `set-priority-${key}`,
name: `${t("Set priority")} ${priority.text}`,
editorCallback: (editor) => {
setPriorityAtCursor(editor, priority.emoji);
},
});
}
});
// Letter priority commands
Object.entries(LETTER_PRIORITIES).forEach(([key, priority]) => {
this.addCommand({
id: `set-priority-letter-${key}`,
name: `${t("Set priority")} ${key}`,
editorCallback: (editor) => {
setPriorityAtCursor(editor, `[#${key}]`);
},
});
});
// Remove priority command
this.addCommand({
id: "remove-priority",
name: t("Remove priority"),
editorCallback: (editor) => {
removePriorityAtCursor(editor);
},
});
}
// Add command for moving tasks
this.addCommand({
id: "move-task-to-file",
name: t("Move task to another file"),
editorCheckCallback: (checking, editor, ctx) => {
return moveTaskCommand(checking, editor, ctx, this);
},
});
// Add commands for moving completed tasks
if (this.settings.completedTaskMover.enableCompletedTaskMover) {
// Command for moving all completed subtasks and their children
this.addCommand({
id: "move-completed-subtasks-to-file",
name: t("Move all completed subtasks to another file"),
editorCheckCallback: (checking, editor, ctx) => {
return moveCompletedTasksCommand(
checking,
editor,
ctx,
this,
"allCompleted",
);
},
});
// Command for moving direct completed children
this.addCommand({
id: "move-direct-completed-subtasks-to-file",
name: t("Move direct completed subtasks to another file"),
editorCheckCallback: (checking, editor, ctx) => {
return moveCompletedTasksCommand(
checking,
editor,
ctx,
this,
"directChildren",
);
},
});
// Command for moving all subtasks (completed and uncompleted)
this.addCommand({
id: "move-all-subtasks-to-file",
name: t("Move all subtasks to another file"),
editorCheckCallback: (checking, editor, ctx) => {
return moveCompletedTasksCommand(
checking,
editor,
ctx,
this,
"all",
);
},
});
// Auto-move commands (using default settings)
if (this.settings.completedTaskMover.enableAutoMove) {
this.addCommand({
id: "auto-move-completed-subtasks",
name: t("Auto-move completed subtasks to default file"),
editorCheckCallback: (checking, editor, ctx) => {
return autoMoveCompletedTasksCommand(
checking,
editor,
ctx,
this,
"allCompleted",
);
},
});
this.addCommand({
id: "auto-move-direct-completed-subtasks",
name: t(
"Auto-move direct completed subtasks to default file",
),
editorCheckCallback: (checking, editor, ctx) => {
return autoMoveCompletedTasksCommand(
checking,
editor,
ctx,
this,
"directChildren",
);
},
});
this.addCommand({
id: "auto-move-all-subtasks",
name: t("Auto-move all subtasks to default file"),
editorCheckCallback: (checking, editor, ctx) => {
return autoMoveCompletedTasksCommand(
checking,
editor,
ctx,
this,
"all",
);
},
});
}
}
// Add commands for moving incomplete tasks
if (this.settings.completedTaskMover.enableIncompletedTaskMover) {
// Command for moving all incomplete subtasks and their children
this.addCommand({
id: "move-incompleted-subtasks-to-file",
name: t("Move all incomplete subtasks to another file"),
editorCheckCallback: (checking, editor, ctx) => {
return moveIncompletedTasksCommand(
checking,
editor,
ctx,
this,
"allIncompleted",
);
},
});
// Command for moving direct incomplete children
this.addCommand({
id: "move-direct-incompleted-subtasks-to-file",
name: t("Move direct incomplete subtasks to another file"),
editorCheckCallback: (checking, editor, ctx) => {
return moveIncompletedTasksCommand(
checking,
editor,
ctx,
this,
"directIncompletedChildren",
);
},
});
// Auto-move commands for incomplete tasks (using default settings)
if (this.settings.completedTaskMover.enableIncompletedAutoMove) {
this.addCommand({
id: "auto-move-incomplete-subtasks",
name: t("Auto-move incomplete subtasks to default file"),
editorCheckCallback: (checking, editor, ctx) => {
return autoMoveCompletedTasksCommand(
checking,
editor,
ctx,
this,
"allIncompleted",
);
},
});
this.addCommand({
id: "auto-move-direct-incomplete-subtasks",
name: t(
"Auto-move direct incomplete subtasks to default file",
),
editorCheckCallback: (checking, editor, ctx) => {
return autoMoveCompletedTasksCommand(
checking,
editor,
ctx,
this,
"directIncompletedChildren",
);
},
});
}
}
// Add command for toggling quick capture panel in editor
this.addCommand({
id: "toggle-quick-capture",
name: t("Toggle quick capture panel in editor"),
editorCallback: (editor) => {
const editorView = editor.cm as EditorView;
try {
// Check if the state field exists
const stateField =
editorView.state.field(quickCaptureState);
// Toggle the quick capture panel
editorView.dispatch({
effects: toggleQuickCapture.of(!stateField),
});
} catch (e) {
// Field doesn't exist, create it with value true (to show panel)
editorView.dispatch({
effects: toggleQuickCapture.of(true),
});
}
},
});
this.addCommand({
id: "toggle-quick-capture-globally",
name: t("Toggle quick capture panel in editor (Globally)"),
callback: () => {
const activeLeaf =
this.app.workspace.getActiveViewOfType(MarkdownView);
if (activeLeaf && activeLeaf.editor) {
// If we're in a markdown editor, use the editor command
const editorView = activeLeaf.editor.cm as EditorView;
// Import necessary functions dynamically to avoid circular dependencies
try {
// Show the quick capture panel
editorView.dispatch({
effects: toggleQuickCapture.of(true),
});
} catch (e) {
// No quick capture state found, try to add the extension first
// This is a simplified approach and might not work in all cases
this.registerEditorExtension([
quickCaptureExtension(this.app, this),
]);
// Try again after registering the extension
setTimeout(() => {
try {
editorView.dispatch({
effects: toggleQuickCapture.of(true),
});
} catch (e) {
new Notice(
t(
"Could not open quick capture panel in the current editor",
),
);
}
}, 100);
}
}
},
});
// Workflow commands
if (this.settings.workflow.enableWorkflow) {
this.addCommand({
id: "create-quick-workflow",
name: t("Create quick workflow"),
editorCheckCallback: (checking, editor, ctx) => {
return createQuickWorkflowCommand(
checking,
editor,
ctx,
this,
);
},
});
this.addCommand({
id: "convert-task-to-workflow",
name: t("Convert task to workflow template"),
editorCheckCallback: (checking, editor, ctx) => {
return convertTaskToWorkflowCommand(
checking,
editor,
ctx,
this,
);
},
});
this.addCommand({
id: "start-workflow-here",
name: t("Start workflow here"),
editorCheckCallback: (checking, editor, ctx) => {
return startWorkflowHereCommand(
checking,
editor,
ctx,
this,
);
},
});
this.addCommand({
id: "convert-to-workflow-root",
name: t("Convert current task to workflow root"),
editorCheckCallback: (checking, editor, ctx) => {
return convertToWorkflowRootCommand(
checking,
editor,
ctx,
this,
);
},
});
this.addCommand({
id: "duplicate-workflow",
name: t("Duplicate workflow"),
editorCheckCallback: (checking, editor, ctx) => {
return duplicateWorkflowCommand(
checking,
editor,
ctx,
this,
);
},
});
this.addCommand({
id: "workflow-quick-actions",
name: t("Workflow quick actions"),
editorCheckCallback: (checking, editor, ctx) => {
return showWorkflowQuickActionsCommand(
checking,
editor,
ctx,
this,
);
},
});
}
// Task timer export/import commands
// Ensure timer manager and exporter are initialized if timer is enabled
if (this.settings.taskTimer?.enabled) {
if (!this.taskTimerManager) {
this.taskTimerManager = new TaskTimerManager(
this.settings.taskTimer,
);
}
if (!this.taskTimerExporter) {
this.taskTimerExporter = new TaskTimerExporter(
this.taskTimerManager,
);
}
}
if (this.settings.taskTimer?.enabled && this.taskTimerExporter) {
this.addCommand({
id: "export-task-timer-data",
name: "Export task timer data",
callback: async () => {
try {
const stats = this.taskTimerExporter.getExportStats();
if (stats.activeTimers === 0) {
new Notice("No timer data to export");
return;
}
const jsonData =
this.taskTimerExporter.exportToJSON(true);
// Create a blob and download link
const blob = new Blob([jsonData], {
type: "application/json",
});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `task-timer-data-${
new Date().toISOString().split("T")[0]
}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
new Notice(
`Exported ${stats.activeTimers} timer records`,
);
} catch (error) {
console.error("Error exporting timer data:", error);
new Notice("Failed to export timer data");
}
},
});
this.addCommand({
id: "import-task-timer-data",
name: "Import task timer data",
callback: async () => {
try {
// Create file input for JSON import
const input = document.createElement("input");
input.type = "file";
input.accept = ".json";
input.onchange = async (e) => {
const file = (e.target as HTMLInputElement)
.files?.[0];
if (!file) return;
try {
const text = await file.text();
const success =
this.taskTimerExporter.importFromJSON(text);
if (success) {
new Notice(
"Timer data imported successfully",
);
} else {
new Notice(
"Failed to import timer data - invalid format",
);
}
} catch (error) {
console.error(
"Error importing timer data:",
error,
);
new Notice("Failed to import timer data");
}
};
input.click();
} catch (error) {
console.error("Error setting up import:", error);
new Notice("Failed to set up import");
}
},
});
this.addCommand({
id: "export-task-timer-yaml",
name: "Export task timer data (YAML)",
callback: async () => {
try {
const stats = this.taskTimerExporter.getExportStats();
if (stats.activeTimers === 0) {
new Notice("No timer data to export");
return;
}
const yamlData =
this.taskTimerExporter.exportToYAML(true);
// Create a blob and download link
const blob = new Blob([yamlData], {
type: "text/yaml",
});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `task-timer-data-${
new Date().toISOString().split("T")[0]
}.yaml`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
new Notice(
`Exported ${stats.activeTimers} timer records to YAML`,
);
} catch (error) {
console.error(
"Error exporting timer data to YAML:",
error,
);
new Notice("Failed to export timer data to YAML");
}
},
});
this.addCommand({
id: "backup-task-timer-data",
name: "Create task timer backup",
callback: async () => {
try {
const backupData =
this.taskTimerExporter.createBackup();
// Create a blob and download link
const blob = new Blob([backupData], {
type: "application/json",
});
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = `task-timer-backup-${new Date()
.toISOString()
.replace(/[:.]/g, "-")}.json`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
new Notice("Task timer backup created");
} catch (error) {
console.error("Error creating timer backup:", error);
new Notice("Failed to create timer backup");
}
},
});
this.addCommand({
id: "show-task-timer-stats",
name: "Show task timer statistics",
callback: () => {
try {
const stats = this.taskTimerExporter.getExportStats();
let message = `Task Timer Statistics:\n`;
message += `Active timers: ${stats.activeTimers}\n`;
message += `Total duration: ${Math.round(
stats.totalDuration / 60000,
)} minutes\n`;
if (stats.oldestTimer) {
message += `Oldest timer: ${stats.oldestTimer}\n`;
}
if (stats.newestTimer) {
message += `Newest timer: ${stats.newestTimer}`;
}
new Notice(message, 10000);
} catch (error) {
console.error("Error getting timer stats:", error);
new Notice("Failed to get timer statistics");
}
},
});
}
}
registerEditorExt() {
this.registerEditorExtension([
taskProgressBarExtension(this.app, this),
]);
// Add task timer extension
if (this.settings.taskTimer?.enabled) {
// Initialize task timer manager and exporter
if (!this.taskTimerManager) {
this.taskTimerManager = new TaskTimerManager(
this.settings.taskTimer,
);
}
if (!this.taskTimerExporter) {
this.taskTimerExporter = new TaskTimerExporter(
this.taskTimerManager,
);
}
this.registerEditorExtension([taskTimerExtension(this)]);
}
this.settings.taskGutter.enableTaskGutter &&
this.registerEditorExtension([taskGutterExtension(this.app, this)]);
this.settings.enableTaskStatusSwitcher &&
this.settings.enableCustomTaskMarks &&
this.registerEditorExtension([
taskStatusSwitcherExtension(this.app, this),
]);
// Add priority picker extension
if (this.settings.enablePriorityPicker) {
this.registerEditorExtension([
priorityPickerExtension(this.app, this),
]);
}
// Add date picker extension
if (this.settings.enableDatePicker) {
this.registerEditorExtension([datePickerExtension(this.app, this)]);
}
// Add workflow extension
if (this.settings.workflow.enableWorkflow) {
this.registerEditorExtension([workflowExtension(this.app, this)]);
this.registerEditorExtension([
workflowDecoratorExtension(this.app, this),
]);
this.registerEditorExtension([
workflowRootEnterHandlerExtension(this.app, this),
]);
}
// CRITICAL: Register in reverse order of desired execution
// (transactionFilters execute in reverse registration order)
//
// Desired execution order:
// 1. cycleCompleteStatus (processes Obsidian toggle, cycles status)
// 2. autoDateManager (adds/removes dates based on new status)
// 3. workflow (updates workflow stages if needed)
//
// Registration order (reverse):
// 1. workflow (already registered above)
// 2. autoDateManager (register second, executes second)
// 3. cycleCompleteStatus (register last, executes first)
if (this.settings.autoDateManager.enabled) {
this.registerEditorExtension([
autoDateManagerExtension(this.app, this),
]);
}
if (this.settings.enableCycleCompleteStatus) {
this.registerEditorExtension([
cycleCompleteStatusExtension(this.app, this),
]);
}
// Add quick capture extension
if (this.settings.quickCapture.enableQuickCapture) {
this.registerEditorExtension([
quickCaptureExtension(this.app, this),
]);
}
// Initialize minimal quick capture suggest
if (this.settings.quickCapture.enableMinimalMode) {
this.minimalQuickCaptureSuggest = new MinimalQuickCaptureSuggest(
this.app,
this,
);
this.registerEditorSuggest(this.minimalQuickCaptureSuggest);
}
// Add task filter extension
if (this.settings.taskFilter.enableTaskFilter) {
this.registerEditorExtension([taskFilterExtension(this)]);
}
// Add task mark cleanup extension (always enabled)
this.registerEditorExtension([taskMarkCleanupExtension()]);
}
onunload() {
// Synchronous cleanup paths run immediately. Asynchronous cleanup
// (currently just dataflowOrchestrator.cleanup() which awaits Repository
// persistence) is gathered into a single promise exposed as
// `unloadComplete` so tests and any external observer can await full
// teardown. Obsidian itself never awaits this — its onunload signature
// is sync — but at least listeners + workers + persistence get a chance
// to settle before the next plugin lifecycle, instead of racing.
const asyncTasks: Array<Promise<void>> = [];
// Clean up global suggest manager (sync)
if (this.globalSuggestManager) {
this.globalSuggestManager.cleanup();
}
// Bases views are automatically unregistered by Obsidian when plugin unloads
// Clean up dataflow orchestrator (async — capture into asyncTasks)
if (this.dataflowOrchestrator) {
const orch = this.dataflowOrchestrator;
// Null out immediately so any other code path that fires during
// teardown can't reach into a half-cleaned-up orchestrator.
this.dataflowOrchestrator = undefined;
asyncTasks.push(
orch.cleanup().catch((error) => {
console.error(
"Error cleaning up dataflow orchestrator:",
error,
);
}),
);
}
// Clean up MCP server manager (desktop only, sync)
if (this.mcpServerManager) {
this.mcpServerManager.cleanup();
}
// Task Genius Icon Manager cleanup is handled automatically by Component system
// Expose a promise so tests / external observers can know when async
// cleanup is fully done. Never rejects — individual catches above
// already log errors.
this.unloadComplete = Promise.all(asyncTasks).then(() => undefined);
}
/**
* Check and show onboarding for first-time users or users who request it
*/
private async checkAndShowOnboarding(): Promise<void> {
try {
// Check if this is the first install and onboarding hasn't been completed
const versionResult =
await this.versionManager.checkVersionChange();
const isFirstInstall = versionResult.versionInfo.isFirstInstall;
const shouldShowOnboarding =
this.onboardingConfigManager.shouldShowOnboarding();
// For existing users with changes, let the view handle the async detection
// For new users, show onboarding directly
if (
(isFirstInstall && shouldShowOnboarding) ||
(!isFirstInstall &&
shouldShowOnboarding &&
this.settingsChangeDetector.hasUserMadeChanges())
) {
// Small delay to ensure UI is ready
this.openOnboardingView();
}
} catch (error) {
console.error("Failed to check onboarding status:", error);
}
}
/**
* Open the onboarding view in a new leaf
*/
async openOnboardingView(): Promise<void> {
const { workspace } = this.app;
// Check if onboarding view is already open
const existingLeaf = workspace.getLeavesOfType(ONBOARDING_VIEW_TYPE)[0];
if (existingLeaf) {
workspace.revealLeaf(existingLeaf);
return;
}
// Create a new leaf in the main area and open the onboarding view
const leaf = workspace.getLeaf("tab");
await leaf.setViewState({ type: ONBOARDING_VIEW_TYPE });
workspace.revealLeaf(leaf);
}
async closeAllViewsFromTaskGenius() {
const { workspace } = this.app;
const v1Leaves = workspace.getLeavesOfType(TASK_VIEW_TYPE);
v1Leaves.forEach((leaf) => leaf.detach());
const v2Leaves = workspace.getLeavesOfType(FLUENT_TASK_VIEW);
v2Leaves.forEach((leaf) => leaf.detach());
const specificLeaves = workspace.getLeavesOfType(
TASK_SPECIFIC_VIEW_TYPE,
);
specificLeaves.forEach((leaf) => leaf.detach());
const timelineLeaves = workspace.getLeavesOfType(
TIMELINE_SIDEBAR_VIEW_TYPE,
);
timelineLeaves.forEach((leaf) => leaf.detach());
const changelogLeaves = workspace.getLeavesOfType(CHANGELOG_VIEW_TYPE);
changelogLeaves.forEach((leaf) => leaf.detach());
}
/**
* Compare two semantic version strings
* Returns: -1 if v1 < v2, 0 if v1 === v2, 1 if v1 > v2
*/
private compareVersions(v1: string, v2: string): number {
if (v1 === v2) return 0;
const v1Parts = v1.split(".").map((n) => parseInt(n, 10) || 0);
const v2Parts = v2.split(".").map((n) => parseInt(n, 10) || 0);
const maxLength = Math.max(v1Parts.length, v2Parts.length);
for (let i = 0; i < maxLength; i++) {
const p1 = v1Parts[i] || 0;
const p2 = v2Parts[i] || 0;
if (p1 < p2) return -1;
if (p1 > p2) return 1;
}
return 0;
}
/**
* Check if user upgraded from 9.8.14 < version < 9.9.0 and show onboarding
* This ensures users who upgraded from intermediate versions see the setup guide
*/
private async checkMigrationAndMaybeShowOnboarding(): Promise<void> {
try {
// Get version info from VersionManager
const versionResult =
await this.versionManager.checkVersionChange();
const previousVersion = versionResult.versionInfo.previous;
// Get last version from changelog settings
const lastVersion = this.settings.changelog?.lastVersion || "";
// Get current version
const currentVersion = this.manifest?.version;
if (!currentVersion) {
return;
}
// Check if user upgraded from >9.8.14 and <9.9.0
// AND hasn't seen the 9.9.0 onboarding yet
if (
previousVersion &&
this.compareVersions(previousVersion, "9.8.14") > 0 &&
this.compareVersions(previousVersion, "9.9.0") < 0 &&
lastVersion !== "9.9.0"
) {
console.log(
`[Task Genius] Migration detected: ${previousVersion} -> ${currentVersion}, opening onboarding`,
);
// Directly open onboarding view (same pattern as maybeShowChangelog)
this.openOnboardingView();
// Mark as shown by updating lastVersion
this.settings.changelog.lastVersion = currentVersion;
await this.saveSettings();
}
} catch (error) {
console.error(
"[Task Genius] Failed to check migration onboarding:",
error,
);
}
}
private maybeShowChangelog(): void {
try {
if (!this.changelogManager) {
return;
}
const manifestVersion = this.manifest?.version;
if (!manifestVersion) {
return;
}
const changelogSettings = this.settings.changelog;
if (!changelogSettings?.enabled) {
return;
}
const lastVersion = changelogSettings.lastVersion || "";
if (manifestVersion === lastVersion) {
return;
}
const isBeta = manifestVersion.toLowerCase().includes("beta");
this.changelogManager.openChangelog(manifestVersion, isBeta);
} catch (error) {
console.error("[Task Genius] Failed to show changelog:", error);
}
}
async loadSettings() {
const savedData = await this.loadData();
this.settings = Object.assign({}, DEFAULT_SETTINGS, savedData);
this.settings.changelog = Object.assign(
{
enabled: true,
lastVersion: "",
},
this.settings.changelog ?? {},
);
try {
console.debug(
"[Plugin][loadSettings] fileMetadataInheritance (raw):",
savedData?.fileMetadataInheritance,
);
console.debug(
"[Plugin][loadSettings] fileMetadataInheritance (effective):",
this.settings.fileMetadataInheritance,
);
} catch {}
// Run migrations through the version-keyed registry. Phase 0 W1.
// The legacy bundle step wraps the prior migrateSettings + inheritance
// + fluent default-backfill paths under one atomic try/commit, so a
// throw in any of them leaves settings untouched. The bundle reads
// `savedData` to detect old projectConfig.metadataConfig.* keys that
// got dropped during the Object.assign with DEFAULT_SETTINGS — we
// stash it on the settings object briefly so the step can see it.
try {
(this.settings as any).__transient_savedData__ = savedData;
const registry = createMigrationRegistry();
const result = await registry.run(this.settings, {
toVersion: this.manifest.version,
});
if (!result.ok) {
console.error(
"[Task Genius] MigrationRegistry run failed:",
result.error,
);
// Fall back to the legacy direct calls so the user isn't left
// in a half-migrated state. Behavior is identical to before W1.
migrateSettings(this.settings);
this.migrateInheritanceSettings(savedData);
} else if (result.changed) {
console.log(
"[Task Genius] Migrations applied:",
Object.values(result.results).flatMap((r) => r.details),
);
}
} finally {
delete (this.settings as any).__transient_savedData__;
}
// Repair and validate status cycles (independent of migration registry —
// runs every load to clean up dynamically-corrupted state).
if (this.settings.statusCycles) {
this.settings.statusCycles = repairStatusCycles(
this.settings.statusCycles,
);
}
}
private migrateInheritanceSettings(savedData: any) {
// Check if old inheritance settings exist and new ones don't
if (
savedData?.projectConfig?.metadataConfig &&
!savedData?.fileMetadataInheritance
) {
const oldConfig = savedData.projectConfig.metadataConfig;
// Migrate to new structure
this.settings.fileMetadataInheritance = {
enabled: true,
inheritFromFrontmatter:
oldConfig.inheritFromFrontmatter ?? true,
inheritFromFrontmatterForSubtasks:
oldConfig.inheritFromFrontmatterForSubtasks ?? false,
};
// Remove old inheritance settings from project config
if (this.settings.projectConfig?.metadataConfig) {
delete (this.settings.projectConfig.metadataConfig as any)
.inheritFromFrontmatter;
delete (this.settings.projectConfig.metadataConfig as any)
.inheritFromFrontmatterForSubtasks;
}
// Save the migrated settings
this.saveSettings();
}
}
async saveSettings() {
try {
console.debug(
"[Plugin][saveSettings] fileMetadataInheritance:",
this.settings?.fileMetadataInheritance,
);
} catch {}
await this.saveData(this.settings);
}
async loadViews() {
const defaultViews = DEFAULT_SETTINGS.viewConfiguration;
// Ensure all default views exist in user settings
if (!this.settings.viewConfiguration) {
this.settings.viewConfiguration = [];
}
// Add any missing default views to user settings
defaultViews.forEach((defaultView) => {
const existingView = this.settings.viewConfiguration.find(
(v) => v.id === defaultView.id,
);
if (!existingView) {
this.settings.viewConfiguration.push({ ...defaultView });
}
});
await this.saveSettings();
}
// Helper method to set priority at cursor position
private isActivatingView = false;
async activateTaskView() {
// Prevent multiple simultaneous activations
if (this.isActivatingView) {
return;
}
this.isActivatingView = true;
try {
const { workspace } = this.app;
// Always use Fluent View as the default
const viewType = FLUENT_TASK_VIEW;
// Check if view is already open
const existingLeaves = workspace.getLeavesOfType(viewType);
if (existingLeaves.length > 0) {
// If view is already open, just reveal the first one
workspace.revealLeaf(existingLeaves[0]);
// Close any duplicate views
for (let i = 1; i < existingLeaves.length; i++) {
existingLeaves[i].detach();
}
return;
}
// Otherwise, create a new leaf and open the view
const leaf = workspace.getLeaf("tab");
await leaf.setViewState({ type: viewType });
await workspace.revealLeaf(leaf);
} finally {
this.isActivatingView = false;
}
}
private isActivatingSidebar = false;
async activateTimelineSidebarView() {
// Prevent multiple simultaneous activations
if (this.isActivatingSidebar) {
return;
}
this.isActivatingSidebar = true;
try {
const { workspace } = this.app;
// Check if view is already open
const existingLeaves = workspace.getLeavesOfType(
TIMELINE_SIDEBAR_VIEW_TYPE,
);
if (existingLeaves.length > 0) {
// If view is already open, just reveal the first one
workspace.revealLeaf(existingLeaves[0]);
// Close any duplicate views
for (let i = 1; i < existingLeaves.length; i++) {
existingLeaves[i].detach();
}
return;
}
// Open in the right sidebar
const leaf = workspace.getRightLeaf(false);
if (leaf) {
await leaf.setViewState({ type: TIMELINE_SIDEBAR_VIEW_TYPE });
workspace.revealLeaf(leaf);
}
} finally {
this.isActivatingSidebar = false;
}
}
async triggerViewUpdate() {
// Update Task Views
const taskViewLeaves =
this.app.workspace.getLeavesOfType(TASK_VIEW_TYPE);
if (taskViewLeaves.length > 0) {
for (const leaf of taskViewLeaves) {
if (leaf.view instanceof TaskView) {
// Avoid overwriting existing tasks with empty preloadedTasks during settings updates
if (
Array.isArray(this.preloadedTasks) &&
this.preloadedTasks.length > 0
) {
leaf.view.tasks = this.preloadedTasks;
}
leaf.view.triggerViewUpdate();
}
}
}
// Update Timeline Sidebar Views
const timelineViewLeaves = this.app.workspace.getLeavesOfType(
TIMELINE_SIDEBAR_VIEW_TYPE,
);
if (timelineViewLeaves.length > 0) {
for (const leaf of timelineViewLeaves) {
if (leaf.view instanceof TimelineSidebarView) {
await leaf.view.triggerViewUpdate();
}
}
}
}
/**
* Get the ICS manager instance
*/
getIcsManager(): IcsManager | undefined {
return this.icsManager;
}
/**
* Get the Calendar Auth Manager instance
*/
getAuthManager(): CalendarAuthManager | undefined {
return this.calendarAuthManager;
}
/**
* Initialize dataflow with version checking and rebuild handling
*/
private async initializeDataflowWithVersionCheck(): Promise<void> {
if (!this.dataflowOrchestrator) {
console.error("Dataflow orchestrator not available");
return;
}
try {
// Validate version storage integrity first
const diagnosticInfo =
await this.versionManager.getDiagnosticInfo();
if (!diagnosticInfo.canWrite) {
throw new Error(
"Cannot write to version storage - storage may be corrupted",
);
}
if (
!diagnosticInfo.versionValid &&
diagnosticInfo.previousVersion
) {
console.warn(
"Invalid version data detected, attempting recovery",
);
await this.versionManager.recoverFromCorruptedVersion();
}
// Check for version changes
const versionResult =
await this.versionManager.checkVersionChange();
if (versionResult.requiresRebuild) {
console.log(
`Task Genius (Dataflow): ${versionResult.rebuildReason}`,
);
// Get all supported files for progress tracking
const allFiles = this.app.vault
.getFiles()
.filter(
(file) =>
file.extension === "md" ||
file.extension === "canvas",
);
// Start rebuild progress tracking
this.rebuildProgressManager.startRebuild(
allFiles.length,
versionResult.rebuildReason,
);
// After dataflow rebuild, refresh habits to keep in sync
try {
await this.habitManager?.initializeHabits();
} catch (e) {
console.warn("Failed to refresh habits after rebuild", e);
}
// Trigger dataflow rebuild
await this.dataflowOrchestrator.rebuild();
// Get final task count from dataflow
const queryAPI = this.dataflowOrchestrator.getQueryAPI();
const allTasks = await queryAPI.getAllTasks();
const finalTaskCount = allTasks.length;
// Mark rebuild as complete
this.rebuildProgressManager.completeRebuild(finalTaskCount);
// Mark version as processed
await this.versionManager.markVersionProcessed();
} else {
// No rebuild needed, dataflow already initialized during creation
console.log(
"Task Genius (Dataflow): No rebuild needed, using existing cache",
);
}
} catch (error) {
console.error(
"Error during dataflow initialization with version check:",
error,
);
// Trigger emergency rebuild for dataflow
try {
const emergencyResult =
await this.versionManager.handleEmergencyRebuild(
`Dataflow initialization failed: ${error.message}`,
);
// Get all supported files for progress tracking
const allFiles = this.app.vault
.getFiles()
.filter(
(file) =>
file.extension === "md" ||
file.extension === "canvas",
);
// Start emergency rebuild
this.rebuildProgressManager.startRebuild(
allFiles.length,
emergencyResult.rebuildReason,
);
// Force rebuild dataflow
await this.dataflowOrchestrator.rebuild();
// Get final task count
const queryAPI = this.dataflowOrchestrator.getQueryAPI();
const allTasks = await queryAPI.getAllTasks();
const finalTaskCount = allTasks.length;
// Mark emergency rebuild as complete
this.rebuildProgressManager.completeRebuild(finalTaskCount);
// Store current version
await this.versionManager.markVersionProcessed();
console.log(
"Emergency dataflow rebuild completed successfully",
);
} catch (emergencyError) {
console.error(
"Emergency dataflow rebuild failed:",
emergencyError,
);
throw emergencyError;
}
}
}
/**
* Initialize task manager with version checking and rebuild handling
* @deprecated This method is no longer used as TaskManager has been removed
* This method is kept for reference only and will be removed in future versions
*/
private async initializeTaskManagerWithVersionCheck(): Promise<void> {
// This method is deprecated and should not be called
console.warn(
"initializeTaskManagerWithVersionCheck is deprecated and should not be used",
);
return Promise.resolve();
}
/**
* Open the settings modal
* @param tabId Optional tab ID to open directly
*/
openSettingsModal(tabId?: string): void {
const modal = new SettingsModal(this.app, this);
modal.open();
if (tabId) {
modal.openTab(tabId);
}
}
}