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]);
+}