From d190024510fd0c551bcd13c197666382bcf873cc Mon Sep 17 00:00:00 2001 From: Jerome Romualdez Date: Thu, 9 Jul 2026 09:53:19 -0400 Subject: [PATCH 1/5] feat(ui): add scheduled tasks first pass Co-authored-by: Cursor --- agentex-ui/components/agentex-ui-root.tsx | 1 + .../primary-content/primary-content.tsx | 60 +- .../components/providers/agentex-provider.tsx | 5 +- .../scheduled-tasks/scheduled-tasks-page.tsx | 647 ++++++++++++++++++ .../components/task-sidebar/task-button.tsx | 1 + .../task-sidebar/task-sidebar-header.tsx | 25 +- .../components/task-sidebar/task-sidebar.tsx | 1 + agentex-ui/hooks/use-agent-run-schedules.ts | 176 +++++ agentex-ui/hooks/use-safe-search-params.ts | 12 +- agentex-ui/lib/agent-run-schedules.ts | 148 ++++ agentex-ui/lib/schedule-utils.test.ts | 124 ++++ agentex-ui/lib/schedule-utils.ts | 169 +++++ 12 files changed, 1340 insertions(+), 29 deletions(-) create mode 100644 agentex-ui/components/scheduled-tasks/scheduled-tasks-page.tsx create mode 100644 agentex-ui/hooks/use-agent-run-schedules.ts create mode 100644 agentex-ui/lib/agent-run-schedules.ts create mode 100644 agentex-ui/lib/schedule-utils.test.ts create mode 100644 agentex-ui/lib/schedule-utils.ts diff --git a/agentex-ui/components/agentex-ui-root.tsx b/agentex-ui/components/agentex-ui-root.tsx index e027b974..1ac111bb 100644 --- a/agentex-ui/components/agentex-ui-root.tsx +++ b/agentex-ui/components/agentex-ui-root.tsx @@ -67,6 +67,7 @@ export function AgentexUIRoot() { (taskId: string | null) => { updateParams({ [SearchParamKey.TASK_ID]: taskId, + [SearchParamKey.VIEW]: null, }); }, [updateParams] diff --git a/agentex-ui/components/primary-content/primary-content.tsx b/agentex-ui/components/primary-content/primary-content.tsx index c7bdbe78..4afcfcff 100644 --- a/agentex-ui/components/primary-content/primary-content.tsx +++ b/agentex-ui/components/primary-content/primary-content.tsx @@ -6,8 +6,9 @@ import { ArrowDown } from 'lucide-react'; import { ChatView } from '@/components/primary-content/chat-view'; import { HomeView } from '@/components/primary-content/home-view'; import { PromptInput } from '@/components/primary-content/prompt-input'; +import { ScheduledTasksPage } from '@/components/scheduled-tasks/scheduled-tasks-page'; import { IconButton } from '@/components/ui/icon-button'; -import { useSafeSearchParams } from '@/hooks/use-safe-search-params'; +import { AppView, useSafeSearchParams } from '@/hooks/use-safe-search-params'; type ContentAreaProps = { isTracesSidebarOpen: boolean; @@ -18,7 +19,8 @@ export function PrimaryContent({ isTracesSidebarOpen, toggleTracesSidebar, }: ContentAreaProps) { - const { taskID } = useSafeSearchParams(); + const { taskID, view } = useSafeSearchParams(); + const isScheduledTasksView = view === AppView.SCHEDULED_TASKS; const [prompt, setPrompt] = useState(''); const [showScrollButton, setShowScrollButton] = useState(false); @@ -56,20 +58,24 @@ export function PrimaryContent({ }, [scrollContainerRef]); useEffect(() => { - if (scrollContainerRef.current && taskID) { + if (scrollContainerRef.current && taskID && !isScheduledTasksView) { setTimeout(() => { scrollToBottom(); }, 150); } - }, [scrollToBottom, taskID]); + }, [scrollToBottom, taskID, isScheduledTasksView]); return ( - {taskID ? ( + {isScheduledTasksView ? ( + + ) : taskID ? ( )} - - - {taskID && showScrollButton && ( - - )} - - - - + {!isScheduledTasksView && ( + + + {taskID && showScrollButton && ( + + )} + + + + + )} ); } diff --git a/agentex-ui/components/providers/agentex-provider.tsx b/agentex-ui/components/providers/agentex-provider.tsx index 4d3481ed..af927f83 100644 --- a/agentex-ui/components/providers/agentex-provider.tsx +++ b/agentex-ui/components/providers/agentex-provider.tsx @@ -6,6 +6,7 @@ import AgentexSDK from 'agentex'; interface AgentexContextValue { agentexClient: AgentexSDK; + agentexAPIBaseURL: string; sgpAppURL: string; } @@ -34,7 +35,9 @@ export function AgentexProvider({ ); return ( - + {children} ); diff --git a/agentex-ui/components/scheduled-tasks/scheduled-tasks-page.tsx b/agentex-ui/components/scheduled-tasks/scheduled-tasks-page.tsx new file mode 100644 index 00000000..24886538 --- /dev/null +++ b/agentex-ui/components/scheduled-tasks/scheduled-tasks-page.tsx @@ -0,0 +1,647 @@ +'use client'; + +import { useMemo, useState } from 'react'; + +import { CalendarClock, Loader2, MoreHorizontal } from 'lucide-react'; + +import { useAgentexClient } from '@/components/providers'; +import { Button } from '@/components/ui/button'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { toast } from '@/components/ui/toast'; +import { useAgentByName } from '@/hooks/use-agent-by-name'; +import { + useAgentRunScheduleDetails, + useAgentRunSchedules, + useCreateAgentRunSchedule, + useScheduleAction, + useUpdateAgentRunSchedule, +} from '@/hooks/use-agent-run-schedules'; +import { useAgents } from '@/hooks/use-agents'; +import { useSafeSearchParams } from '@/hooks/use-safe-search-params'; +import type { AgentRunSchedule } from '@/lib/agent-run-schedules'; +import { + cadenceToPayload, + DEFAULT_CADENCE, + describeCadence, + generateScheduleName, + normalizeScheduleName, + scheduleToCadence, + type CadenceConfig, + type CadenceType, +} from '@/lib/schedule-utils'; +import { cn } from '@/lib/utils'; + +const DAYS = [ + ['MON', 'Monday'], + ['TUE', 'Tuesday'], + ['WED', 'Wednesday'], + ['THU', 'Thursday'], + ['FRI', 'Friday'], + ['SAT', 'Saturday'], + ['SUN', 'Sunday'], +] as const; + +const COMMON_TIMEZONES = [ + 'UTC', + 'America/Los_Angeles', + 'America/Denver', + 'America/Chicago', + 'America/New_York', + 'America/Toronto', + 'America/Sao_Paulo', + 'Europe/London', + 'Europe/Paris', + 'Europe/Berlin', + 'Asia/Dubai', + 'Asia/Kolkata', + 'Asia/Singapore', + 'Asia/Tokyo', + 'Australia/Sydney', +] as const; + +function getBrowserTimezone() { + return Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC'; +} + +export function ScheduledTasksPage() { + const { agentName } = useSafeSearchParams(); + const { agentexClient, agentexAPIBaseURL } = useAgentexClient(); + const { data: agents = [] } = useAgents(agentexClient); + const { data: agentByName } = useAgentByName(agentexClient, agentName); + const selectedAgent = + agents.find(agent => agent.name === agentName) ?? agentByName ?? null; + const agentId = selectedAgent?.id ?? null; + + const schedulesQuery = useAgentRunSchedules(agentexAPIBaseURL, agentId); + const schedules = schedulesQuery.data ?? []; + const detailQueries = useAgentRunScheduleDetails( + agentexAPIBaseURL, + agentId, + schedules + ); + const schedulesWithLiveFields = schedules.map((schedule, index) => { + const detail = detailQueries[index]?.data; + return detail ?? schedule; + }); + + return ( +
+
+
+

+ Scheduled Tasks +

+

+ {agentName + ? `Run ${agentName} automatically on a cadence.` + : 'Select an agent to schedule recurring tasks.'} +

+
+
+ +
+ {!selectedAgent ? ( + + ) : ( + <> + + + + )} +
+
+ ); +} + +function ScheduleComposer({ + agentId, + baseURL, + schedules, +}: { + agentId: string; + baseURL: string; + schedules: AgentRunSchedule[]; +}) { + const [prompt, setPrompt] = useState(''); + const [cadence, setCadence] = useState(DEFAULT_CADENCE); + const [timezone, setTimezone] = useState(getBrowserTimezone); + const [pendingName, setPendingName] = useState(null); + const createSchedule = useCreateAgentRunSchedule({ baseURL, agentId }); + + const handleReview = () => { + if (!prompt.trim()) { + toast.error('Enter a prompt to schedule'); + return; + } + setPendingName(generateScheduleName(prompt, schedules)); + }; + + const handleCreate = async () => { + if (!pendingName) return; + await createSchedule.mutateAsync({ + name: pendingName, + timezone, + ...cadenceToPayload(cadence), + initial_input: { + type: 'text', + author: 'user', + content: prompt.trim(), + }, + }); + setPrompt(''); + setPendingName(null); + }; + + return ( +
+
+