fix(ui): remove action buttons from agent list (#13101)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work.
> - The agents list helps operators scan names, roles, and status.
> - Each row also shows task, heartbeat, trace, pause, and overflow
controls.
> - These controls add a second line to every row and crowd the list.
> - This pull request removes the action bar from list rows.
> - Operators can still use agent detail pages for those actions.

## Linked Issues or Issue Description

Related: #7543 introduced list actions. #13011 moved the row actions
onto a separate line. No duplicate fix was found.

**What happened?**

The `/agents/all` list shows Assign Task, Run Heartbeat, Run with
provider trace, Pause or Resume, and an overflow menu below each agent.

**Expected behavior**

The agent list should show agent information without this action bar.

**Steps to reproduce**

1. Open `/agents/all` in an organization with agents.
2. Select the list view.
3. Inspect the controls below each agent name.

**Paperclip version or commit**

Reproduced in source at fac07b42a.

**Deployment mode**

Board UI. The change is independent of deployment mode.

## What Changed

- Removed AgentActionButtons from both agent list implementations,
including filtered lists.
- Removed the unused board-access query and trace permission
calculation.
- Kept built-in setup controls, status, and membership actions.
- Enabled the normal Run now control on the streamlined agent detail
page so operators can still trigger a heartbeat there.
- Updated the existing mobile row test and added active/paused row
coverage for both list implementations.

## Verification

- Passed: `pnpm check:token-gates`.
- Passed: `pnpm exec vitest run ui/src/pages/Agents.test.tsx
ui/src/components/AgentActionButtons.test.tsx` (27 tests).
- Passed: `pnpm -r typecheck` and `pnpm build`. After the review fixes,
the UI typecheck, UI build, token gates, and targeted tests passed
again.
- Full CI passed on 6d527b02a, including all server/workspace test
shards, browser tests, typecheck, build, and canary dry run.
- Local `pnpm test:run` passed 520 server suites (7,234 tests). The
later UI batch had two Inbox failures under load; an isolated Inbox
rerun passed all 27 tests. Stopped the redundant local full run after
all CI shards passed.
- The CI build first hit an unrelated port collision in a
Codex-credentials test. One rerun passed runner verification and the
full build.
- Greptile: 5/5. Both review threads are resolved.
- Manual review: open the agent list and verify the action bar is
absent. Open an agent to access its actions.

## Risks

- Low risk. This changes agent list rendering and restores the normal
detail-page run control. Operators must open an agent to use the removed
shortcuts.
- No API, database, or permission contracts change.

## Model Used

- OpenAI GPT-6 (Codex). Exact deployment model ID and context window
size are not exposed in this session. Used reasoning, repository
inspection, code editing, and command execution.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Dotta 2026-09-09 12:05:42 -05:00 committed by GitHub
parent fac07b42ad
commit 622376e995
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 44 additions and 63 deletions

View File

@ -1264,7 +1264,6 @@ export function AgentDetail() {
agent={agent}
companyId={resolvedCompanyId}
assignLabel="Assign Task"
showRun={false}
showStatus={false}
canRunWithProviderTrace={canUseProviderTrace}
actionsDisabled={agentAction.isPending}

View File

@ -13,7 +13,6 @@ import { useSidebar } from "../context/SidebarContext";
import { queryKeys } from "../lib/queryKeys";
import { isPlatformManagedEnvironment } from "../lib/managed-sandbox-environment";
import { AgentStatusBadge, AgentStatusCapsule } from "../components/StatusBadge";
import { AgentActionButtons } from "../components/AgentActionButtons";
import { MembershipAction } from "../components/MembershipAction";
import { StarToggle } from "../components/StarToggle";
import { EntityRow } from "../components/EntityRow";
@ -442,22 +441,6 @@ export function Agents() {
<span className="w-20 flex justify-end">
<AgentStatusBadge status={agent.status} />
</span>
{/* Row actions mirror the agent detail page; stop the click
from bubbling to the row link so buttons don't navigate.
Hidden on mobile so the agent name keeps room to render. */}
<div
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
>
<AgentActionButtons
agent={agent}
companyId={selectedCompanyId}
runLabel="Run Heartbeat"
showStatus={false}
/>
</div>
<StarToggle
size="row"
starred={agentStarred}

View File

@ -9,6 +9,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { ToastProvider } from "../context/ToastContext";
import type { BuiltInAgentState } from "../api/builtInAgents";
import { Agents } from "./Agents";
import { Agents as ProductionAgents } from "./Agents.production";
import type { AgentOrgChainHealth } from "@paperclipai/shared";
const mockRouterState = vi.hoisted(() => ({
@ -357,6 +358,40 @@ describe("Agents", () => {
vi.clearAllMocks();
});
it.each([
["streamlined", Agents],
["production", ProductionAgents],
] as const)("omits the action bar from %s agent list rows", async (_mode, AgentList) => {
mockAgentsApi.list.mockResolvedValue([
makeAgent({ name: "Alpha", status: "active" }),
makeAgent({ id: "agent-paused", name: "Paused agent", status: "paused" }),
]);
root = createRoot(container);
await act(async () => {
root!.render(
<QueryClientProvider client={queryClient}>
<ToastProvider>
<AgentList />
</ToastProvider>
</QueryClientProvider>,
);
});
await flushReact();
await flushReact();
const listToggle = container.querySelector<HTMLButtonElement>('button[aria-label="List view"]');
await act(async () => { listToggle?.click(); });
await flushReact();
for (const name of ["Alpha", "Paused agent"]) {
const row = findAgentRow(container, name);
expect(row).not.toBeNull();
expect(row?.getAttribute("href")).toMatch(/^\/agents\//);
expect(row?.querySelector('button[aria-label^="Open actions for"]')).toBeNull();
const buttons = Array.from(row?.querySelectorAll("button") ?? []).map((button) => button.textContent);
expect(buttons).not.toEqual(expect.arrayContaining([expect.stringMatching(/Assign Task|Run Heartbeat|Run with provider trace|Pause|Resume/)]));
}
});
it("shows the configured model beside the adapter on the all agents page", async () => {
root = createRoot(container);
await act(async () => {
@ -479,15 +514,11 @@ describe("Agents", () => {
expect(subtitle).toBeDefined();
expect(subtitle?.classList.contains("truncate")).toBe(true);
const actions = row?.querySelector('button[aria-label="Open actions for Paperclip Engineer With A Much Longer Display Name"]');
expect(actions).not.toBeNull();
// Neither the action button nor its ancestors may hide the mobile menu.
for (let node = actions; node && node !== row; node = node.parentElement) {
expect(node.classList.contains("hidden")).toBe(false);
}
await act(async () => { (actions as HTMLButtonElement).click(); });
await flushReact();
expect(document.body.textContent).toContain("Duplicate");
expect(document.body.textContent).toContain("Terminate");
expect(actions).toBeNull();
expect(row?.textContent).not.toContain("Assign Task");
expect(row?.textContent).not.toContain("Run Heartbeat");
expect(row?.textContent).not.toContain("Run with provider trace");
expect(row?.textContent).not.toContain("Pause");
});
it("uses the built-in agents route segment as the built-in filter", async () => {

View File

@ -6,7 +6,6 @@ import { builtInAgentsApi, type BuiltInAgentState } from "../api/builtInAgents";
import { environmentsApi } from "../api/environments";
import { heartbeatsApi } from "../api/heartbeats";
import { instanceSettingsApi } from "../api/instanceSettings";
import { accessApi } from "../api/access";
import { useCompany } from "../context/CompanyContext";
import { useDialogActions } from "../context/DialogContext";
import { useBreadcrumbs } from "../context/BreadcrumbContext";
@ -15,7 +14,6 @@ import { useStreamlinedUiEnabled } from "../hooks/useStreamlinedUiEnabled";
import { queryKeys } from "../lib/queryKeys";
import { isPlatformManagedEnvironment } from "../lib/managed-sandbox-environment";
import { AgentStatusBadge, AgentStatusCapsule } from "../components/StatusBadge";
import { AgentActionButtons } from "../components/AgentActionButtons";
import { MembershipAction } from "../components/MembershipAction";
import { StarToggle } from "../components/StarToggle";
import { EntityRow } from "../components/EntityRow";
@ -211,15 +209,6 @@ export function Agents({ initialView = "list" }: { initialView?: AgentsView } =
setView(streamlinedUiEnabled ? initialView : "org");
}, [initialView, streamlinedUiEnabled]);
const { data: boardAccess } = useQuery({
queryKey: queryKeys.access.currentBoardAccess,
queryFn: () => accessApi.getCurrentBoardAccess(),
retry: false,
});
const canUseProviderTrace =
boardAccess?.source === "local_implicit" ||
boardAccess?.isInstanceAdmin === true;
const { data: instanceSettings } = useQuery({
queryKey: queryKeys.instance.settings,
queryFn: () => instanceSettingsApi.get(),
@ -411,32 +400,11 @@ export function Agents({ initialView = "list" }: { initialView?: AgentsView } =
) : (
<AgentStatusCapsule status={agent.status} />
)}
secondaryRow={
<div className="flex flex-col gap-2">
{builtInCluster && (
<div className="@5xl:hidden flex flex-wrap items-center gap-1.5">
{builtInCluster}
</div>
)}
{/* Actions have their own wrapping line, so names keep their width. */}
<div
className="pt-1"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
}}
>
<AgentActionButtons
agent={agent}
companyId={selectedCompanyId}
runLabel="Run Heartbeat"
showStatus={false}
canRunWithProviderTrace={canUseProviderTrace}
className="flex flex-wrap items-center gap-2"
/>
</div>
secondaryRow={builtInCluster && (
<div className="@5xl:hidden flex flex-wrap items-center gap-1.5">
{builtInCluster}
</div>
}
)}
meta={
<div className="flex items-center gap-3">
{builtInCluster && (