Skip to content

feat(scheduler): Loops and Schedules view + /loop and /schedule#975

Draft
doomspork wants to merge 2 commits into
mainfrom
doomspork/investigate-loops-scheduling
Draft

feat(scheduler): Loops and Schedules view + /loop and /schedule#975
doomspork wants to merge 2 commits into
mainfrom
doomspork/investigate-loops-scheduling

Conversation

@doomspork
Copy link
Copy Markdown
Member

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 (via ScheduleWakeup / CronCreate tool calls) or the claudette routine CLI, 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.

  • Clock icon next to the dashboard icon in the sidebar opens the Loops and Schedules view. It renders inside the content area (like the Dashboard), keeping the sidebar + PanelHeader + PanelToggles chrome in place. Click the clock again, the dashboard icon, or a workspace to leave it.
  • The view has two sections — Loops (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.
  • New scheduled task button opens a modal with One-shot (wakeup) or Recurring (cron) mode, a session picker defaulting to the active chat, datetime-local or cron-expression inputs, optional name, and a prompt.
  • /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 leading YYYY-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"]
Loading

Complexity Notes

  • Reuses main's schema, doesn't extend it. This was a deliberate pivot from an earlier version of this branch that introduced a separate scheduled_jobs table. As a result, a few semantics from the first design are intentionally not present because agent_scheduled_tasks can't express them: /loop has no iteration cap and no sub-minute intervals; there are no tri-state composer-toggle overrides; and /schedule targets an existing chat session rather than spinning up a fresh workspace per fire. The original implementation is preserved on the scheduler-original-impl tag for reference.
  • /loop interval → cron translation (intervalToCron in nativeSlashCommands.ts) only accepts intervals that evenly divide their unit; everything else falls through to the modal. Worth a sanity check on the whitelist.
  • Navigation coupling: selectWorkspace / selectRepository / goToDashboard in workspacesSlice now also clear schedulerOpen (with their no-op guards updated), and the clock button is a toggle. This is how you exit the view without a close button.
  • No backend Rust changes beyond two slash_commands.rs registry entries — the scheduler service, DB layer, and Tauri commands are all main's.

Test Steps

  1. cd src/ui && bun install && bunx tsc -b && bun run test && bun run lint:css && bun run build — all green.
  2. 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.
  3. Run the app (./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.
  4. In a chat session, run /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).
  5. Run /schedule 2026-06-01T09:00 cut the release → a wakeup appears under Schedules with the fire time + countdown. Run /schedule with no args → modal opens.
  6. Click New scheduled task, create both a one-shot and a recurring task; confirm they appear on the page and under Settings → Automation. Use Run now and Delete on a row.
  7. Confirm a created task fires through the normal chat pipeline at its scheduled time (or via Run now).

Checklist

  • Tests added/updated (native slash-command test fixture extended for the new context fields; existing scheduling tests on main continue to pass)
  • Documentation updated — site/.../features/agent-scheduling.mdx (new "Loops and Schedules view" + slash-command sections) and slash-commands.mdx rows for /loop and /schedule

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.
Copilot AI review requested due to automatic review settings May 21, 2026 19:20
@jamesbrink
Copy link
Copy Markdown
Member

finally

@codecov
Copy link
Copy Markdown

codecov Bot commented May 21, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 80.52%. Comparing base (17a9683) to head (d3d3438).

Additional details and impacted files

Impacted file tree graph

@@           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:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 scheduler i18n namespace + locale resources and a new Zustand schedulingSlice to 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.selectedWorkspaceId but schedulerOpen is true, this path re-calls notifyBackendSelection(id) and prewarmWorkspaceSelection(...) 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 thread src/ui/src/i18n.ts
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,
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 31 out of 32 changed files in this pull request and generated 13 comments.

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants