From e13b4526c60c588f1c7b824290819c8fad58f92d Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Sun, 9 Aug 2026 21:43:16 +0300 Subject: [PATCH] feat(tasks): manage markdown loops from scheduled tasks --- .../docs/content/docs/scheduled-tasks.mdx | 2 +- .../session/ScheduledTasksDialog.tsx | 62 +++++--- .../ui/src/components/views/FilesView.tsx | 12 ++ packages/ui/src/lib/i18n/messages/de.ts | 1 + packages/ui/src/lib/i18n/messages/en.ts | 1 + packages/ui/src/lib/i18n/messages/es.ts | 1 + packages/ui/src/lib/i18n/messages/fr.ts | 1 + packages/ui/src/lib/i18n/messages/ja.ts | 1 + packages/ui/src/lib/i18n/messages/ko.ts | 1 + packages/ui/src/lib/i18n/messages/pl.ts | 1 + packages/ui/src/lib/i18n/messages/pt-BR.ts | 1 + packages/ui/src/lib/i18n/messages/uk.ts | 1 + packages/ui/src/lib/i18n/messages/zh-CN.ts | 1 + packages/ui/src/lib/i18n/messages/zh-TW.ts | 1 + packages/ui/src/lib/scheduledTasksApi.ts | 31 ++++ .../lib/scheduled-tasks/DOCUMENTATION.md | 17 ++- .../web/server/lib/scheduled-tasks/loops.js | 11 +- .../web/server/lib/scheduled-tasks/routes.js | 29 ++++ .../web/server/lib/scheduled-tasks/service.js | 44 ++++++ .../lib/scheduled-tasks/service.test.js | 137 +++++++++++++++++- 20 files changed, 322 insertions(+), 34 deletions(-) diff --git a/packages/docs/content/docs/scheduled-tasks.mdx b/packages/docs/content/docs/scheduled-tasks.mdx index ed2fe202..358fbb3e 100644 --- a/packages/docs/content/docs/scheduled-tasks.mdx +++ b/packages/docs/content/docs/scheduled-tasks.mdx @@ -60,7 +60,7 @@ If a project loop and a user loop share a name, the project loop wins. ### How loops behave -- The **file is authoritative** while it exists: edits made in the UI are reverted on the next sync. The scheduled-tasks dialog marks loop tasks and disables their edit/enable/delete actions — **run now** still works. To stop a loop, delete the file (or set `enabled: false`). +- The **file is authoritative** while it exists. **Edit** opens it in the built-in file editor, the enabled toggle updates its frontmatter, and deleting the task deletes the markdown file after confirmation. **Run now** remains available. - Runtime state (last run, next run, status) lives in the project config and is never written back into the markdown file. - Renaming the `name` field renames the task in place. If a loop file temporarily fails to parse (mid-edit, merge conflict), its task is kept with the last good definition until the file is fixed. - `daily`/`weekly`/`once` schedules and goal settings remain UI-only; loop files are always cron. diff --git a/packages/ui/src/components/session/ScheduledTasksDialog.tsx b/packages/ui/src/components/session/ScheduledTasksDialog.tsx index 6590e67b..a186b742 100644 --- a/packages/ui/src/components/session/ScheduledTasksDialog.tsx +++ b/packages/ui/src/components/session/ScheduledTasksDialog.tsx @@ -21,14 +21,17 @@ import { useI18n } from '@/lib/i18n'; import type { ProjectEntry } from '@/lib/api/types'; import { deleteScheduledTask, + deleteScheduledTaskLoopFile, fetchScheduledTasks, runScheduledTaskNow, + setLoopScheduledTaskEnabled, upsertScheduledTask, type ScheduledTask, type ScheduledTaskStatus, } from '@/lib/scheduledTasksApi'; import { ScheduledTaskEditorDialog } from './ScheduledTaskEditorDialog'; import { canonicalizeTimezone } from '@/lib/timezones'; +import { useFilesViewTabsStore } from '@/stores/useFilesViewTabsStore'; const scheduleTimes = (task: ScheduledTask): string[] => { const raw = Array.isArray(task.schedule.times) @@ -314,10 +317,11 @@ export function ScheduledTasksDialog() { setMutatingTaskID(task.id); setTasks((prev) => prev.map((item) => (item.id === task.id ? { ...item, enabled } : item))); try { - await upsertScheduledTask(selectedProjectID, { - ...task, - enabled, - }); + if (task.loopFile) { + await setLoopScheduledTaskEnabled(selectedProjectID, task.id, enabled); + } else { + await upsertScheduledTask(selectedProjectID, { ...task, enabled }); + } await reloadTasks(selectedProjectID, { silent: true }); } catch (error) { toast.error(error instanceof Error ? error.message : t('sessions.scheduledTasks.dialog.toast.updateFailed')); @@ -331,14 +335,20 @@ export function ScheduledTasksDialog() { if (!selectedProjectID) { return; } - const confirmed = window.confirm(t('sessions.scheduledTasks.dialog.confirm.deleteTask', { taskName: task.name })); + const confirmed = window.confirm(task.loopFile + ? t('sessions.scheduledTasks.dialog.confirm.deleteLoopFile', { taskName: task.name }) + : t('sessions.scheduledTasks.dialog.confirm.deleteTask', { taskName: task.name })); if (!confirmed) { return; } setMutatingTaskID(task.id); try { - await deleteScheduledTask(selectedProjectID, task.id); + if (task.loopFile) { + await deleteScheduledTaskLoopFile(selectedProjectID, task.id); + } else { + await deleteScheduledTask(selectedProjectID, task.id); + } await reloadTasks(selectedProjectID, { silent: true }); toast.success(t('sessions.scheduledTasks.dialog.toast.deleted')); } catch (error) { @@ -348,6 +358,24 @@ export function ScheduledTasksDialog() { } }, [selectedProjectID, reloadTasks, t]); + const handleEditTask = React.useCallback((task: ScheduledTask) => { + if (!task.loopFile) { + setEditorTask(task); + setEditorOpen(true); + return; + } + if (!selectedProject?.path) { + return; + } + setOpen(false); + if (isMobile) { + useFilesViewTabsStore.getState().setSelectedPath(selectedProject.path, task.loopFile, { allowOutsideRoot: true }); + useUIStore.getState().setActiveMainTab('files'); + return; + } + useUIStore.getState().openContextFile(selectedProject.path, task.loopFile); + }, [isMobile, selectedProject?.path, setOpen]); + const handleRunNow = React.useCallback(async (task: ScheduledTask) => { if (!selectedProjectID) { return; @@ -533,11 +561,8 @@ export function ScheduledTasksDialog() { className={cn( 'inline-flex cursor-pointer items-center gap-2 typography-micro font-medium', task.enabled ? 'text-foreground' : 'text-muted-foreground', - (isBusy || task.loopFile) && 'cursor-not-allowed opacity-50', + isBusy && 'cursor-not-allowed opacity-50', )} - title={task.loopFile - ? t('sessions.scheduledTasks.dialog.loopFile.toggleDisabled') - : undefined} > {task.enabled ? t('sessions.scheduledTasks.dialog.taskToggle.enabled') : t('sessions.scheduledTasks.dialog.taskToggle.paused')} @@ -562,14 +587,8 @@ export function ScheduledTasksDialog() {