Add a Schedules screen to manage recurring agents (#1246)

* feat: add Schedules screen to manage scheduled agents

Adds a Schedules section in the sidebar header (below Sessions) for viewing
and managing new-agent schedules: recurring jobs that spawn an agent on a
cron or interval cadence.

- Borderless table showing provider icon, model, cadence, next run, and
  status, with row actions to edit, pause/resume, run now, and delete.
- Create/edit sheet that reuses the agent provider/model/mode/working-
  directory pickers, plus an interval/cron cadence editor with presets,
  validation, and a UTC preview.
- Responsive: hover-revealed row actions on desktop, an always-visible
  action menu on mobile.
- Data layer over the existing schedule RPCs (list/create/update/pause/
  resume/delete/run-once) with optimistic pause, resume, and delete.

Scope is limited to new-agent schedules for now.

* Make the schedule form controls visually consistent

Merge the provider and model pickers into the single nested selector the
draft screen uses, render it as a full-width field, mute the placeholder,
proportion the cadence segmented controls, unify the cron preset chips,
and route the sheet scroll through the themed scrollbar.

* Address schedules review feedback

* Translate schedule hook client fallback errors

* Make schedules global and project scoped

* Stabilize project settings transport E2E

* Reset schedule model on cross-host project change

* Reject malformed cron step expressions

* Tighten schedules project query freshness

* refactor(schedules): clean up review feedback

* test(schedules): extract fake host setup

* refactor(schedules): share cron expression parsing

Use one structural cron parser for app previews and daemon cadence validation so the accepted grammar cannot drift between surfaces.

Remove the unused host-scoped schedules redirect while keeping the global schedules route.
This commit is contained in:
Mohamed Boudra
2026-06-30 22:58:49 +02:00
committed by GitHub
parent b8b66816ca
commit bf5e3b47e7
36 changed files with 3622 additions and 146 deletions
@@ -0,0 +1,81 @@
import type { DaemonClient } from "@getpaseo/client/internal/daemon-client";
import type { ScheduleSummary } from "@getpaseo/protocol/schedule/types";
import { isNewAgentSchedule } from "@/utils/schedule-format";
import { toErrorMessage } from "@/utils/error-messages";
export interface ScheduleHostInput {
serverId: string;
serverName: string;
}
export interface ScheduleRuntimeSnapshot {
connectionStatus: string;
}
export interface ScheduleRuntime {
getClient(serverId: string): Pick<DaemonClient, "scheduleList"> | null;
getSnapshot(serverId: string): ScheduleRuntimeSnapshot | null | undefined;
}
export interface ScheduleHostSection {
serverId: string;
serverName: string;
isOnline: boolean;
schedules: ScheduleSummary[];
error: string | null;
}
export interface FetchAggregatedSchedulesInput {
hosts: ScheduleHostInput[];
runtime: ScheduleRuntime;
}
export interface FetchAggregatedSchedulesResult {
sections: ScheduleHostSection[];
}
export async function fetchAggregatedSchedules(
input: FetchAggregatedSchedulesInput,
): Promise<FetchAggregatedSchedulesResult> {
const sections = await Promise.all(
input.hosts.map(async (host): Promise<ScheduleHostSection> => {
const snapshot = input.runtime.getSnapshot(host.serverId);
const isOnline = snapshot?.connectionStatus === "online";
const client = input.runtime.getClient(host.serverId);
if (!client || !isOnline) {
return {
serverId: host.serverId,
serverName: host.serverName,
isOnline,
schedules: [],
error: null,
};
}
try {
const payload = await client.scheduleList();
if (payload.error) {
throw new Error(payload.error);
}
return {
serverId: host.serverId,
serverName: host.serverName,
isOnline,
schedules: payload.schedules.filter(isNewAgentSchedule),
error: null,
};
} catch (error) {
return {
serverId: host.serverId,
serverName: host.serverName,
isOnline,
schedules: [],
error: toErrorMessage(error),
};
}
}),
);
return { sections };
}