feat(scheduler): Loops and Schedules view + /loop and /schedule#975
Draft
doomspork wants to merge 2 commits into
Draft
feat(scheduler): Loops and Schedules view + /loop and /schedule#975doomspork wants to merge 2 commits into
doomspork wants to merge 2 commits into
Conversation
Add a user-facing UI on top of the existing agent-scheduling backend (`agent_scheduled_tasks`). Previously routines were only creatable via agent tool calls or the `claudette routine` CLI; this exposes them in the GUI without inventing a parallel schema. - Clock icon next to the dashboard icon in the sidebar opens the "Loops and Schedules" view (renders inside the content area, keeps the sidebar + PanelHeader + PanelToggles in place). Two sections: Loops (kind=cron) and Schedules (kind=wakeup), with Run-now and Delete actions sharing main's existing API. - "New scheduled task" button opens a modal with One-shot (wakeup) or Recurring (cron) mode, a session picker that defaults to the active chat, datetime-local or cron-expression inputs, optional name, and a prompt textarea. - `/loop <interval> <prompt>` translates whole-minute intervals that evenly tile their unit (1m/2m/5m/10m/15m/20m/30m, 1h/2h/3h/4h/6h/8h/12h, 1d) to a 5-field cron expression and creates a recurring routine in the current session. Sub-minute or odd intervals open the modal so the user can write the cron expression directly. - `/schedule [<when>] [<prompt>]` fires a wakeup inline when both a leading `YYYY-MM-DD[THH:MM]` timestamp and a prompt are present; otherwise opens the modal with whatever was provided. The same task list also drives Settings → Automation, so creating from the page or a slash command shows up in both places.
Member
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #975 +/- ##
=======================================
Coverage 80.51% 80.52%
=======================================
Files 122 122
Lines 45322 45338 +16
=======================================
+ Hits 36491 36507 +16
Misses 8831 8831 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
Adds a first-class “Loops and Schedules” UI surface (plus /loop and /schedule native slash commands) on top of the existing scheduled-task backend (agent_scheduled_tasks), wiring it into the app layout/sidebar, creating a task-creation modal, and documenting the feature.
Changes:
- Introduces a dedicated scheduler page (sidebar clock icon) and a “New scheduled task” modal for wakeups + cron routines.
- Adds
/loop(interval→cron) and/schedule(timestamp parsing / modal fallback) native slash commands and registers them in the Rust slash command registry. - Adds a new
scheduleri18n namespace + locale resources and a new ZustandschedulingSliceto load/store scheduled tasks.
Reviewed changes
Copilot reviewed 31 out of 32 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| src/ui/src/types/i18next.d.ts | Adds scheduler namespace typing for i18next. |
| src/ui/src/stores/useAppStore.ts | Registers the new schedulingSlice in the app store. |
| src/ui/src/stores/slices/workspacesSlice.ts | Clears schedulerOpen during navigation and updates no-op guards. |
| src/ui/src/stores/slices/uiSlice.ts | Adds scheduler page state + openScheduler/closeScheduler. |
| src/ui/src/stores/slices/schedulingSlice.ts | New slice to load and store scheduled tasks list. |
| src/ui/src/services/tauri/scheduling.ts | Adds Tauri service wrappers for schedule_wakeup and create_cron_routine. |
| src/ui/src/locales/en/sidebar.json | Adds sidebar label for scheduler entry. |
| src/ui/src/locales/es/sidebar.json | Adds sidebar label for scheduler entry. |
| src/ui/src/locales/pt-BR/sidebar.json | Adds sidebar label for scheduler entry. |
| src/ui/src/locales/ja/sidebar.json | Adds sidebar label for scheduler entry. |
| src/ui/src/locales/zh-CN/sidebar.json | Adds sidebar label for scheduler entry. |
| src/ui/src/locales/en/scheduler.json | New scheduler namespace strings (English). |
| src/ui/src/locales/es/scheduler.json | New scheduler namespace strings (currently English content). |
| src/ui/src/locales/pt-BR/scheduler.json | New scheduler namespace strings (currently English content). |
| src/ui/src/locales/ja/scheduler.json | New scheduler namespace strings (currently English content). |
| src/ui/src/locales/zh-CN/scheduler.json | New scheduler namespace strings (currently English content). |
| src/ui/src/i18n.ts | Registers scheduler namespace and resources for all supported languages. |
| src/ui/src/components/sidebar/Sidebar.tsx | Adds clock icon toggle to open/close scheduler view. |
| src/ui/src/components/sidebar/Sidebar.module.css | Adds an active style variant for the dashboard/scheduler header buttons. |
| src/ui/src/components/scheduler/SchedulerPage.tsx | New Loops/Schedules page: list, refresh, run-now, delete, countdown ticker. |
| src/ui/src/components/scheduler/SchedulerPage.module.css | Styling for the new scheduler page. |
| src/ui/src/components/modals/ModalRouter.tsx | Routes the new createScheduledTask modal. |
| src/ui/src/components/modals/CreateScheduledTaskModal.tsx | New modal to create wakeups or cron routines. |
| src/ui/src/components/modals/CreateScheduledTaskModal.module.css | Styling for the new modal. |
| src/ui/src/components/layout/AppLayout.tsx | Renders scheduler page when schedulerOpen and hides workspace-only panes. |
| src/ui/src/components/chat/nativeSlashCommands.ts | Adds /loop and /schedule handlers + interval→cron helper. |
| src/ui/src/components/chat/nativeSlashCommands.test.ts | Extends native command context fixture with scheduler fields. |
| src/ui/src/components/chat/ChatPanel.tsx | Passes sessionId and openScheduler into native slash command context. |
| src/ui/src/App.tsx | Loads scheduled tasks list during app initialization. |
| src/slash_commands.rs | Registers /loop and /schedule in the native command registry. |
| site/src/content/docs/features/slash-commands.mdx | Documents /loop and /schedule. |
| site/src/content/docs/features/agent-scheduling.mdx | Adds “Loops and Schedules view” docs and creation/slash-command guidance. |
Comments suppressed due to low confidence (1)
src/ui/src/stores/slices/workspacesSlice.ts:619
- When
id === s.selectedWorkspaceIdbutschedulerOpenis true, this path re-callsnotifyBackendSelection(id)andprewarmWorkspaceSelection(...)even though the workspace selection didn’t actually change (the user is just exiting the scheduler view). Consider handling this case by returning a minimal state update (e.g.{ schedulerOpen: false }) to avoid redundant backend notifications and prewarm work.
selectWorkspace: (id) =>
set((s) => {
if (id === s.selectedWorkspaceId && !s.schedulerOpen) return s;
notifyBackendSelection(id);
prewarmWorkspaceSelection(
id,
s.fileTreeRefreshNonceByWorkspace,
s.pendingCreates,
s.pendingForks,
);
Comment on lines
+308
to
+309
| const wantsModal = | ||
| !!prefill && (!!prefill.prompt || !!prefill.fireAt || !!prefill.cronExpr); |
Comment on lines
+833
to
+836
| const firstTok = trimmed.split(/\s+/, 1)[0] ?? ""; | ||
| if (SCHEDULE_TIME_RE.test(firstTok)) { | ||
| const parsed = new Date(firstTok); | ||
| const rest = trimmed.slice(firstTok.length).trim(); |
Comment on lines
+22
to
+27
| function formatCountdown(iso: string | null, now: number): string { | ||
| if (!iso) return "—"; | ||
| const target = new Date(iso).getTime(); | ||
| if (Number.isNaN(target)) return iso; | ||
| let delta = Math.round((target - now) / 1000); | ||
| if (delta <= 0) return "due now"; |
| setError(null); | ||
| try { | ||
| await deleteScheduledRoutine(id); | ||
| setScheduledTasks(scheduledTasks.filter((task) => task.id !== id)); |
Comment on lines
+214
to
+230
| ? `· ${t("last_fired", { time: formatLocalDateTime(task.last_fired_at) ?? "" })}` | ||
| : ""; | ||
| const mode = !isWakeup | ||
| ? task.recurring | ||
| ? t("mode_recurring") | ||
| : t("mode_one_shot") | ||
| : null; | ||
| const isBusy = busyId === task.id; | ||
|
|
||
| return ( | ||
| <li className={`${styles.row} ${task.enabled ? "" : styles.rowMuted}`}> | ||
| <div className={styles.rowMain}> | ||
| <div className={styles.rowTitle}>{task.name || task.prompt}</div> | ||
| <div className={styles.rowMeta}> | ||
| {schedule} | ||
| {mode ? ` · ${mode}` : ""} · {next} {last} · {workspaceLabel(task.workspace_id)} | ||
| </div> |
Comment on lines
+5
to
+9
| export interface SchedulingSlice { | ||
| /** All scheduled tasks (wakeups + cron routines), source-of-truth list | ||
| * shared with the Settings → Automation panel. */ | ||
| scheduledTasks: ScheduledTask[]; | ||
| setScheduledTasks: (tasks: ScheduledTask[]) => void; |
Comment on lines
+29
to
+32
| import schedulerEs from "./locales/es/scheduler.json"; | ||
| import schedulerPtBr from "./locales/pt-BR/scheduler.json"; | ||
| import schedulerJa from "./locales/ja/scheduler.json"; | ||
| import schedulerZhCn from "./locales/zh-CN/scheduler.json"; |
Comment on lines
1062
to
+1065
| helpHandler, | ||
| initHandler, | ||
| loopHandler, | ||
| scheduleHandler, |
10 tasks
Comment on lines
+307
to
+316
| openScheduler: (prefill) => { | ||
| const wantsModal = | ||
| !!prefill && (!!prefill.prompt || !!prefill.fireAt || !!prefill.cronExpr); | ||
| set({ | ||
| schedulerOpen: true, | ||
| settingsOpen: false, | ||
| ...(wantsModal | ||
| ? { activeModal: "createScheduledTask", modalData: { ...prefill } } | ||
| : {}), | ||
| }); |
Comment on lines
+753
to
+757
| if (unit === "m") { | ||
| if (n >= 60) return null; | ||
| if (60 % n !== 0) return null; // odd intervals don't tile the hour | ||
| return `*/${n} * * * *`; | ||
| } |
Comment on lines
+786
to
+794
| const cronExpr = intervalToCron(tokens[0]); | ||
| const prompt = tokens.slice(1).join(" ").trim(); | ||
| if (!cronExpr) { | ||
| ctx.openScheduler({ | ||
| sessionId: ctx.sessionId, | ||
| prompt: prompt || undefined, | ||
| }); | ||
| ctx.addLocalMessage(LOOP_USAGE); | ||
| return handled; |
Comment on lines
+818
to
+819
| * starts with a date for an explicit schedule time. */ | ||
| const SCHEDULE_TIME_RE = /^\d{4}-\d{2}-\d{2}([T ]\d{2}:\d{2}(:\d{2})?)?$/; |
Comment on lines
+833
to
+836
| const firstTok = trimmed.split(/\s+/, 1)[0] ?? ""; | ||
| if (SCHEDULE_TIME_RE.test(firstTok)) { | ||
| const parsed = new Date(firstTok); | ||
| const rest = trimmed.slice(firstTok.length).trim(); |
| setError(null); | ||
| try { | ||
| await deleteScheduledRoutine(id); | ||
| setScheduledTasks(scheduledTasks.filter((task) => task.id !== id)); |
Comment on lines
+2
to
+38
| "title": "Loops and Schedules", | ||
| "new_btn": "New scheduled task", | ||
| "refresh": "Refresh", | ||
| "loops_heading": "Loops", | ||
| "loops_empty": "No cron routines scheduled. Use /loop <interval> <prompt> in a chat, or click \"New scheduled task\".", | ||
| "schedules_heading": "Schedules", | ||
| "schedules_empty": "No one-shot wakeups scheduled.", | ||
| "run_now": "Run now", | ||
| "delete": "Delete", | ||
| "next_in": "next in {{time}}", | ||
| "last_fired": "last fired {{time}}", | ||
| "not_scheduled": "not scheduled", | ||
| "fire_at_unknown": "no fire time", | ||
| "schedule_unknown": "no schedule", | ||
| "mode_recurring": "recurring", | ||
| "mode_one_shot": "one-shot", | ||
| "modal_title": "New scheduled task", | ||
| "type_label": "Type", | ||
| "type_wakeup": "One-shot (wakeup)", | ||
| "type_cron": "Recurring (cron)", | ||
| "session_label": "Chat session", | ||
| "session_current": "Current session", | ||
| "session_pick": "Pick a session…", | ||
| "session_none_available": "No chat sessions available", | ||
| "fire_at_label": "Fire at", | ||
| "cron_expr_label": "Cron expression", | ||
| "cron_expr_hint": "Standard 5-field cron in local time (e.g. `0 9 * * 1-5`, `*/15 * * * *`).", | ||
| "name_label": "Name (optional)", | ||
| "recurring_label": "Recurring (runs every time the cron matches)", | ||
| "prompt_label": "Prompt", | ||
| "prompt_placeholder": "What should the agent do when this fires?", | ||
| "creating": "Scheduling…", | ||
| "create_btn": "Schedule", | ||
| "error_pick_session": "Pick a chat session.", | ||
| "error_empty_prompt": "Enter a prompt.", | ||
| "error_bad_time": "Enter a valid date and time.", | ||
| "error_bad_cron": "Enter a valid cron expression (5 fields)." |
Comment on lines
+2
to
+38
| "title": "Loops and Schedules", | ||
| "new_btn": "New scheduled task", | ||
| "refresh": "Refresh", | ||
| "loops_heading": "Loops", | ||
| "loops_empty": "No cron routines scheduled. Use /loop <interval> <prompt> in a chat, or click \"New scheduled task\".", | ||
| "schedules_heading": "Schedules", | ||
| "schedules_empty": "No one-shot wakeups scheduled.", | ||
| "run_now": "Run now", | ||
| "delete": "Delete", | ||
| "next_in": "next in {{time}}", | ||
| "last_fired": "last fired {{time}}", | ||
| "not_scheduled": "not scheduled", | ||
| "fire_at_unknown": "no fire time", | ||
| "schedule_unknown": "no schedule", | ||
| "mode_recurring": "recurring", | ||
| "mode_one_shot": "one-shot", | ||
| "modal_title": "New scheduled task", | ||
| "type_label": "Type", | ||
| "type_wakeup": "One-shot (wakeup)", | ||
| "type_cron": "Recurring (cron)", | ||
| "session_label": "Chat session", | ||
| "session_current": "Current session", | ||
| "session_pick": "Pick a session…", | ||
| "session_none_available": "No chat sessions available", | ||
| "fire_at_label": "Fire at", | ||
| "cron_expr_label": "Cron expression", | ||
| "cron_expr_hint": "Standard 5-field cron in local time (e.g. `0 9 * * 1-5`, `*/15 * * * *`).", | ||
| "name_label": "Name (optional)", | ||
| "recurring_label": "Recurring (runs every time the cron matches)", | ||
| "prompt_label": "Prompt", | ||
| "prompt_placeholder": "What should the agent do when this fires?", | ||
| "creating": "Scheduling…", | ||
| "create_btn": "Schedule", | ||
| "error_pick_session": "Pick a chat session.", | ||
| "error_empty_prompt": "Enter a prompt.", | ||
| "error_bad_time": "Enter a valid date and time.", | ||
| "error_bad_cron": "Enter a valid cron expression (5 fields)." |
Comment on lines
+2
to
+38
| "title": "Loops and Schedules", | ||
| "new_btn": "New scheduled task", | ||
| "refresh": "Refresh", | ||
| "loops_heading": "Loops", | ||
| "loops_empty": "No cron routines scheduled. Use /loop <interval> <prompt> in a chat, or click \"New scheduled task\".", | ||
| "schedules_heading": "Schedules", | ||
| "schedules_empty": "No one-shot wakeups scheduled.", | ||
| "run_now": "Run now", | ||
| "delete": "Delete", | ||
| "next_in": "next in {{time}}", | ||
| "last_fired": "last fired {{time}}", | ||
| "not_scheduled": "not scheduled", | ||
| "fire_at_unknown": "no fire time", | ||
| "schedule_unknown": "no schedule", | ||
| "mode_recurring": "recurring", | ||
| "mode_one_shot": "one-shot", | ||
| "modal_title": "New scheduled task", | ||
| "type_label": "Type", | ||
| "type_wakeup": "One-shot (wakeup)", | ||
| "type_cron": "Recurring (cron)", | ||
| "session_label": "Chat session", | ||
| "session_current": "Current session", | ||
| "session_pick": "Pick a session…", | ||
| "session_none_available": "No chat sessions available", | ||
| "fire_at_label": "Fire at", | ||
| "cron_expr_label": "Cron expression", | ||
| "cron_expr_hint": "Standard 5-field cron in local time (e.g. `0 9 * * 1-5`, `*/15 * * * *`).", | ||
| "name_label": "Name (optional)", | ||
| "recurring_label": "Recurring (runs every time the cron matches)", | ||
| "prompt_label": "Prompt", | ||
| "prompt_placeholder": "What should the agent do when this fires?", | ||
| "creating": "Scheduling…", | ||
| "create_btn": "Schedule", | ||
| "error_pick_session": "Pick a chat session.", | ||
| "error_empty_prompt": "Enter a prompt.", | ||
| "error_bad_time": "Enter a valid date and time.", | ||
| "error_bad_cron": "Enter a valid cron expression (5 fields)." |
Comment on lines
+2
to
+38
| "title": "Loops and Schedules", | ||
| "new_btn": "New scheduled task", | ||
| "refresh": "Refresh", | ||
| "loops_heading": "Loops", | ||
| "loops_empty": "No cron routines scheduled. Use /loop <interval> <prompt> in a chat, or click \"New scheduled task\".", | ||
| "schedules_heading": "Schedules", | ||
| "schedules_empty": "No one-shot wakeups scheduled.", | ||
| "run_now": "Run now", | ||
| "delete": "Delete", | ||
| "next_in": "next in {{time}}", | ||
| "last_fired": "last fired {{time}}", | ||
| "not_scheduled": "not scheduled", | ||
| "fire_at_unknown": "no fire time", | ||
| "schedule_unknown": "no schedule", | ||
| "mode_recurring": "recurring", | ||
| "mode_one_shot": "one-shot", | ||
| "modal_title": "New scheduled task", | ||
| "type_label": "Type", | ||
| "type_wakeup": "One-shot (wakeup)", | ||
| "type_cron": "Recurring (cron)", | ||
| "session_label": "Chat session", | ||
| "session_current": "Current session", | ||
| "session_pick": "Pick a session…", | ||
| "session_none_available": "No chat sessions available", | ||
| "fire_at_label": "Fire at", | ||
| "cron_expr_label": "Cron expression", | ||
| "cron_expr_hint": "Standard 5-field cron in local time (e.g. `0 9 * * 1-5`, `*/15 * * * *`).", | ||
| "name_label": "Name (optional)", | ||
| "recurring_label": "Recurring (runs every time the cron matches)", | ||
| "prompt_label": "Prompt", | ||
| "prompt_placeholder": "What should the agent do when this fires?", | ||
| "creating": "Scheduling…", | ||
| "create_btn": "Schedule", | ||
| "error_pick_session": "Pick a chat session.", | ||
| "error_empty_prompt": "Enter a prompt.", | ||
| "error_bad_time": "Enter a valid date and time.", | ||
| "error_bad_cron": "Enter a valid cron expression (5 fields)." |
* feat(scheduler): expose scheduling tools to Codex and Pi agents The ScheduleWakeup / CronCreate / CronList / CronDelete tools previously reached only Claude agents, and only while the send_to_user "Agent Attachments" plugin was enabled — disabling that plugin silently killed agent scheduling. - Pi sidecar: register the four scheduling tools as native customTools. Pi has no MCP bridge, so each tool round-trips a generic host_tool message through the new PiSdkSession sink (a ChatBridgeSink), reusing the existing BridgePayload + agent_mcp_sink DB path — no new DB code. The host_tool channel is deliberately generic so a future user Pi extension needing host services can reuse it. - Codex + Claude: inject the Claudette MCP server unconditionally. It carries always-on scheduling tools, so the send_to_user toggle now gates only that tool's prompt nudge, not the whole server. - Pi system prompt: add PI_SCHEDULING_NUDGE (MCP-free wording) so Pi models know the native scheduling tools exist. * fix(scheduler): preserve Agent Attachments toggle and extend decoupling to Remote Control Codex peer-review findings on 6b13daf: 1. Unconditional MCP injection (the fix that gives Claude/Codex always-on scheduling) re-exposed the `send_to_user` tool when the Agent Attachments plugin is disabled, breaking the Settings contract that turning it off removes the tool. Gate the call inside the sink — at the point that actually does the delivery — instead of trying to manipulate `tools/list` in the out-of-process MCP grandchild. Returns a clear error so the agent can adjust. 2. `chat/remote_control.rs` had the same `if send_to_user_enabled { … } else { start_chat_bridge }` ternary as the spawn / respawn paths and was missed in 6b13daf. Without this, Remote Control sessions still lose scheduling when Agent Attachments is off — contradicting the doc claim that scheduling stays available regardless. Same unconditional injection now applies here too. * fix(scheduler): pin backend per task and persist schedule confirmation Two bugs surfaced after the agent-callable rollout: 1. `/loop` (and `/schedule`) created from a Codex or Pi chat fired on Claude when its turn came due. `dispatch_prompt_to_session` was calling `send_chat_message` with `backend_id: None`, and the resolver falls back to the `default_agent_backend` app setting (defaults to "anthropic") — not the user's toolbar choice. So the first cron fire in a fresh Codex chat looked like nothing fired at all. 2. The "Looping (`*/1 * * * *`)" / "Scheduled for …" confirmation was posted via `addLocalMessage({persisted:false})`, so it vanished on reload and the user had no chat-side evidence the schedule existed. Fix: - New migration adds nullable `backend_id` + `model` columns to `agent_scheduled_tasks`. Pre-migration rows and agent-callable scheduling stay `NULL` and keep the prior global-default behavior. - `/loop` and `/schedule` capture the toolbar's `selectedModelProvider` + `selectedModel` and persist them on the row. `dispatch_task_prompt` forwards both into `send_chat_message`. - The `schedule_wakeup` / `create_cron_routine` Tauri commands now write a System `ChatMessage` on success and emit `chat-system-message`, which the existing `useAgentStream` listener appends to chat state. One persisted note for all 3 backends; frontend `addLocalMessage` is reserved for failure messages only. * fix(scheduler): address Copilot review on #980 Three findings, all valid: - `post_creation_note` was writing `Utc::now().to_rfc3339()` while every other chat-message site uses `now_iso()` (Unix-seconds string). Mixing formats breaks the frontend's string-sort over `created_at`. - Stale comment above the unconditional MCP-bridge injection in `send_chat_message`'s spawn block claimed injection was gated on the `send_to_user` toggle. - `SYSTEM_PROMPT_NUDGE` bundled the scheduling/Monitor guidance with the `send_to_user` guidance, so disabling Agent Attachments stripped both — partially re-coupling scheduling discoverability to that toggle. Split into `MCP_SCHEDULING_NUDGE` (always on, mirroring the unconditional bridge) and `SEND_TO_USER_NUDGE` (toggle-gated), with a new `mcp_system_prompt_nudge()` helper composing them. Both call sites (`send.rs` and `remote_control.rs`) now use the helper; two new tests pin the toggle behavior.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Summary
Adds a user-facing UI on top of the existing agent-scheduling backend (
agent_scheduled_tasks). Until now, scheduled wakeups and cron routines could only be created by the agent (viaScheduleWakeup/CronCreatetool calls) or theclaudette routineCLI, and the only GUI surface was a read-only list under Settings → Automation. This branch makes them first-class in the app: a dedicated sidebar-accessible view, a creation dialog, and two slash commands — all reusing the same Tauri commands, DB table, and Rust scheduler that already shipped, so there is no parallel schema.PanelHeader+PanelToggleschrome in place. Click the clock again, the dashboard icon, or a workspace to leave it.kind: cron) and Schedules (kind: wakeup) — each row showing the human-readable schedule / fire time, target session, next-fire countdown, with Run now and Delete actions./loop <interval> <prompt>translates whole-minute intervals that evenly tile their unit (1m/2m/5m/10m/15m/20m/30m, 1h/2h/3h/4h/6h/8h/12h, 1d) into a 5-field cron expression and creates a recurring routine in the current session. Sub-minute or odd intervals open the modal so the user can write the cron expression directly./schedule [<when>] [<prompt>]fires a wakeup inline when a leadingYYYY-MM-DD[THH:MM]timestamp and a prompt are both present; otherwise opens the modal prefilled with whatever was provided.The list is shared (via a new
schedulingSlice) with Settings → Automation, so tasks created from the page or a slash command appear in both places.flowchart LR subgraph Create A["/loop, /schedule\n(nativeSlashCommands)"] B["New scheduled task modal"] end A -->|"scheduleWakeup /\ncreateCronRoutine"| C["Tauri commands\n(main's commands::scheduling)"] B -->|"scheduleWakeup /\ncreateCronRoutine"| C C --> D[("agent_scheduled_tasks")] D --> E["list_scheduled_routines"] E --> F["schedulingSlice.scheduledTasks"] F --> G["Loops and Schedules page"] F --> H["Settings -> Automation"] D --> I["Rust scheduler (start_scheduler)\nfires via send_chat_message"]Complexity Notes
scheduled_jobstable. As a result, a few semantics from the first design are intentionally not present becauseagent_scheduled_taskscan't express them:/loophas no iteration cap and no sub-minute intervals; there are no tri-state composer-toggle overrides; and/scheduletargets an existing chat session rather than spinning up a fresh workspace per fire. The original implementation is preserved on thescheduler-original-impltag for reference./loopinterval → cron translation (intervalToCroninnativeSlashCommands.ts) only accepts intervals that evenly divide their unit; everything else falls through to the modal. Worth a sanity check on the whitelist.selectWorkspace/selectRepository/goToDashboardinworkspacesSlicenow also clearschedulerOpen(with their no-op guards updated), and the clock button is a toggle. This is how you exit the view without a close button.slash_commands.rsregistry entries — the scheduler service, DB layer, and Tauri commands are all main's.Test Steps
cd src/ui && bun install && bunx tsc -b && bun run test && bun run lint:css && bun run build— all green.cargo fmt --all --check && cargo clippy -p claudette -p claudette-server -p claudette-cli --all-targets --all-features && cargo test -p claudette --all-features— all green../scripts/dev.sh). Click the clock icon next to the dashboard icon — the Loops and Schedules view opens with the sidebar + header still present. Click it again to toggle back./loop 5m say hi→ a confirmation message appears; the loop shows under Loops with a human schedule ("every 5 minutes") and a next-fire countdown. Run/loop 37s x→ the New scheduled task modal opens instead (odd interval)./schedule 2026-06-01T09:00 cut the release→ a wakeup appears under Schedules with the fire time + countdown. Run/schedulewith no args → modal opens.Checklist
site/.../features/agent-scheduling.mdx(new "Loops and Schedules view" + slash-command sections) andslash-commands.mdxrows for/loopand/schedule