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