UI: approval detail page, agent hiring UX, costs breakdown, sidebar badges, and dashboard improvements

Add ApprovalDetail page with comment thread, revision request/resubmit flow,
and ApprovalPayload component for structured payload display. Extend AgentDetail
with permissions management, config revision history, and duplicate action.
Add agent hire dialog with permission-gated access. Rework Costs page with
per-agent breakdown table and period filtering. Add sidebar badge counts for
pending approvals and inbox items. Enhance Dashboard with live metrics and
sparkline trends. Extend Agents list with pending_approval status and bulk
actions. Update IssueDetail with approval linking. Various component improvements
to MetricCard, InlineEditor, CommentThread, and StatusBadge.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Forgotten
2026-02-19 13:03:08 -06:00
parent 0d73e1b407
commit 176d279403
31 changed files with 1271 additions and 214 deletions

View File

@@ -1,4 +1,11 @@
import type { Agent, AgentKeyCreated, AgentRuntimeState, HeartbeatRun } from "@paperclip/shared";
import type {
Agent,
AgentKeyCreated,
AgentRuntimeState,
HeartbeatRun,
Approval,
AgentConfigRevision,
} from "@paperclip/shared";
import { api } from "./client";
export interface AgentKey {
@@ -21,16 +28,35 @@ export interface OrgNode {
reports: OrgNode[];
}
export interface AgentHireResponse {
agent: Agent;
approval: Approval | null;
}
export const agentsApi = {
list: (companyId: string) => api.get<Agent[]>(`/companies/${companyId}/agents`),
org: (companyId: string) => api.get<OrgNode[]>(`/companies/${companyId}/org`),
listConfigurations: (companyId: string) =>
api.get<Record<string, unknown>[]>(`/companies/${companyId}/agent-configurations`),
get: (id: string) => api.get<Agent>(`/agents/${id}`),
getConfiguration: (id: string) => api.get<Record<string, unknown>>(`/agents/${id}/configuration`),
listConfigRevisions: (id: string) =>
api.get<AgentConfigRevision[]>(`/agents/${id}/config-revisions`),
getConfigRevision: (id: string, revisionId: string) =>
api.get<AgentConfigRevision>(`/agents/${id}/config-revisions/${revisionId}`),
rollbackConfigRevision: (id: string, revisionId: string) =>
api.post<Agent>(`/agents/${id}/config-revisions/${revisionId}/rollback`, {}),
create: (companyId: string, data: Record<string, unknown>) =>
api.post<Agent>(`/companies/${companyId}/agents`, data),
hire: (companyId: string, data: Record<string, unknown>) =>
api.post<AgentHireResponse>(`/companies/${companyId}/agent-hires`, data),
update: (id: string, data: Record<string, unknown>) => api.patch<Agent>(`/agents/${id}`, data),
updatePermissions: (id: string, data: { canCreateAgents: boolean }) =>
api.patch<Agent>(`/agents/${id}/permissions`, data),
pause: (id: string) => api.post<Agent>(`/agents/${id}/pause`, {}),
resume: (id: string) => api.post<Agent>(`/agents/${id}/resume`, {}),
terminate: (id: string) => api.post<Agent>(`/agents/${id}/terminate`, {}),
remove: (id: string) => api.delete<{ ok: true }>(`/agents/${id}`),
listKeys: (id: string) => api.get<AgentKey[]>(`/agents/${id}/keys`),
createKey: (id: string, name: string) => api.post<AgentKeyCreated>(`/agents/${id}/keys`, { name }),
revokeKey: (agentId: string, keyId: string) => api.delete<{ ok: true }>(`/agents/${agentId}/keys/${keyId}`),

View File

@@ -1,4 +1,4 @@
import type { Approval } from "@paperclip/shared";
import type { Approval, ApprovalComment, Issue } from "@paperclip/shared";
import { api } from "./client";
export const approvalsApi = {
@@ -8,8 +8,17 @@ export const approvalsApi = {
),
create: (companyId: string, data: Record<string, unknown>) =>
api.post<Approval>(`/companies/${companyId}/approvals`, data),
get: (id: string) => api.get<Approval>(`/approvals/${id}`),
approve: (id: string, decisionNote?: string) =>
api.post<Approval>(`/approvals/${id}/approve`, { decisionNote }),
reject: (id: string, decisionNote?: string) =>
api.post<Approval>(`/approvals/${id}/reject`, { decisionNote }),
requestRevision: (id: string, decisionNote?: string) =>
api.post<Approval>(`/approvals/${id}/request-revision`, { decisionNote }),
resubmit: (id: string, payload?: Record<string, unknown>) =>
api.post<Approval>(`/approvals/${id}/resubmit`, { payload }),
listComments: (id: string) => api.get<ApprovalComment[]>(`/approvals/${id}/comments`),
addComment: (id: string, body: string) =>
api.post<ApprovalComment>(`/approvals/${id}/comments`, { body }),
listIssues: (id: string) => api.get<Issue[]>(`/approvals/${id}/issues`),
};

View File

@@ -11,7 +11,12 @@ export const companiesApi = {
api.post<Company>("/companies", data),
update: (
companyId: string,
data: Partial<Pick<Company, "name" | "description" | "status" | "budgetMonthlyCents">>,
data: Partial<
Pick<
Company,
"name" | "description" | "status" | "budgetMonthlyCents" | "requireBoardApprovalForNewAgents"
>
>,
) => api.patch<Company>(`/companies/${companyId}`, data),
archive: (companyId: string) => api.post<Company>(`/companies/${companyId}/archive`, {}),
remove: (companyId: string) => api.delete<{ ok: true }>(`/companies/${companyId}`),

View File

@@ -1,4 +1,4 @@
import type { CostSummary } from "@paperclip/shared";
import type { CostSummary, CostByAgent } from "@paperclip/shared";
import { api } from "./client";
export interface CostByEntity {
@@ -9,10 +9,19 @@ export interface CostByEntity {
outputTokens: number;
}
function dateParams(from?: string, to?: string): string {
const params = new URLSearchParams();
if (from) params.set("from", from);
if (to) params.set("to", to);
const qs = params.toString();
return qs ? `?${qs}` : "";
}
export const costsApi = {
summary: (companyId: string) => api.get<CostSummary>(`/companies/${companyId}/costs/summary`),
byAgent: (companyId: string) =>
api.get<CostByEntity[]>(`/companies/${companyId}/costs/by-agent`),
byProject: (companyId: string) =>
api.get<CostByEntity[]>(`/companies/${companyId}/costs/by-project`),
summary: (companyId: string, from?: string, to?: string) =>
api.get<CostSummary>(`/companies/${companyId}/costs/summary${dateParams(from, to)}`),
byAgent: (companyId: string, from?: string, to?: string) =>
api.get<CostByAgent[]>(`/companies/${companyId}/costs/by-agent${dateParams(from, to)}`),
byProject: (companyId: string, from?: string, to?: string) =>
api.get<CostByEntity[]>(`/companies/${companyId}/costs/by-project${dateParams(from, to)}`),
};

View File

@@ -9,3 +9,4 @@ export { costsApi } from "./costs";
export { activityApi } from "./activity";
export { dashboardApi } from "./dashboard";
export { heartbeatsApi } from "./heartbeats";
export { sidebarBadgesApi } from "./sidebarBadges";

View File

@@ -1,4 +1,4 @@
import type { Issue, IssueComment } from "@paperclip/shared";
import type { Approval, Issue, IssueComment } from "@paperclip/shared";
import { api } from "./client";
export const issuesApi = {
@@ -17,4 +17,9 @@ export const issuesApi = {
listComments: (id: string) => api.get<IssueComment[]>(`/issues/${id}/comments`),
addComment: (id: string, body: string, reopen?: boolean) =>
api.post<IssueComment>(`/issues/${id}/comments`, reopen === undefined ? { body } : { body, reopen }),
listApprovals: (id: string) => api.get<Approval[]>(`/issues/${id}/approvals`),
linkApproval: (id: string, approvalId: string) =>
api.post<Approval[]>(`/issues/${id}/approvals`, { approvalId }),
unlinkApproval: (id: string, approvalId: string) =>
api.delete<{ ok: true }>(`/issues/${id}/approvals/${approvalId}`),
};

View File

@@ -0,0 +1,6 @@
import type { SidebarBadges } from "@paperclip/shared";
import { api } from "./client";
export const sidebarBadgesApi = {
get: (companyId: string) => api.get<SidebarBadges>(`/companies/${companyId}/sidebar-badges`),
};