Fix sidebar agent action edge cases

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-04-27 16:13:53 -05:00
1 parent f5fafa369e
commit 8730e30ed8
2 files changed
+102 -4

No files matched your search

+72
View File
@@ -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(
<QueryClientProvider client={queryClient}>
<SidebarAgents />
</QueryClientProvider>,
);
});
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(
<QueryClientProvider client={queryClient}>
<SidebarAgents />
</QueryClientProvider>,
);
});
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();
});
});
+30 -4
View File
@@ -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 (
<div className="group/agent relative flex items-center">
@@ -130,11 +137,15 @@ function SidebarAgentItem({
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => 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 ? <PlayCircle className="size-4" /> : <PauseCircle className="size-4" />}
<span>{disabled ? "Updating..." : pauseResumeLabel}</span>
<span>{pauseResumeDisabledLabel}</span>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
@@ -144,6 +155,7 @@ function SidebarAgentItem({
export function SidebarAgents() {
const [open, setOpen] = useState(true);
const [pendingAgentIds, setPendingAgentIds] = useState<Set<string>>(() => 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}