From b0ca1bebc0d9c9181527b853f17c4837b7d2b2a6 Mon Sep 17 00:00:00 2001
From: scotttong
Date: Tue, 2 Jun 2026 19:17:20 -0700
Subject: [PATCH] =?UTF-8?q?PAP-59:=20IA=20Phase=205=20=E2=80=94=20sidebar?=
=?UTF-8?q?=20active/recent=20fallback=20+=20agents=20list=20row=20actions?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Extract the agent detail action cluster (Assign Task, Run Heartbeat,
Pause/Resume, status, overflow ⋯ with Duplicate/Copy ID/Reset/Terminate)
into a shared AgentActionButtons component so the detail header and the
agents list rows share identical behavior.
- Agents list (/agents/all) rows now expose those actions; row click still
routes to agent detail (action clicks stop propagation).
- SidebarAgents: show only active agents when any have a live run; otherwise
up to 5 recently-active agents + a "See all agents" link.
Co-Authored-By: Paperclip
---
ui/src/components/AgentActionButtons.tsx | 296 ++++++++++++++++++++++-
ui/src/components/SidebarAgents.test.tsx | 45 ++++
ui/src/components/SidebarAgents.tsx | 33 ++-
ui/src/pages/AgentDetail.tsx | 224 ++---------------
ui/src/pages/Agents.tsx | 16 ++
5 files changed, 412 insertions(+), 202 deletions(-)
diff --git a/ui/src/components/AgentActionButtons.tsx b/ui/src/components/AgentActionButtons.tsx
index 2d698e47ee3..b0f95a27b8d 100644
--- a/ui/src/components/AgentActionButtons.tsx
+++ b/ui/src/components/AgentActionButtons.tsx
@@ -1,5 +1,40 @@
-import { Pause, Play } from "lucide-react";
+import { useCallback, useState } from "react";
+import { useNavigate } from "@/lib/router";
+import { useMutation, useQueryClient } from "@tanstack/react-query";
+import {
+ Pause,
+ Play,
+ Plus,
+ MoreHorizontal,
+ Loader2,
+ Copy,
+ RotateCcw,
+ Trash2,
+} from "lucide-react";
import { Button } from "@/components/ui/button";
+import {
+ Popover,
+ PopoverContent,
+ PopoverTrigger,
+} from "@/components/ui/popover";
+import { StatusBadge } from "./StatusBadge";
+import { agentsApi } from "../api/agents";
+import { ApiError } from "../api/client";
+import { queryKeys } from "../lib/queryKeys";
+import { agentRouteRef } from "../lib/utils";
+import { useDialogActions } from "../context/DialogContext";
+import { useToastActions } from "../context/ToastContext";
+import {
+ buildDuplicateAgentPayload,
+ duplicateAgentName,
+ type DuplicateInstructionsBundle,
+} from "../lib/duplicate-agent-payload";
+import type {
+ Agent,
+ AgentInstructionsBundle,
+ AgentInstructionsFileSummary,
+ HeartbeatRun,
+} from "@paperclipai/shared";
export function RunButton({
onClick,
@@ -49,3 +84,262 @@ export function PauseResumeButton({
);
}
+
+function duplicateInstructionFilePath(
+ _bundle: AgentInstructionsBundle,
+ summary: AgentInstructionsFileSummary,
+): string | null {
+ if (summary.deprecated || summary.virtual) return null;
+ return summary.path;
+}
+
+export async function loadDuplicateInstructionsBundle(
+ agentId: string,
+ companyId?: string,
+): Promise {
+ const bundle = await agentsApi.instructionsBundle(agentId, companyId);
+ const files: Record = {};
+
+ for (const summary of bundle.files) {
+ const path = duplicateInstructionFilePath(bundle, summary);
+ if (!path) continue;
+ const file = await agentsApi.instructionsFile(agentId, summary.path, companyId);
+ files[path] = file.content;
+ }
+
+ const entryFile = Object.prototype.hasOwnProperty.call(files, bundle.entryFile)
+ ? bundle.entryFile
+ : Object.keys(files)[0] ?? "AGENTS.md";
+ return Object.keys(files).length > 0 ? { entryFile, files } : null;
+}
+
+/**
+ * Shared agent action cluster used by both the agent detail header and the
+ * agents list rows. Encapsulates the invoke / pause / resume / terminate /
+ * duplicate / reset-session mutations so callers do not diverge in behavior.
+ */
+export function AgentActionButtons({
+ agent,
+ companyId,
+ size = "sm",
+ assignLabel = "Assign Task",
+ runLabel = "Run now",
+ showStatus = true,
+ actionsDisabled = false,
+ navigateToRunOnInvoke = true,
+ onActionError,
+ children,
+ className,
+}: {
+ agent: Agent;
+ companyId?: string | null;
+ size?: "sm" | "default";
+ assignLabel?: string;
+ runLabel?: string;
+ showStatus?: boolean;
+ actionsDisabled?: boolean;
+ navigateToRunOnInvoke?: boolean;
+ /**
+ * Optional inline error reporter. When provided it is used instead of a toast
+ * for action failures (preserves the detail page's inline error banner). When
+ * omitted, failures surface as toasts (used by the list view).
+ */
+ onActionError?: (message: string | null) => void;
+ /** Extra content rendered just before the overflow menu (e.g. live-run link). */
+ children?: React.ReactNode;
+ className?: string;
+}) {
+ const navigate = useNavigate();
+ const queryClient = useQueryClient();
+ const { openNewIssue } = useDialogActions();
+ const { pushToast } = useToastActions();
+ const [moreOpen, setMoreOpen] = useState(false);
+
+ const resolvedCompanyId = companyId ?? agent.companyId;
+ const canonicalAgentRef = agentRouteRef(agent);
+ const isPaused = agent.status === "paused";
+
+ const reportError = useCallback(
+ (message: string) => {
+ if (onActionError) {
+ onActionError(message);
+ } else {
+ pushToast({ title: "Action failed", body: message, tone: "error" });
+ }
+ },
+ [onActionError, pushToast],
+ );
+
+ const invalidateAgent = useCallback(() => {
+ queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(agent.id) });
+ queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(canonicalAgentRef) });
+ queryClient.invalidateQueries({ queryKey: queryKeys.agents.runtimeState(agent.id) });
+ queryClient.invalidateQueries({ queryKey: queryKeys.agents.taskSessions(agent.id) });
+ if (resolvedCompanyId) {
+ queryClient.invalidateQueries({ queryKey: queryKeys.agents.list(resolvedCompanyId) });
+ queryClient.invalidateQueries({ queryKey: queryKeys.liveRuns(resolvedCompanyId) });
+ queryClient.invalidateQueries({ queryKey: queryKeys.heartbeats(resolvedCompanyId, agent.id) });
+ }
+ }, [agent.id, canonicalAgentRef, queryClient, resolvedCompanyId]);
+
+ const agentAction = useMutation({
+ mutationFn: async (action: "invoke" | "pause" | "resume" | "approve" | "terminate") => {
+ switch (action) {
+ case "invoke": return agentsApi.invoke(agent.id, resolvedCompanyId ?? undefined);
+ case "pause": return agentsApi.pause(agent.id, resolvedCompanyId ?? undefined);
+ case "resume": return agentsApi.resume(agent.id, resolvedCompanyId ?? undefined);
+ case "approve": return agentsApi.approve(agent.id, resolvedCompanyId ?? undefined);
+ case "terminate": return agentsApi.terminate(agent.id, resolvedCompanyId ?? undefined);
+ }
+ },
+ onSuccess: (data, action) => {
+ onActionError?.(null);
+ invalidateAgent();
+ if (action === "invoke" && navigateToRunOnInvoke && data && typeof data === "object" && "id" in data) {
+ navigate(`/agents/${canonicalAgentRef}/runs/${(data as HeartbeatRun).id}`);
+ }
+ },
+ onError: (err) => {
+ reportError(err instanceof Error ? err.message : "Action failed");
+ },
+ });
+
+ const duplicateAgent = useMutation({
+ mutationFn: async () => {
+ if (!resolvedCompanyId) {
+ throw new Error("Agent is not ready to duplicate");
+ }
+ const instructionsBundle = await loadDuplicateInstructionsBundle(agent.id, resolvedCompanyId);
+ const payload = buildDuplicateAgentPayload(agent, instructionsBundle);
+ try {
+ return await agentsApi.create(resolvedCompanyId, payload);
+ } catch (error) {
+ if (error instanceof ApiError && error.status === 409 && error.message.includes("requires board approval")) {
+ const hire = await agentsApi.hire(resolvedCompanyId, payload);
+ return hire.agent;
+ }
+ throw error;
+ }
+ },
+ onSuccess: async (createdAgent) => {
+ onActionError?.(null);
+ if (resolvedCompanyId) {
+ await queryClient.invalidateQueries({ queryKey: queryKeys.agents.list(resolvedCompanyId) });
+ }
+ pushToast({ title: "Agent duplicated", body: createdAgent.name, tone: "success" });
+ navigate(`/agents/${agentRouteRef(createdAgent)}/dashboard`);
+ },
+ onError: (err) => {
+ const message = err instanceof Error ? err.message : "Failed to duplicate agent";
+ reportError(message);
+ pushToast({ title: "Could not duplicate agent", body: message, tone: "error" });
+ },
+ });
+
+ const handleDuplicateAgent = useCallback(() => {
+ if (duplicateAgent.isPending) return;
+ const nextName = duplicateAgentName(agent.name);
+ const confirmed = window.confirm(`Duplicate ${agent.name} as ${nextName}?`);
+ setMoreOpen(false);
+ if (!confirmed) return;
+ duplicateAgent.mutate();
+ }, [agent.name, duplicateAgent]);
+
+ const resetTaskSession = useMutation({
+ mutationFn: () => agentsApi.resetSession(agent.id, null, resolvedCompanyId ?? undefined),
+ onSuccess: () => {
+ onActionError?.(null);
+ queryClient.invalidateQueries({ queryKey: queryKeys.agents.runtimeState(agent.id) });
+ queryClient.invalidateQueries({ queryKey: queryKeys.agents.taskSessions(agent.id) });
+ },
+ onError: (err) => {
+ reportError(err instanceof Error ? err.message : "Failed to reset session");
+ },
+ });
+
+ const isPendingApproval = agent.status === "pending_approval";
+ const disabled = actionsDisabled || agentAction.isPending;
+
+ return (
+
+
+
agentAction.mutate("invoke")}
+ disabled={disabled || isPendingApproval}
+ label={runLabel}
+ size={size}
+ />
+ agentAction.mutate("pause")}
+ onResume={() => agentAction.mutate("resume")}
+ disabled={disabled || isPendingApproval}
+ size={size}
+ />
+ {showStatus && (
+
+
+
+ )}
+ {children}
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/ui/src/components/SidebarAgents.test.tsx b/ui/src/components/SidebarAgents.test.tsx
index 82cd7a48e0f..30295cc424b 100644
--- a/ui/src/components/SidebarAgents.test.tsx
+++ b/ui/src/components/SidebarAgents.test.tsx
@@ -186,10 +186,19 @@ async function chooseSortMode(label: string) {
function agentLinkLabels(container: HTMLElement) {
return Array.from(container.querySelectorAll('a[href^="/agents/"]'))
+ .filter((anchor) => anchor.getAttribute("href") !== "/agents/all")
.map((anchor) => anchor.textContent?.trim())
.filter(Boolean);
}
+function seeAllAgentsLink(container: HTMLElement) {
+ return (
+ Array.from(container.querySelectorAll('a[href="/agents/all"]')).find((anchor) =>
+ anchor.textContent?.includes("See all agents"),
+ ) ?? null
+ );
+}
+
describe("SidebarAgents", () => {
let container: HTMLDivElement;
let root: ReturnType | null;
@@ -467,6 +476,42 @@ describe("SidebarAgents", () => {
expect(document.body.textContent).not.toContain("Updating...");
});
+ it("shows only active agents when any agent has a live run", async () => {
+ mockAgentsApi.list.mockResolvedValue([
+ makeAgent({ id: "agent-a", name: "Alpha", urlKey: "alpha" }),
+ makeAgent({ id: "agent-b", name: "Bravo", urlKey: "bravo" }),
+ makeAgent({ id: "agent-c", name: "Charlie", urlKey: "charlie" }),
+ ]);
+ mockHeartbeatsApi.liveRunsForCompany.mockResolvedValue([
+ { id: "run-1", agentId: "agent-b", status: "running" },
+ ]);
+
+ await renderSidebarAgents();
+
+ const labels = agentLinkLabels(container);
+ expect(labels).toHaveLength(1);
+ expect(labels[0]).toContain("Bravo");
+ expect(seeAllAgentsLink(container)).toBeNull();
+ });
+
+ it("shows up to 5 recently-active agents plus a See all link when none are running", async () => {
+ mockAgentsApi.list.mockResolvedValue(
+ Array.from({ length: 7 }, (_, index) =>
+ makeAgent({
+ id: `agent-${index}`,
+ name: `Agent ${index}`,
+ urlKey: `agent-${index}`,
+ }),
+ ),
+ );
+ mockHeartbeatsApi.liveRunsForCompany.mockResolvedValue([]);
+
+ await renderSidebarAgents();
+
+ expect(agentLinkLabels(container)).toHaveLength(5);
+ expect(seeAllAgentsLink(container)?.getAttribute("href")).toBe("/agents/all");
+ });
+
it("does not offer sidebar resume for budget-paused agents", async () => {
mockAgentsApi.list.mockResolvedValue([
makeAgent({
diff --git a/ui/src/components/SidebarAgents.tsx b/ui/src/components/SidebarAgents.tsx
index 02171d92d02..a2f06b34801 100644
--- a/ui/src/components/SidebarAgents.tsx
+++ b/ui/src/components/SidebarAgents.tsx
@@ -44,6 +44,12 @@ import {
} from "@/components/ui/dropdown-menu";
import type { Agent } from "@paperclipai/shared";
+/**
+ * When no agent is running, the sidebar falls back to showing at most this many
+ * recently-active agents plus a "See all agents" link (IA Phase 5).
+ */
+const RECENT_AGENT_LIMIT = 5;
+
const AGENT_SORT_CHOICES: SidebarSectionRadioChoice[] = [
{ value: "top", label: "Top" },
{ value: "alphabetical", label: "Alphabetical" },
@@ -277,6 +283,18 @@ export function SidebarAgents() {
[orderedAgents, sortMode],
);
+ // IA Phase 5: if any agent has a live run, show only those active agents.
+ // Otherwise fall back to up to RECENT_AGENT_LIMIT agents plus "See all agents".
+ const runningAgents = useMemo(
+ () => sortedAgents.filter((agent: Agent) => (liveCountByAgent.get(agent.id) ?? 0) > 0),
+ [sortedAgents, liveCountByAgent],
+ );
+ const hasActiveAgents = runningAgents.length > 0;
+ const displayedAgents = hasActiveAgents
+ ? runningAgents
+ : sortedAgents.slice(0, RECENT_AGENT_LIMIT);
+ const showSeeAllLink = !hasActiveAgents && sortedAgents.length > 0;
+
const agentMatch = location.pathname.match(/^\/(?:[^/]+\/)?agents\/([^/]+)(?:\/([^/]+))?/);
const activeAgentId = agentMatch?.[1] ?? null;
const activeTab = agentMatch?.[2] ?? null;
@@ -406,7 +424,7 @@ export function SidebarAgents() {
onRadioValueChange: persistSortMode,
}}
>
- {sortedAgents.map((agent: Agent) => {
+ {displayedAgents.map((agent: Agent) => {
const runCount = liveCountByAgent.get(agent.id) ?? 0;
return (
);
})}
+ {showSeeAllLink && (
+ {
+ if (isMobile) setSidebarOpen(false);
+ }}
+ className="flex items-center gap-2.5 px-3 py-1.5 pointer-coarse:py-1 text-[13px] font-medium text-muted-foreground transition-colors hover:bg-accent/50 hover:text-foreground"
+ >
+
+ See all agents
+
+ )}
);
}
diff --git a/ui/src/pages/AgentDetail.tsx b/ui/src/pages/AgentDetail.tsx
index 8bbf3cf9635..a88bc1668f9 100644
--- a/ui/src/pages/AgentDetail.tsx
+++ b/ui/src/pages/AgentDetail.tsx
@@ -20,7 +20,6 @@ import { usePanel } from "../context/PanelContext";
import { useSidebar } from "../context/SidebarContext";
import { useCompany } from "../context/CompanyContext";
import { useToastActions } from "../context/ToastContext";
-import { useDialogActions } from "../context/DialogContext";
import { useBreadcrumbs } from "../context/BreadcrumbContext";
import { queryKeys } from "../lib/queryKeys";
import { AgentConfigForm } from "../components/AgentConfigForm";
@@ -40,7 +39,7 @@ import { EntityRow } from "../components/EntityRow";
import { MembershipAction } from "../components/MembershipAction";
import { Identity } from "../components/Identity";
import { PageSkeleton } from "../components/PageSkeleton";
-import { RunButton, PauseResumeButton } from "../components/AgentActionButtons";
+import { AgentActionButtons } from "../components/AgentActionButtons";
import { BudgetPolicyCard } from "../components/BudgetPolicyCard";
import { TrustPresetSection } from "../components/TrustPresetSection";
import { FileTree, buildFileTree } from "../components/FileTree";
@@ -52,18 +51,11 @@ import { buildSameOriginWebSocketUrl } from "../lib/websocket-url";
import { formatCents, formatDate, relativeTime, formatTokens, visibleRunCostUsd } from "../lib/utils";
import { cn } from "../lib/utils";
import { describeRunRetryState } from "../lib/runRetryState";
-import { buildDuplicateAgentPayload, duplicateAgentName, type DuplicateInstructionsBundle } from "../lib/duplicate-agent-payload";
import { Button } from "@/components/ui/button";
import { Skeleton } from "@/components/ui/skeleton";
import { Tabs } from "@/components/ui/tabs";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
import {
- Popover,
- PopoverContent,
- PopoverTrigger,
-} from "@/components/ui/popover";
-import {
- MoreHorizontal,
CheckCircle2,
XCircle,
Clock,
@@ -71,7 +63,6 @@ import {
Loader2,
Slash,
RotateCcw,
- Trash2,
Plus,
Key,
Eye,
@@ -91,8 +82,6 @@ import { RunTranscriptView, type TranscriptMode } from "../components/transcript
import {
isUuidLike,
type Agent,
- type AgentInstructionsBundle,
- type AgentInstructionsFileSummary,
type AgentSkillEntry,
type AgentSkillSnapshot,
type AgentDetail as AgentDetailRecord,
@@ -117,34 +106,6 @@ import {
isReadOnlyUnmanagedSkillEntry,
} from "../lib/agent-skills-state";
-async function loadDuplicateInstructionsBundle(
- agentId: string,
- companyId?: string,
-): Promise {
- const bundle = await agentsApi.instructionsBundle(agentId, companyId);
- const files: Record = {};
-
- for (const summary of bundle.files) {
- const path = duplicateInstructionFilePath(bundle, summary);
- if (!path) continue;
- const file = await agentsApi.instructionsFile(agentId, summary.path, companyId);
- files[path] = file.content;
- }
-
- const entryFile = Object.prototype.hasOwnProperty.call(files, bundle.entryFile)
- ? bundle.entryFile
- : Object.keys(files)[0] ?? "AGENTS.md";
- return Object.keys(files).length > 0 ? { entryFile, files } : null;
-}
-
-function duplicateInstructionFilePath(
- _bundle: AgentInstructionsBundle,
- summary: AgentInstructionsFileSummary,
-): string | null {
- if (summary.deprecated || summary.virtual) return null;
- return summary.path;
-}
-
const runStatusIcons: Record = {
succeeded: { icon: CheckCircle2, color: "text-green-600 dark:text-green-400" },
failed: { icon: XCircle, color: "text-red-600 dark:text-red-400" },
@@ -678,14 +639,11 @@ export function AgentDetail() {
}>();
const { companies, selectedCompanyId, setSelectedCompanyId } = useCompany();
const { closePanel } = usePanel();
- const { openNewIssue } = useDialogActions();
- const { pushToast } = useToastActions();
const { setBreadcrumbs } = useBreadcrumbs();
const queryClient = useQueryClient();
const navigate = useNavigate();
const [actionError, setActionError] = useState(null);
const [dismissedLeftAgentIds, setDismissedLeftAgentIds] = useState>(() => new Set());
- const [moreOpen, setMoreOpen] = useState(false);
const activeView = urlRunId ? "runs" as AgentDetailView : parseAgentDetailView(urlTab ?? null);
const needsDashboardData = activeView === "dashboard";
const needsRunData = activeView === "runs" || Boolean(urlRunId);
@@ -824,18 +782,17 @@ export function AgentDetail() {
setSelectedCompanyId(agent.companyId, { source: "route_sync" });
}, [agent?.companyId, selectedCompanyId, setSelectedCompanyId]);
+ // Invoke / pause / resume / terminate / duplicate / reset live in the shared
+ // AgentActionButtons component. The detail header keeps only "approve" here,
+ // which is surfaced via the pending-approval banner below.
const agentAction = useMutation({
- mutationFn: async (action: "invoke" | "pause" | "resume" | "approve" | "terminate") => {
+ mutationFn: async (action: "approve") => {
if (!agentLookupRef) return Promise.reject(new Error("No agent reference"));
- switch (action) {
- case "invoke": return agentsApi.invoke(agentLookupRef, resolvedCompanyId ?? undefined);
- case "pause": return agentsApi.pause(agentLookupRef, resolvedCompanyId ?? undefined);
- case "resume": return agentsApi.resume(agentLookupRef, resolvedCompanyId ?? undefined);
- case "approve": return agentsApi.approve(agentLookupRef, resolvedCompanyId ?? undefined);
- case "terminate": return agentsApi.terminate(agentLookupRef, resolvedCompanyId ?? undefined);
+ if (action === "approve") {
+ return agentsApi.approve(agentLookupRef, resolvedCompanyId ?? undefined);
}
},
- onSuccess: (data, action) => {
+ onSuccess: () => {
setActionError(null);
queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(routeAgentRef) });
queryClient.invalidateQueries({ queryKey: queryKeys.agents.detail(agentLookupRef) });
@@ -847,66 +804,12 @@ export function AgentDetail() {
queryClient.invalidateQueries({ queryKey: queryKeys.heartbeats(resolvedCompanyId, agent.id) });
}
}
- if (action === "invoke" && data && typeof data === "object" && "id" in data) {
- navigate(`/agents/${canonicalAgentRef}/runs/${(data as HeartbeatRun).id}`);
- }
},
onError: (err) => {
setActionError(err instanceof Error ? err.message : "Action failed");
},
});
- const duplicateAgent = useMutation({
- mutationFn: async () => {
- if (!agent?.id || !resolvedCompanyId) {
- throw new Error("Agent is not ready to duplicate");
- }
-
- const instructionsBundle = await loadDuplicateInstructionsBundle(agent.id, resolvedCompanyId);
- const payload = buildDuplicateAgentPayload(agent, instructionsBundle);
-
- try {
- return await agentsApi.create(resolvedCompanyId, payload);
- } catch (error) {
- if (error instanceof ApiError && error.status === 409 && error.message.includes("requires board approval")) {
- const hire = await agentsApi.hire(resolvedCompanyId, payload);
- return hire.agent;
- }
- throw error;
- }
- },
- onSuccess: async (createdAgent) => {
- setActionError(null);
- if (resolvedCompanyId) {
- await queryClient.invalidateQueries({ queryKey: queryKeys.agents.list(resolvedCompanyId) });
- }
- pushToast({
- title: "Agent duplicated",
- body: createdAgent.name,
- tone: "success",
- });
- navigate(`/agents/${agentRouteRef(createdAgent)}/dashboard`);
- },
- onError: (err) => {
- const message = err instanceof Error ? err.message : "Failed to duplicate agent";
- setActionError(message);
- pushToast({
- title: "Could not duplicate agent",
- body: message,
- tone: "error",
- });
- },
- });
-
- const handleDuplicateAgent = useCallback(() => {
- if (!agent || duplicateAgent.isPending) return;
- const nextName = duplicateAgentName(agent.name);
- const confirmed = window.confirm(`Duplicate ${agent.name} as ${nextName}?`);
- setMoreOpen(false);
- if (!confirmed) return;
- duplicateAgent.mutate();
- }, [agent, duplicateAgent]);
-
const budgetMutation = useMutation({
mutationFn: (amount: number) =>
budgetsApi.upsertPolicy(resolvedCompanyId!, {
@@ -936,19 +839,6 @@ export function AgentDetail() {
},
});
- const resetTaskSession = useMutation({
- mutationFn: (taskKey: string | null) =>
- agentsApi.resetSession(agentLookupRef, taskKey, resolvedCompanyId ?? undefined),
- onSuccess: () => {
- setActionError(null);
- queryClient.invalidateQueries({ queryKey: queryKeys.agents.runtimeState(agentLookupRef) });
- queryClient.invalidateQueries({ queryKey: queryKeys.agents.taskSessions(agentLookupRef) });
- },
- onError: (err) => {
- setActionError(err instanceof Error ? err.message : "Failed to reset session");
- },
- });
-
const updatePermissions = useMutation({
mutationFn: (permissions: AgentPermissionUpdate) =>
agentsApi.updatePermissions(agentLookupRef, permissions, resolvedCompanyId ?? undefined),
@@ -1086,27 +976,14 @@ export function AgentDetail() {
-
-
-
agentAction.mutate("invoke")}
- disabled={agentAction.isPending || isPendingApproval}
- label="Run Heartbeat"
- />
- agentAction.mutate("pause")}
- onResume={() => agentAction.mutate("resume")}
- disabled={agentAction.isPending || isPendingApproval}
- />
-
+
{mobileLiveRun && (
Live
)}
-
- {/* Overflow menu */}
-
-
-
-
-
-
-
-
-
-
-
-
+
{!urlRunId && (
@@ -1454,10 +1278,10 @@ function AgentOverview({
-
+
-
+
@@ -1468,7 +1292,7 @@ function AgentOverview({
{/* Recent Issues */}
-
Recent Issues
+ Recent Tasks
{assignedIssues.length === 0 ? (
-
No recent issues.
+
No recent tasks.
) : (
{assignedIssues.slice(0, 10).map((issue) => (
@@ -1491,7 +1315,7 @@ function AgentOverview({
))}
{assignedIssues.length > 10 && (
- +{assignedIssues.length - 10} more issues
+ +{assignedIssues.length - 10} more tasks
)}
@@ -3651,7 +3475,7 @@ function RunDetail({ run: initialRun, agentRouteId, adapterType, adapterConfig }
>
{clearSessionsForTouchedIssues.isPending
? "clearing session..."
- : "clear session for these issues"}
+ : "clear session for these tasks"}
{clearSessionsForTouchedIssues.isError && (
@@ -3671,7 +3495,7 @@ function RunDetail({ run: initialRun, agentRouteId, adapterType, adapterConfig }
{/* Issues touched by this run */}
{touchedIssues && touchedIssues.length > 0 && (
-
Issues Touched ({touchedIssues.length})
+
Tasks Touched ({touchedIssues.length})
{touchedIssues.map((issue) => (
+ {/* Row actions mirror the agent detail page; stop the click
+ from bubbling to the row link so buttons don't navigate. */}
+
{
+ e.preventDefault();
+ e.stopPropagation();
+ }}
+ >
+
+