diff --git a/ui/src/components/AgentActionButtons.test.tsx b/ui/src/components/AgentActionButtons.test.tsx index 6a156c0cee..507530bdbd 100644 --- a/ui/src/components/AgentActionButtons.test.tsx +++ b/ui/src/components/AgentActionButtons.test.tsx @@ -1,6 +1,6 @@ // @vitest-environment jsdom -import type { ReactNode } from "react"; +import type { ComponentProps, ReactNode } from "react"; import { flushSync } from "react-dom"; import { createRoot } from "react-dom/client"; import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; @@ -107,6 +107,7 @@ describe("AgentActionButtons", () => { mockAgentsApi.clearError.mockResolvedValue(makeAgent({ status: "idle" })); mockAgentsApi.pause.mockResolvedValue(makeAgent({ status: "paused" })); mockAgentsApi.resume.mockResolvedValue(makeAgent({ status: "idle" })); + mockAgentsApi.terminate.mockResolvedValue(makeAgent({ status: "terminated" })); mockAgentsApi.invoke.mockResolvedValue({ id: "run-1" }); mockAgentsApi.resetSession.mockResolvedValue(undefined); }); @@ -124,11 +125,11 @@ describe("AgentActionButtons", () => { vi.clearAllMocks(); }); - function render(agent: Agent) { + function render(agent: Agent, props: Partial> = {}) { root = createRoot(container); root.render( - + , ); } @@ -175,4 +176,33 @@ describe("AgentActionButtons", () => { expect(container.textContent).toContain("Pause"); expect(container.textContent).not.toContain("Clear error"); }); + + it("calls the terminate success handler after terminating an agent", async () => { + const onTerminateSuccess = vi.fn(); + render(makeAgent(), { onTerminateSuccess }); + await flushReact(); + + await act(async () => { + container.querySelector('[aria-label="Open actions for Alpha Agent"]')?.click(); + }); + await flushReact(); + + const terminateButton = Array.from(document.body.querySelectorAll("button")) + .find((button) => button.textContent?.includes("Terminate")); + expect(terminateButton).toBeTruthy(); + + await act(async () => { + terminateButton?.click(); + }); + await flushReact(); + + expect(mockAgentsApi.terminate).toHaveBeenCalledWith("agent-1", "company-1"); + expect(onTerminateSuccess).toHaveBeenCalledWith(expect.objectContaining({ + id: "agent-1", + status: "terminated", + })); + expect(invalidateQueries).toHaveBeenCalledWith({ queryKey: ["agents", "detail", "agent-1"] }); + expect(invalidateQueries).toHaveBeenCalledWith({ queryKey: ["agents", "detail", "alpha"] }); + expect(invalidateQueries).toHaveBeenCalledWith({ queryKey: ["agents", "company-1"] }); + }); }); diff --git a/ui/src/components/AgentActionButtons.tsx b/ui/src/components/AgentActionButtons.tsx index dc13464b06..48e444ca92 100644 --- a/ui/src/components/AgentActionButtons.tsx +++ b/ui/src/components/AgentActionButtons.tsx @@ -165,6 +165,7 @@ export function AgentActionButtons({ workActionsDisabledReason, navigateToRunOnInvoke = true, onActionError, + onTerminateSuccess, pauseConfirm, hideTerminate = false, children, @@ -193,6 +194,8 @@ export function AgentActionButtons({ * omitted, failures surface as toasts (used by the list view). */ onActionError?: (message: string | null) => void; + /** Called after termination succeeds so callers can leave now-hidden detail routes. */ + onTerminateSuccess?: (agent: Agent) => void; /** Extra content rendered just before the overflow menu (e.g. live-run link). */ children?: React.ReactNode; className?: string; @@ -246,6 +249,9 @@ export function AgentActionButtons({ onSuccess: (data, action) => { onActionError?.(null); invalidateAgent(); + if (action === "terminate") { + onTerminateSuccess?.(data as Agent); + } if (action === "invoke" && navigateToRunOnInvoke && data && typeof data === "object" && "id" in data) { navigate(`/agents/${canonicalAgentRef}/runs/${(data as HeartbeatRun).id}`); } diff --git a/ui/src/pages/AgentDetail.tsx b/ui/src/pages/AgentDetail.tsx index e97c56c071..1cee14ad96 100644 --- a/ui/src/pages/AgentDetail.tsx +++ b/ui/src/pages/AgentDetail.tsx @@ -1157,6 +1157,7 @@ export function AgentDetail() { workActionsDisabled={hasInvalidOrgChain} workActionsDisabledReason="Repair this agent's reporting chain before assigning tasks or starting runs" onActionError={setActionError} + onTerminateSuccess={() => navigate("/agents/all", { replace: true })} hideTerminate={Boolean(builtInState)} pauseConfirm={ builtInState