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:
@@ -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}`),
|
||||
|
||||
@@ -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`),
|
||||
};
|
||||
|
||||
@@ -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}`),
|
||||
|
||||
@@ -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)}`),
|
||||
};
|
||||
|
||||
@@ -9,3 +9,4 @@ export { costsApi } from "./costs";
|
||||
export { activityApi } from "./activity";
|
||||
export { dashboardApi } from "./dashboard";
|
||||
export { heartbeatsApi } from "./heartbeats";
|
||||
export { sidebarBadgesApi } from "./sidebarBadges";
|
||||
|
||||
@@ -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}`),
|
||||
};
|
||||
|
||||
6
ui/src/api/sidebarBadges.ts
Normal file
6
ui/src/api/sidebarBadges.ts
Normal 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`),
|
||||
};
|
||||
Reference in New Issue
Block a user