diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/selected-views/SelectedRunView/SelectedRunView.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/selected-views/SelectedRunView/SelectedRunView.tsx index 2a80aafed4..857127164d 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/selected-views/SelectedRunView/SelectedRunView.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/selected-views/SelectedRunView/SelectedRunView.tsx @@ -69,7 +69,7 @@ export function SelectedRunView({ if (isLoading && !run) { return ( -
+
diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/selected-views/SelectedScheduleView/SelectedScheduleView.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/selected-views/SelectedScheduleView/SelectedScheduleView.tsx index a431e68021..48d93ec64d 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/selected-views/SelectedScheduleView/SelectedScheduleView.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/selected-views/SelectedScheduleView/SelectedScheduleView.tsx @@ -13,6 +13,7 @@ import { } from "@/components/molecules/TabsLine/TabsLine"; import { humanizeCronExpression } from "@/lib/cron-expression-utils"; import { formatInTimezone, getTimezoneDisplayName } from "@/lib/timezone-utils"; +import { AGENT_LIBRARY_SECTION_PADDING_X } from "../../../helpers"; import { AgentInputsReadOnly } from "../../modals/AgentInputsReadOnly/AgentInputsReadOnly"; import { RunDetailCard } from "../RunDetailCard/RunDetailCard"; import { RunDetailHeader } from "../RunDetailHeader/RunDetailHeader"; @@ -68,7 +69,7 @@ export function SelectedScheduleView({ if (isLoading && !schedule) { return ( -
+
@@ -103,7 +104,7 @@ export function SelectedScheduleView({
- + Your input Schedule diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/sidebar/SidebarRunsList/SidebarRunsList.tsx b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/sidebar/SidebarRunsList/SidebarRunsList.tsx index ae4423931a..6d5f2f98a6 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/sidebar/SidebarRunsList/SidebarRunsList.tsx +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/sidebar/SidebarRunsList/SidebarRunsList.tsx @@ -23,7 +23,7 @@ interface Props { selectedRunId?: string; onSelectRun: (id: string, tab?: "runs" | "scheduled") => void; onClearSelectedRun?: () => void; - onTabChange?: (tab: "runs" | "scheduled") => void; + onTabChange?: (tab: "runs" | "scheduled" | "templates") => void; onCountsChange?: (info: { runsCount: number; schedulesCount: number; @@ -74,7 +74,7 @@ export function SidebarRunsList({ { - const value = v as "runs" | "scheduled"; + const value = v as "runs" | "scheduled" | "templates"; onTabChange?.(value); if (value === "runs") { if (runs && runs.length) { @@ -82,12 +82,14 @@ export function SidebarRunsList({ } else { onClearSelectedRun?.(); } - } else { + } else if (value === "scheduled") { if (schedules && schedules.length) { onSelectRun(schedules[0].id, "scheduled"); } else { onClearSelectedRun?.(); } + } else if (value === "templates") { + onClearSelectedRun?.(); } }} className="flex min-h-0 flex-col overflow-hidden" @@ -134,7 +136,7 @@ export function SidebarRunsList({ diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/sidebar/SidebarRunsList/helpers.ts b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/sidebar/SidebarRunsList/helpers.ts index 4cbe6787d9..096e40239b 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/sidebar/SidebarRunsList/helpers.ts +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/sidebar/SidebarRunsList/helpers.ts @@ -1,8 +1,6 @@ import type { GraphExecutionsPaginated } from "@/app/api/__generated__/models/graphExecutionsPaginated"; import type { InfiniteData } from "@tanstack/react-query"; -const AGENT_RUNNING_POLL_INTERVAL = 1500; - function hasValidExecutionsData( page: unknown, ): page is { data: GraphExecutionsPaginated } { @@ -16,26 +14,6 @@ function hasValidExecutionsData( ); } -export function getRunsPollingInterval( - pages: Array | undefined, - isRunsTab: boolean, -): number | false { - if (!isRunsTab || !pages?.length) return false; - - try { - const executions = pages.flatMap((page) => { - if (!hasValidExecutionsData(page)) return []; - return page.data.executions || []; - }); - const hasActive = executions.some( - (e) => e.status === "RUNNING" || e.status === "QUEUED", - ); - return hasActive ? AGENT_RUNNING_POLL_INTERVAL : false; - } catch { - return false; - } -} - export function computeRunsCount( infiniteData: InfiniteData | undefined, runsLength: number, diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/sidebar/SidebarRunsList/useSidebarRunsList.ts b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/sidebar/SidebarRunsList/useSidebarRunsList.ts index 80900e8cc6..eecada463a 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/sidebar/SidebarRunsList/useSidebarRunsList.ts +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/components/sidebar/SidebarRunsList/useSidebarRunsList.ts @@ -6,12 +6,13 @@ import { useGetV1ListGraphExecutionsInfinite } from "@/app/api/__generated__/end import { useGetV1ListExecutionSchedulesForAGraph } from "@/app/api/__generated__/endpoints/schedules/schedules"; import type { GraphExecutionJobInfo } from "@/app/api/__generated__/models/graphExecutionJobInfo"; import { okData } from "@/app/api/helpers"; +import { useExecutionEvents } from "@/hooks/useExecutionEvents"; +import { useQueryClient } from "@tanstack/react-query"; import { parseAsString, useQueryStates } from "nuqs"; import { computeRunsCount, extractRunsFromPages, getNextRunsPageParam, - getRunsPollingInterval, } from "./helpers"; function parseTab(value: string | null): "runs" | "scheduled" | "templates" { @@ -42,6 +43,7 @@ export function useSidebarRunsList({ }); const tabValue = useMemo(() => parseTab(activeTabRaw), [activeTabRaw]); + const queryClient = useQueryClient(); const runsQuery = useGetV1ListGraphExecutionsInfinite( graphId || "", @@ -49,9 +51,6 @@ export function useSidebarRunsList({ { query: { enabled: !!graphId, - refetchInterval: (q) => - getRunsPollingInterval(q.state.data?.pages, tabValue === "runs"), - refetchIntervalInBackground: true, refetchOnWindowFocus: false, getNextPageParam: getNextRunsPageParam, }, @@ -79,6 +78,19 @@ export function useSidebarRunsList({ const schedulesCount = schedules.length; const loading = !schedulesQuery.isSuccess || !runsQuery.isSuccess; + // Update query cache when execution events arrive via websocket + useExecutionEvents({ + graphId: graphId || undefined, + enabled: !!graphId && tabValue === "runs", + onExecutionUpdate: (_execution) => { + // Invalidate and refetch the query to ensure we have the latest data + // This is simpler and more reliable than manually updating the cache + // The queryKey is stable and includes the graphId, so this only invalidates + // queries for this specific graph's executions + queryClient.invalidateQueries({ queryKey: runsQuery.queryKey }); + }, + }); + // Notify parent about counts and loading state useEffect(() => { if (onCountsChange) { diff --git a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/useNewAgentLibraryView.ts b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/useNewAgentLibraryView.ts index a97e64650c..011956cb40 100644 --- a/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/useNewAgentLibraryView.ts +++ b/autogpt_platform/frontend/src/app/(platform)/library/agents/[id]/components/NewAgentLibraryView/useNewAgentLibraryView.ts @@ -77,7 +77,7 @@ export function useNewAgentLibraryView() { }); } - function handleSetActiveTab(tab: "runs" | "scheduled") { + function handleSetActiveTab(tab: "runs" | "scheduled" | "templates") { setQueryStates({ activeTab: tab, }); diff --git a/autogpt_platform/frontend/src/components/layout/Navbar/components/AgentActivityDropdown/useAgentActivityDropdown.ts b/autogpt_platform/frontend/src/components/layout/Navbar/components/AgentActivityDropdown/useAgentActivityDropdown.ts index 6df18738ca..df8402906b 100644 --- a/autogpt_platform/frontend/src/components/layout/Navbar/components/AgentActivityDropdown/useAgentActivityDropdown.ts +++ b/autogpt_platform/frontend/src/components/layout/Navbar/components/AgentActivityDropdown/useAgentActivityDropdown.ts @@ -1,19 +1,17 @@ import { useGetV1ListAllExecutions } from "@/app/api/__generated__/endpoints/graphs/graphs"; -import BackendAPI from "@/lib/autogpt-server-api/client"; -import type { GraphExecution, GraphID } from "@/lib/autogpt-server-api/types"; -import { useCallback, useEffect, useState } from "react"; -import * as Sentry from "@sentry/nextjs"; +import { useExecutionEvents } from "@/hooks/useExecutionEvents"; +import { useLibraryAgents } from "@/hooks/useLibraryAgents/useLibraryAgents"; +import type { GraphExecution } from "@/lib/autogpt-server-api/types"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { NotificationState, categorizeExecutions, handleExecutionUpdate, } from "./helpers"; -import { useLibraryAgents } from "@/hooks/useLibraryAgents/useLibraryAgents"; export function useAgentActivityDropdown() { const [isOpen, setIsOpen] = useState(false); - const [api] = useState(() => new BackendAPI()); const { agentInfoMap } = useLibraryAgents(); const [notifications, setNotifications] = useState({ @@ -23,8 +21,6 @@ export function useAgentActivityDropdown() { totalCount: 0, }); - const [isConnected, setIsConnected] = useState(false); - const { data: executions, isSuccess: executionsSuccess, @@ -33,6 +29,12 @@ export function useAgentActivityDropdown() { query: { select: (res) => (res.status === 200 ? res.data : null) }, }); + // Get all graph IDs from agentInfoMap + const graphIds = useMemo( + () => Array.from(agentInfoMap.keys()), + [agentInfoMap], + ); + // Handle real-time execution updates const handleExecutionEvent = useCallback( (execution: GraphExecution) => { @@ -51,45 +53,15 @@ export function useAgentActivityDropdown() { } }, [executions, executionsSuccess, agentInfoMap]); - // Initialize WebSocket connection for real-time updates - useEffect(() => { - if (!agentInfoMap.size) return; - - const connectHandler = api.onWebSocketConnect(() => { - setIsConnected(true); - agentInfoMap.forEach((_, graphId) => { - api.subscribeToGraphExecutions(graphId as GraphID).catch((error) => { - Sentry.captureException(error, { - tags: { - graphId, - }, - }); - }); - }); - }); - - const disconnectHandler = api.onWebSocketDisconnect(() => { - setIsConnected(false); - }); - - const messageHandler = api.onWebSocketMessage( - "graph_execution_event", - handleExecutionEvent, - ); - - api.connectWebSocket(); - - return () => { - connectHandler(); - disconnectHandler(); - messageHandler(); - api.disconnectWebSocket(); - }; - }, [api, handleExecutionEvent, agentInfoMap]); + // Subscribe to execution events for all graphs + useExecutionEvents({ + graphIds: graphIds.length > 0 ? graphIds : undefined, + enabled: graphIds.length > 0, + onExecutionUpdate: handleExecutionEvent, + }); return { ...notifications, - isConnected, isReady: executionsSuccess, error: executionsError, isOpen, diff --git a/autogpt_platform/frontend/src/hooks/useExecutionEvents.ts b/autogpt_platform/frontend/src/hooks/useExecutionEvents.ts new file mode 100644 index 0000000000..9af2b8aead --- /dev/null +++ b/autogpt_platform/frontend/src/hooks/useExecutionEvents.ts @@ -0,0 +1,99 @@ +"use client"; + +import { useBackendAPI } from "@/lib/autogpt-server-api/context"; +import type { GraphExecution, GraphID } from "@/lib/autogpt-server-api/types"; +import * as Sentry from "@sentry/nextjs"; +import { useEffect, useRef } from "react"; + +type ExecutionEventHandler = (execution: GraphExecution) => void; + +interface UseExecutionEventsOptions { + graphId?: GraphID | string | null; + graphIds?: (GraphID | string)[]; + enabled?: boolean; + onExecutionUpdate?: ExecutionEventHandler; +} + +/** + * Generic hook to subscribe to graph execution events via WebSocket. + * Automatically handles subscription/unsubscription and reconnection. + * + * @param options - Configuration options + * @param options.graphId - The graph ID to subscribe to (single graph) + * @param options.graphIds - Array of graph IDs to subscribe to (multiple graphs) + * @param options.enabled - Whether the subscription is enabled (default: true) + * @param options.onExecutionUpdate - Callback invoked when an execution is updated + */ +export function useExecutionEvents({ + graphId, + graphIds, + enabled = true, + onExecutionUpdate, +}: UseExecutionEventsOptions) { + const api = useBackendAPI(); + const onExecutionUpdateRef = useRef(onExecutionUpdate); + + useEffect(() => { + onExecutionUpdateRef.current = onExecutionUpdate; + }, [onExecutionUpdate]); + + useEffect(() => { + if (!enabled) return; + + const idsToSubscribe = graphIds || (graphId ? [graphId] : []); + if (idsToSubscribe.length === 0) return; + + // Normalize IDs to strings for consistent comparison + const normalizedIds = idsToSubscribe.map((id) => String(id)); + const subscribedIds = new Set(); + + const handleExecutionEvent = (execution: GraphExecution) => { + // Filter by graphIds if provided, using normalized string comparison + if (normalizedIds.length > 0) { + const executionGraphId = String(execution.graph_id); + if (!normalizedIds.includes(executionGraphId)) return; + } + + onExecutionUpdateRef.current?.(execution); + }; + + const connectHandler = api.onWebSocketConnect(() => { + normalizedIds.forEach((id) => { + // Track subscriptions to avoid duplicate subscriptions + if (subscribedIds.has(id)) return; + subscribedIds.add(id); + + api + .subscribeToGraphExecutions(id as GraphID) + .then(() => { + console.debug(`Subscribed to execution updates for graph ${id}`); + }) + .catch((error) => { + console.error( + `Failed to subscribe to execution updates for graph ${id}:`, + error, + ); + Sentry.captureException(error, { + tags: { graphId: id }, + }); + subscribedIds.delete(id); + }); + }); + }); + + const messageHandler = api.onWebSocketMessage( + "graph_execution_event", + handleExecutionEvent, + ); + + api.connectWebSocket(); + + return () => { + connectHandler(); + messageHandler(); + // Note: Backend automatically cleans up subscriptions on websocket disconnect + // If IDs change while connected, old subscriptions remain but are filtered client-side + subscribedIds.clear(); + }; + }, [api, graphId, graphIds, enabled]); +}