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() {