mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-29 12:59:13 +01:00
<!-- Write all pull request text in Simplified Technical English (ASD-STE100): short sentences, one instruction per sentence, simple approved vocabulary, and the active voice. --> ## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - The agent detail page shows an overview of one agent in the board UI > - The streamlined shell shows the agent contextual sidebar beside this page > - The sidebar has an Audit group with Activity, Runs, Costs, and Budgets links > - The overview also ends with an Audit card grid that has the same four links > - The duplicate links add noise and make the overview longer with no benefit > - This pull request removes the Audit card grid from the agent overview > - The benefit is a shorter overview with one clear place for audit navigation ## Linked Issues or Issue Description **What existing behavior does this improve?** The agent overview page shows a section of Audit link cards at the end of the page. **Current behavior** The overview shows four cards: Activity, Runs, Costs, and Budgets. Each card opens the company audit page, filtered to the agent. The agent contextual sidebar shows the same four links. Both use `agentScopedAuditHref`. #12746 added the sidebar links. #12747 added the overview cards in the same PR stack. **Proposed behavior** Remove the overview cards. Keep the sidebar Audit group as the single entry point. **Reason and benefit** Operators see each audit link one time. The overview stays focused on agent status and recent tasks. ## What Changed - Remove the Audit card grid from the agent overview in `ui/src/pages/AgentDetail.tsx`. Before: <img width="1488" height="377" alt="Screenshot 2026-10-27 at 5 33 29 PM" src="https://github.com/user-attachments/assets/fd27fee2-63c4-420e-8b39-405e4a633f63" /> After: <img width="1526" height="271" alt="Screenshot 2026-10-27 at 5 33 41 PM" src="https://github.com/user-attachments/assets/bca0635d-3be6-42cd-b1d6-dc36b89ae1b0" /> ## Verification - Run `pnpm --filter ./ui exec tsc --noEmit`. The result is clean. - Run `pnpm --filter ./ui exec vitest run src/pages/AgentDetail`. All 7 files and 56 tests pass. - Run `pnpm check:token-gates`. All gates are clean. - Manual check: opened an agent overview page. Made sure that the Audit cards are not at the end of the page. Made sure that the sidebar Audit links still open the agent-filtered audit pages. ## Risks - Low risk. The change only removes UI markup. - The agent-scoped audit routes and the legacy URL redirects do not change. - Operators who used the overview cards must use the sidebar Audit links. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used - Anthropic Claude Opus 5.5 (`claude-opus-5-5`) in Claude Code. The model used tool use, code search, git history inspection, and local test execution. Reasoning mode: adaptive thinking. ## 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 - [ ] 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: Claude Opus 5.5 <noreply@anthropic.com>
177 lines
6.6 KiB
TypeScript
177 lines
6.6 KiB
TypeScript
// @vitest-environment jsdom
|
|
|
|
import { renderToStaticMarkup } from "react-dom/server";
|
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
import type { AgentDetail, AgentRuntimeState, HeartbeatRun, Issue } from "@paperclipai/shared";
|
|
import { describe, expect, it, vi } from "vitest";
|
|
import { AgentOverview } from "./AgentDetail";
|
|
import { queryKeys } from "../lib/queryKeys";
|
|
|
|
vi.mock("@/lib/router", async () => {
|
|
const actual = await vi.importActual<typeof import("react-router-dom")>("react-router-dom");
|
|
return {
|
|
...actual,
|
|
Link: ({ to, children, ...props }: { to: string; children: React.ReactNode }) => (
|
|
<a href={to} {...props}>{children}</a>
|
|
),
|
|
};
|
|
});
|
|
|
|
vi.mock("../components/MarkdownBody", () => ({
|
|
MarkdownBody: ({ children }: { children: string }) => <div>{children}</div>,
|
|
}));
|
|
|
|
describe("AgentOverview", () => {
|
|
it("identifies Muse and shows its provider-managed model even when old model fields remain", () => {
|
|
const agent = {
|
|
id: "muse-agent", companyId: "company-1", name: "Muse Researcher", role: "researcher", status: "active",
|
|
adapterType: "paperclip_runner", adapterConfig: { provider: "muse", model: "old-model" },
|
|
runtimeConfig: { model: "old-runtime-model" },
|
|
} as unknown as AgentDetail;
|
|
const markup = renderToStaticMarkup(
|
|
<QueryClientProvider client={new QueryClient()}>
|
|
<AgentOverview agent={agent} runs={[]} assignedIssues={[]} directReportCount={0} skillNames={[]} agentRouteId="muse-agent" />
|
|
</QueryClientProvider>,
|
|
);
|
|
expect(markup).toContain("Muse — Personal agent");
|
|
expect(markup).toContain("Managed by Muse");
|
|
expect(markup).not.toContain("Adapter default");
|
|
expect(markup).not.toContain("old-model");
|
|
expect(markup).not.toContain("old-runtime-model");
|
|
});
|
|
|
|
it("prioritizes identity, capability, runtime, skills, and tasks without duplicating sidebar Audit links", () => {
|
|
const agent = {
|
|
id: "agent-1",
|
|
companyId: "company-1",
|
|
name: "Codex Coder",
|
|
urlKey: "codexcoder",
|
|
role: "engineer",
|
|
title: "Product engineer",
|
|
status: "active",
|
|
reportsTo: null,
|
|
capabilities: "Builds and verifies product changes.",
|
|
adapterType: "codex_local",
|
|
adapterConfig: { model: "gpt-5.6-sol" },
|
|
runtimeConfig: {},
|
|
chainOfCommand: [],
|
|
access: { canAssignTasks: true, taskAssignSource: "explicit_grant", membership: null, grants: [] },
|
|
} as unknown as AgentDetail;
|
|
const issue = {
|
|
id: "issue-1",
|
|
companyId: "company-1",
|
|
identifier: "PAP-42",
|
|
title: "Simplify agent information architecture",
|
|
status: "in_progress",
|
|
priority: "high",
|
|
updatedAt: new Date("2026-08-31T12:00:00Z"),
|
|
} as unknown as Issue;
|
|
const runtime = {
|
|
sessionDisplayId: "codex-session-42",
|
|
} as unknown as AgentRuntimeState;
|
|
|
|
const markup = renderToStaticMarkup(
|
|
<QueryClientProvider client={new QueryClient()}>
|
|
<AgentOverview
|
|
agent={agent}
|
|
runs={[] as HeartbeatRun[]}
|
|
assignedIssues={[issue]}
|
|
runtimeState={runtime}
|
|
directReportCount={2}
|
|
skillNames={["Design Guide", "Check PR"]}
|
|
agentRouteId="codexcoder"
|
|
/>
|
|
</QueryClientProvider>,
|
|
);
|
|
|
|
expect(markup).toContain("Identity");
|
|
expect(markup).toContain("Capabilities");
|
|
expect(markup).toContain("Harness / Runtime");
|
|
expect(markup).toContain("Design Guide");
|
|
expect(markup).toContain("Simplify agent information architecture");
|
|
expect(markup).toContain("PAP-42");
|
|
expect(markup).not.toContain("agent-audit-links-heading");
|
|
expect(markup).not.toContain("agentId=agent-1");
|
|
expect(markup).not.toContain("Run Activity");
|
|
expect(markup).not.toContain("Tasks by Status");
|
|
expect(markup).not.toContain("Created by");
|
|
});
|
|
|
|
function renderOverview(createdByUserId: string | null, client: QueryClient) {
|
|
const agent = {
|
|
id: "agent-2",
|
|
companyId: "company-1",
|
|
name: "Shared Agent",
|
|
urlKey: "sharedagent",
|
|
role: "engineer",
|
|
title: null,
|
|
status: "idle",
|
|
reportsTo: null,
|
|
capabilities: null,
|
|
adapterType: "codex_local",
|
|
adapterConfig: {},
|
|
runtimeConfig: {},
|
|
createdByUserId,
|
|
chainOfCommand: [],
|
|
access: { canAssignTasks: true, taskAssignSource: "explicit_grant", membership: null, grants: [] },
|
|
} as unknown as AgentDetail;
|
|
return renderToStaticMarkup(
|
|
<QueryClientProvider client={client}>
|
|
<AgentOverview
|
|
agent={agent}
|
|
runs={[] as HeartbeatRun[]}
|
|
assignedIssues={[]}
|
|
directReportCount={0}
|
|
skillNames={[]}
|
|
agentRouteId="sharedagent"
|
|
/>
|
|
</QueryClientProvider>,
|
|
);
|
|
}
|
|
|
|
function renderWithDirectory(createdByUserId: string, principalIds: string[]) {
|
|
const client = new QueryClient();
|
|
client.setQueryData(queryKeys.access.companyUserDirectory("company-1"), {
|
|
users: principalIds.map((principalId) => ({
|
|
principalId,
|
|
status: "active",
|
|
user: { id: principalId, name: principalId === "user-alice" ? "Alice Admin" : "Bob Builder", email: null, image: null },
|
|
})),
|
|
});
|
|
return renderOverview(createdByUserId, client);
|
|
}
|
|
|
|
it("shows who created the agent when the company has more than one human user", () => {
|
|
const markup = renderWithDirectory("user-alice", ["user-alice", "user-bob"]);
|
|
expect(markup).toContain("Created by");
|
|
expect(markup).toContain("Alice Admin");
|
|
});
|
|
|
|
it("hides the creator when only one human user shares the company with the Board", () => {
|
|
const markup = renderWithDirectory("user-alice", ["user-alice", "local-board"]);
|
|
expect(markup).not.toContain("Created by");
|
|
});
|
|
|
|
function failedDirectoryClient() {
|
|
const client = new QueryClient({ defaultOptions: { queries: { retryOnMount: false } } });
|
|
client.getQueryCache().build(client, { queryKey: queryKeys.access.companyUserDirectory("company-1") }).setState({
|
|
status: "error",
|
|
error: new Error("Directory unavailable"),
|
|
fetchStatus: "idle",
|
|
});
|
|
return client;
|
|
}
|
|
|
|
it("shows an error when the company user directory cannot be loaded", () => {
|
|
const markup = renderOverview("user-alice", failedDirectoryClient());
|
|
expect(markup).toContain("Created by");
|
|
expect(markup).toContain("Could not load creator");
|
|
});
|
|
|
|
it.each([null, "local-board"])("keeps the creator row hidden for creator %s when the directory failed", (createdByUserId) => {
|
|
const markup = renderOverview(createdByUserId, failedDirectoryClient());
|
|
expect(markup).not.toContain("Created by");
|
|
expect(markup).not.toContain("Could not load creator");
|
|
});
|
|
});
|