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 atfac07b42a. **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 on6d527b02a, 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:
parent
fac07b42ad
commit
622376e995
|
|
@ -1264,7 +1264,6 @@ export function AgentDetail() {
|
|||
agent={agent}
|
||||
companyId={resolvedCompanyId}
|
||||
assignLabel="Assign Task"
|
||||
showRun={false}
|
||||
showStatus={false}
|
||||
canRunWithProviderTrace={canUseProviderTrace}
|
||||
actionsDisabled={agentAction.isPending}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
|
|
|
|||
Loading…
Reference in New Issue