diff --git a/ui/src/components/SidebarAgents.test.tsx b/ui/src/components/SidebarAgents.test.tsx index ed4b28e783f..c75327fb767 100644 --- a/ui/src/components/SidebarAgents.test.tsx +++ b/ui/src/components/SidebarAgents.test.tsx @@ -238,4 +238,76 @@ describe("SidebarAgents", () => { expect(mockAgentsApi.resume).toHaveBeenCalledWith("agent-1", "company-1"); expect(mockPushToast).toHaveBeenCalledWith(expect.objectContaining({ title: "Agent resumed" })); }); + + it("only shows updating state for the agent currently being changed", async () => { + mockAgentsApi.list.mockResolvedValue([ + makeAgent({ id: "agent-1", name: "Alpha", urlKey: "alpha" }), + makeAgent({ id: "agent-2", name: "Beta", urlKey: "beta" }), + ]); + mockAgentsApi.pause.mockImplementation(() => new Promise(() => {})); + const currentRoot = createRoot(container); + root = currentRoot; + + await act(async () => { + currentRoot.render( + + + , + ); + }); + await flushReact(); + await openAgentMenu(); + + const pauseItem = Array.from(document.body.querySelectorAll('[data-slot="dropdown-menu-item"]')) + .find((element) => element.textContent?.includes("Pause agent")); + expect(pauseItem).toBeTruthy(); + + await act(async () => { + pauseItem?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + await flushReact(); + await openAgentMenu("Open actions for Beta"); + + const betaPauseItem = Array.from( + document.body.querySelectorAll('[data-slot="dropdown-menu-item"]'), + ) + .find((element) => element.textContent?.includes("Pause agent")); + expect(betaPauseItem).toBeTruthy(); + expect(document.body.textContent).not.toContain("Updating..."); + }); + + it("does not offer sidebar resume for budget-paused agents", async () => { + mockAgentsApi.list.mockResolvedValue([ + makeAgent({ + status: "paused", + pauseReason: "budget", + pausedAt: new Date("2026-01-02T00:00:00Z"), + }), + ]); + const currentRoot = createRoot(container); + root = currentRoot; + + await act(async () => { + currentRoot.render( + + + , + ); + }); + await flushReact(); + await openAgentMenu(); + + const budgetPausedItem = Array.from( + document.body.querySelectorAll('[data-slot="dropdown-menu-item"]'), + ) + .find((element) => element.textContent?.includes("Budget paused")); + expect(budgetPausedItem).toBeTruthy(); + + await act(async () => { + budgetPausedItem?.dispatchEvent(new MouseEvent("click", { bubbles: true })); + }); + await flushReact(); + + expect(mockAgentsApi.resume).not.toHaveBeenCalled(); + }); }); diff --git a/ui/src/components/SidebarAgents.tsx b/ui/src/components/SidebarAgents.tsx index e0aa98bb60b..e2efa56eb50 100644 --- a/ui/src/components/SidebarAgents.tsx +++ b/ui/src/components/SidebarAgents.tsx @@ -61,7 +61,14 @@ function SidebarAgentItem({ const editHref = `${agentUrl(agent)}/configuration`; const isActive = activeAgentId === routeRef; const isPaused = agent.status === "paused"; + const isBudgetPaused = isPaused && agent.pauseReason === "budget"; const pauseResumeLabel = isPaused ? "Resume agent" : "Pause agent"; + const pauseResumeDisabled = disabled || agent.status === "pending_approval" || isBudgetPaused; + const pauseResumeDisabledLabel = disabled + ? "Updating..." + : isBudgetPaused + ? "Budget paused" + : pauseResumeLabel; return (
@@ -130,11 +137,15 @@ function SidebarAgentItem({ onPauseResume(agent, isPaused ? "resume" : "pause")} - disabled={disabled || agent.status === "pending_approval"} + onClick={() => { + if (pauseResumeDisabled) return; + onPauseResume(agent, isPaused ? "resume" : "pause"); + }} + disabled={pauseResumeDisabled} + title={isBudgetPaused ? "Agent was paused by budget limits" : undefined} > {isPaused ? : } - {disabled ? "Updating..." : pauseResumeLabel} + {pauseResumeDisabledLabel} @@ -144,6 +155,7 @@ function SidebarAgentItem({ export function SidebarAgents() { const [open, setOpen] = useState(true); + const [pendingAgentIds, setPendingAgentIds] = useState>(() => new Set()); const queryClient = useQueryClient(); const { selectedCompanyId } = useCompany(); const { openNewAgent } = useDialog(); @@ -198,6 +210,13 @@ export function SidebarAgents() { action === "pause" ? agentsApi.pause(agent.id, selectedCompanyId ?? undefined) : agentsApi.resume(agent.id, selectedCompanyId ?? undefined), + onMutate: ({ agent }) => { + setPendingAgentIds((current) => { + const next = new Set(current); + next.add(agent.id); + return next; + }); + }, onSuccess: async (_agent, { agent, action }) => { if (selectedCompanyId) { await Promise.all([ @@ -223,6 +242,13 @@ export function SidebarAgents() { tone: "error", }); }, + onSettled: (_data, _error, { agent }) => { + setPendingAgentIds((current) => { + const next = new Set(current); + next.delete(agent.id); + return next; + }); + }, }); return ( @@ -263,7 +289,7 @@ export function SidebarAgents() { activeAgentId={activeAgentId} activeTab={activeTab} agent={agent} - disabled={pauseResumeAgent.isPending} + disabled={pendingAgentIds.has(agent.id)} isMobile={isMobile} onPauseResume={(targetAgent, action) => pauseResumeAgent.mutate({ agent: targetAgent, action })} runCount={runCount}