Skip to content

Commit 0d859c2

Browse files
feat(ui): add scheduled task management (#354)
## Summary - Add a Scheduled Tasks experience with direct creation, a consolidated cadence editor, active/paused controls, edit flows, irreversible-delete confirmation, and a disabled “Snooze — coming soon” affordance. - Add an occurrence-focused Upcoming timeline with current/all-agent scope, synchronized task-sidebar filtering, balanced previews for frequent schedules, run-now flows, and exact skip/unskip actions. - Use the generated TypeScript schedule SDK, label scheduled runs consistently in the task sidebar, and keep scheduled task titles stable without appending occurrence timestamps. ## UX details - Create schedules immediately without a confirmation step; show animated pending/success feedback with Edit and auto-dismiss. - Group upcoming runs into Today, Tomorrow, and Later; show 10 by default with at most 3 per schedule, then allow expansion to the 10 occurrences returned per schedule. - Keep occurrence actions scoped to Skip/Unskip and Edit schedule; keep Pause/Resume, Delete, Run now, and Skip next run in the Schedules view. - Keep schedule names consistent with agent naming semantics: auto-generated kebab-case names are displayed and copied to scheduled task metadata. ## Test plan - [x] `npm --prefix agentex-ui run typecheck` - [x] `npm --prefix agentex-ui run lint` - [x] `npm --prefix agentex-ui run test:run -- hooks/use-safe-search-params.test.tsx lib/agent-run-schedules.test.ts lib/schedule-utils.test.ts` - [x] Local smoke test with two registered agents, cross-agent schedule scope, upcoming schedule details, and schedule actions - [x] Pre-commit Agentex UI lint hooks https://github.com/user-attachments/assets/decc0659-8c22-43db-b13d-55a26bc0a3e4 <!-- greptile_comment --> <h3>Greptile Summary</h3> This PR delivers a full Scheduled Tasks UI surface: inline schedule creation with animated feedback, an Upcoming timeline grouped by Today/Tomorrow/Later, a Schedules list with pause/resume toggle, skip/unskip per-occurrence, run-now flows, edit and delete modals, and a feature flag (`ENABLE_AGENT_RUN_SCHEDULES`) that gates the entire experience. - New `agentex-ui/components/scheduled-tasks/` module (~1 400 lines) provides the page, composer, cadence picker, upcoming list, schedules list, modals, and shared helpers; routing is handled entirely via URL search params (`view`, `schedule_scope`, `agent_name`). - `lib/schedule-utils.ts` owns cadence serialization, validation, display, and the `scheduleToCadence` round-trip used by the edit form. - `agentex/src/temporal/activities/scheduled_agent_run_activities.py` removes `_format_fire_time` and the per-occurrence timestamp from `task_metadata.display_name`, keeping task names stable across retries. <details><summary><h3>Confidence Score: 5/5</h3></summary> Safe to merge; the feature is flag-gated and all CRUD flows, cache invalidation, and error paths look correct. Additive, flag-gated change. TanStack Query cache keys are well-structured, mutation error handling surfaces toasts via onError, and search-param routing clears stale VIEW values on task selection. The two findings only affect externally-created sub-minute or complex-cron schedules that the UI itself never produces. agentex-ui/lib/schedule-utils.ts — specifically scheduleToCadence (sub-minute round-trip) and describeCadence (missing complex-cron fallback). </details> <details><summary><h3>Important Files Changed</h3></summary> | Filename | Overview | |----------|----------| | agentex-ui/lib/schedule-utils.ts | New utility module for cadence serialization, validation, and display. scheduleToCadence silently coerces sub-minute interval_seconds (e.g. 30 s → 1 min) and describeCadence lacks the raw-expression fallback its inline comment promises for complex external crons. | | agentex-ui/hooks/use-agent-run-schedules.ts | New TanStack Query hooks for schedule CRUD and actions. Cache key structure is consistent, invalidation logic is correct, and error toasts are surfaced via onError. Limit is 50 and is surfaced in the UI. | | agentex-ui/components/scheduled-tasks/scheduled-tasks-page.tsx | Main page orchestrating scope/agent/view state. Correctly wires search-param-driven scope selection and shows ScheduleComposer only for the current-agent scope. | | agentex-ui/components/scheduled-tasks/all-schedules-list.tsx | Schedule list with inline pause/resume toggle and overflow menu. Functionally correct; idempotent API calls handle the case where row-level and menu-level mutation pending states don't reflect each other. | | agentex-ui/components/scheduled-tasks/upcoming-schedule-list.tsx | Upcoming runs timeline with Today/Tomorrow/Later grouping. Run-now/skip sequential flow is handled correctly with .catch(() => undefined) to suppress uncaught-rejection noise while onError toasts surface failures. | | agentex-ui/components/scheduled-tasks/schedule-modals.tsx | Delete confirmation and edit modals. Name normalization/sanitization on blur, cadence round-trip via scheduleToCadence, and inline delete from the edit modal all look correct. | | agentex/src/temporal/activities/scheduled_agent_run_activities.py | Removes per-occurrence timestamp from display_name (now just schedule.name), removes the unused _format_fire_time helper, and keeps _extract_fire_time/regex for UI occurrence-time parsing. | </details> <details><summary><h3>Flowchart</h3></summary> <a href="#gh-light-mode-only"> ```mermaid %%{init: {'theme': 'neutral'}}%% flowchart TD A[Page Load] --> B{agentRunSchedulesEnabled?} B -->|false| C[Chat / Home only] B -->|true| D{view=scheduled_tasks?} D -->|no| E{taskID param?} E -->|yes| F[ChatView] E -->|no| G[HomeView] D -->|yes| H[ScheduledTasksPage] H --> I{scheduleScope?} I -->|ALL| J[useAgentRunSchedulesForAgents all agents] I -->|CURRENT| K{agent selected?} K -->|no| L[Empty State: Select an agent] K -->|yes| M[useAgentRunSchedules current agent] J --> N[useAgentRunScheduleDetailsForItems one req per schedule] M --> N N --> O{scheduleView?} O -->|upcoming| P[UpcomingScheduleList Today/Tomorrow/Later] O -->|all| Q[AllSchedulesList sorted by next run] M --> R[ScheduleComposer create new schedule] ``` </a> <a href="#gh-dark-mode-only"> ```mermaid %%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%% flowchart TD A[Page Load] --> B{agentRunSchedulesEnabled?} B -->|false| C[Chat / Home only] B -->|true| D{view=scheduled_tasks?} D -->|no| E{taskID param?} E -->|yes| F[ChatView] E -->|no| G[HomeView] D -->|yes| H[ScheduledTasksPage] H --> I{scheduleScope?} I -->|ALL| J[useAgentRunSchedulesForAgents all agents] I -->|CURRENT| K{agent selected?} K -->|no| L[Empty State: Select an agent] K -->|yes| M[useAgentRunSchedules current agent] J --> N[useAgentRunScheduleDetailsForItems one req per schedule] M --> N N --> O{scheduleView?} O -->|upcoming| P[UpcomingScheduleList Today/Tomorrow/Later] O -->|all| Q[AllSchedulesList sorted by next run] M --> R[ScheduleComposer create new schedule] ``` </a> </details> <sub>Reviews (8): Last reviewed commit: ["Merge branch &#39;main&#39; into jerome/schedule..."](b2fde1c) | [Re-trigger Greptile](https://app.greptile.com/api/retrigger?id=43852411)</sub> <!-- /greptile_comment --> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent e2c8a22 commit 0d859c2

29 files changed

Lines changed: 3229 additions & 62 deletions

agentex-ui/app/page.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,17 @@ import { connection } from 'next/server';
22

33
import { AgentexUIRoot } from '@/components/agentex-ui-root';
44
import { AgentexProvider } from '@/components/providers';
5+
import { parseBooleanEnv } from '@/lib/env-utils';
56

67
export default async function RootPage() {
78
await connection();
89

910
const sgpAppURL = process.env.NEXT_PUBLIC_SGP_APP_URL ?? '';
1011
const authEnabled = !!process.env.AGENTEX_UI_AUTH_PROVIDER_ID;
12+
const agentRunSchedulesEnabled = parseBooleanEnv(
13+
process.env.ENABLE_AGENT_RUN_SCHEDULES,
14+
'ENABLE_AGENT_RUN_SCHEDULES'
15+
);
1116
// The account picker needs the platform API (accounts come from /api/user-info).
1217
const accountsEnabled = !!(
1318
process.env.SGP_API_URL ?? process.env.NEXT_PUBLIC_SGP_APP_URL
@@ -19,7 +24,7 @@ export default async function RootPage() {
1924
accountsEnabled={accountsEnabled}
2025
sgpAppURL={sgpAppURL}
2126
>
22-
<AgentexUIRoot />
27+
<AgentexUIRoot agentRunSchedulesEnabled={agentRunSchedulesEnabled} />
2328
</AgentexProvider>
2429
);
2530
}

agentex-ui/components/agentex-ui-root.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,13 @@ import {
1616
useSafeSearchParams,
1717
} from '@/hooks/use-safe-search-params';
1818

19-
export function AgentexUIRoot() {
19+
type AgentexUIRootProps = {
20+
agentRunSchedulesEnabled: boolean;
21+
};
22+
23+
export function AgentexUIRoot({
24+
agentRunSchedulesEnabled,
25+
}: AgentexUIRootProps) {
2026
const { agentName, taskID, sgpAccountID, updateParams } =
2127
useSafeSearchParams();
2228
const [isTracesSidebarOpen, setIsTracesSidebarOpen] = useState(false);
@@ -84,6 +90,7 @@ export function AgentexUIRoot() {
8490
(taskId: string | null) => {
8591
updateParams({
8692
[SearchParamKey.TASK_ID]: taskId,
93+
[SearchParamKey.VIEW]: null,
8794
});
8895
},
8996
[updateParams]
@@ -115,8 +122,9 @@ export function AgentexUIRoot() {
115122
return (
116123
<>
117124
<div className="fixed inset-0 flex w-full">
118-
<TaskSidebar />
125+
<TaskSidebar agentRunSchedulesEnabled={agentRunSchedulesEnabled} />
119126
<PrimaryContent
127+
agentRunSchedulesEnabled={agentRunSchedulesEnabled}
120128
isTracesSidebarOpen={isTracesSidebarOpen}
121129
toggleTracesSidebar={() =>
122130
setIsTracesSidebarOpen(!isTracesSidebarOpen)

agentex-ui/components/primary-content/primary-content.tsx

Lines changed: 37 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,19 +6,24 @@ import { ArrowDown } from 'lucide-react';
66
import { ChatView } from '@/components/primary-content/chat-view';
77
import { HomeView } from '@/components/primary-content/home-view';
88
import { PromptInput } from '@/components/primary-content/prompt-input';
9+
import { ScheduledTasksPage } from '@/components/scheduled-tasks/scheduled-tasks-page';
910
import { IconButton } from '@/components/ui/icon-button';
10-
import { useSafeSearchParams } from '@/hooks/use-safe-search-params';
11+
import { AppView, useSafeSearchParams } from '@/hooks/use-safe-search-params';
1112

1213
type ContentAreaProps = {
14+
agentRunSchedulesEnabled: boolean;
1315
isTracesSidebarOpen: boolean;
1416
toggleTracesSidebar: () => void;
1517
};
1618

1719
export function PrimaryContent({
20+
agentRunSchedulesEnabled,
1821
isTracesSidebarOpen,
1922
toggleTracesSidebar,
2023
}: ContentAreaProps) {
21-
const { taskID } = useSafeSearchParams();
24+
const { taskID, view } = useSafeSearchParams();
25+
const isScheduledTasksView =
26+
agentRunSchedulesEnabled && view === AppView.SCHEDULED_TASKS;
2227

2328
const [prompt, setPrompt] = useState<string>('');
2429
const [showScrollButton, setShowScrollButton] = useState(false);
@@ -56,20 +61,24 @@ export function PrimaryContent({
5661
}, [scrollContainerRef]);
5762

5863
useEffect(() => {
59-
if (scrollContainerRef.current && taskID) {
64+
if (scrollContainerRef.current && taskID && !isScheduledTasksView) {
6065
setTimeout(() => {
6166
scrollToBottom();
6267
}, 150);
6368
}
64-
}, [scrollToBottom, taskID]);
69+
}, [scrollToBottom, taskID, isScheduledTasksView]);
6570

6671
return (
6772
<motion.div
6873
layout
69-
className={`relative flex h-full flex-1 flex-col ${!taskID ? 'justify-center' : 'justify-between'}`}
74+
className={`relative flex h-full flex-1 flex-col ${
75+
!taskID && !isScheduledTasksView ? 'justify-center' : 'justify-between'
76+
}`}
7077
transition={{ duration: 0.25, ease: 'easeInOut' }}
7178
>
72-
{taskID ? (
79+
{isScheduledTasksView ? (
80+
<ScheduledTasksPage />
81+
) : taskID ? (
7382
<ChatView
7483
taskID={taskID}
7584
isTracesSidebarOpen={isTracesSidebarOpen}
@@ -81,26 +90,28 @@ export function PrimaryContent({
8190
<HomeView />
8291
)}
8392

84-
<motion.div
85-
layout="position"
86-
className="relative flex w-full justify-center px-4 py-4 sm:px-6 md:px-8"
87-
transition={{
88-
layout: {
89-
type: 'spring',
90-
damping: 40,
91-
stiffness: 300,
92-
mass: 0.8,
93-
},
94-
}}
95-
>
96-
<AnimatePresence>
97-
{taskID && showScrollButton && (
98-
<ScrollToBottomButton scrollToBottom={scrollToBottom} />
99-
)}
100-
</AnimatePresence>
101-
102-
<PromptInput prompt={prompt} setPrompt={setPrompt} />
103-
</motion.div>
93+
{!isScheduledTasksView && (
94+
<motion.div
95+
layout="position"
96+
className="relative flex w-full justify-center px-4 py-4 sm:px-6 md:px-8"
97+
transition={{
98+
layout: {
99+
type: 'spring',
100+
damping: 40,
101+
stiffness: 300,
102+
mass: 0.8,
103+
},
104+
}}
105+
>
106+
<AnimatePresence>
107+
{taskID && showScrollButton && (
108+
<ScrollToBottomButton scrollToBottom={scrollToBottom} />
109+
)}
110+
</AnimatePresence>
111+
112+
<PromptInput prompt={prompt} setPrompt={setPrompt} />
113+
</motion.div>
114+
)}
104115
</motion.div>
105116
);
106117
}

0 commit comments

Comments
 (0)