+ PAPERCLIP_* variables are injected automatically at runtime. +
+
+ {JSON.stringify(payload, null, 2)}
+
+ )}
+ {comment.body}
++ {value} +
++ {label} +
+ {description && ( +{value}
-{label}
-{description}
- )} ); diff --git a/ui/src/components/NewAgentDialog.tsx b/ui/src/components/NewAgentDialog.tsx index 94e12a93..e1f1157a 100644 --- a/ui/src/components/NewAgentDialog.tsx +++ b/ui/src/components/NewAgentDialog.tsx @@ -76,12 +76,13 @@ export function NewAgentDialog() { const createAgent = useMutation({ mutationFn: (data: RecordNo configuration revisions yet.
+ ) : ( ++ Changed:{" "} + {revision.changedKeys.length > 0 ? revision.changedKeys.join(", ") : "no tracked changes"} +
+{filtered.length} agent{filtered.length !== 1 ? "s" : ""}
+ )} + {isLoading &&Loading...
} {error &&{error.message}
} @@ -165,8 +201,10 @@ export function Agents() { ? "bg-cyan-400 animate-pulse" : agent.status === "active" ? "bg-green-400" - : agent.status === "paused" + : agent.status === "paused" ? "bg-yellow-400" + : agent.status === "pending_approval" + ? "bg-amber-400" : agent.status === "error" ? "bg-red-400" : "bg-neutral-400" @@ -260,6 +298,8 @@ function OrgTreeNode({ ? "bg-green-400" : node.status === "paused" ? "bg-yellow-400" + : node.status === "pending_approval" + ? "bg-amber-400" : node.status === "error" ? "bg-red-400" : "bg-neutral-400"; diff --git a/ui/src/pages/ApprovalDetail.tsx b/ui/src/pages/ApprovalDetail.tsx new file mode 100644 index 00000000..fb09bd8a --- /dev/null +++ b/ui/src/pages/ApprovalDetail.tsx @@ -0,0 +1,352 @@ +import { useEffect, useMemo, useState } from "react"; +import { Link, useNavigate, useParams, useSearchParams } from "react-router-dom"; +import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; +import { approvalsApi } from "../api/approvals"; +import { agentsApi } from "../api/agents"; +import { useCompany } from "../context/CompanyContext"; +import { useBreadcrumbs } from "../context/BreadcrumbContext"; +import { queryKeys } from "../lib/queryKeys"; +import { StatusBadge } from "../components/StatusBadge"; +import { Identity } from "../components/Identity"; +import { typeLabel, typeIcon, defaultTypeIcon, ApprovalPayloadRenderer } from "../components/ApprovalPayload"; +import { Button } from "@/components/ui/button"; +import { Textarea } from "@/components/ui/textarea"; +import { CheckCircle2, ChevronRight, Sparkles } from "lucide-react"; +import type { ApprovalComment } from "@paperclip/shared"; + +export function ApprovalDetail() { + const { approvalId } = useParams<{ approvalId: string }>(); + const { selectedCompanyId } = useCompany(); + const { setBreadcrumbs } = useBreadcrumbs(); + const navigate = useNavigate(); + const [searchParams] = useSearchParams(); + const queryClient = useQueryClient(); + const [commentBody, setCommentBody] = useState(""); + const [error, setError] = useStateLoading...
; + if (!approval) returnApproval not found.
; + + const payload = approval.payload as RecordApproval confirmed
++ Requesting agent was notified to review this approval and linked issues. +
+{approval.id}
+
+ {JSON.stringify(payload, null, 2)}
+
+ )}
+ {approval.decisionNote && (
+ Decision note: {approval.decisionNote}
+ )} +{error}
} + {linkedIssues && linkedIssues.length > 0 && ( +Linked Issues
++ Linked issues remain open until the requesting agent follows up and closes them. +
+{comment.body}
+
- {JSON.stringify(payload, null, 2)}
-
- )}
- Select a company first.
; @@ -263,6 +207,7 @@ export function Approvals() { requesterAgent={approval.requestedByAgentId ? (agents ?? []).find((a) => a.id === approval.requestedByAgentId) ?? null : null} onApprove={() => approveMutation.mutate(approval.id)} onReject={() => rejectMutation.mutate(approval.id)} + onOpen={() => navigate(`/approvals/${approval.id}`)} isPending={approveMutation.isPending || rejectMutation.isPending} /> ))} diff --git a/ui/src/pages/Companies.tsx b/ui/src/pages/Companies.tsx index f6f8a9e2..065b4160 100644 --- a/ui/src/pages/Companies.tsx +++ b/ui/src/pages/Companies.tsx @@ -68,6 +68,14 @@ export function Companies() { }, }); + const companySettingsMutation = useMutation({ + mutationFn: ({ id, requireApproval }: { id: string; requireApproval: boolean }) => + companiesApi.update(id, { requireBoardApprovalForNewAgents: requireApproval }), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: queryKeys.companies.all }); + }, + }); + useEffect(() => { setBreadcrumbs([{ label: "Companies" }]); }, [setBreadcrumbs]); @@ -260,6 +268,40 @@ export function Companies() {Loading...
} {error &&{error.message}
} {data && ( <> + {/* Summary card */}Month to Date
+{PRESET_LABELS[preset]}
- {data.summary.monthUtilizationPercent}% utilized + {data.summary.utilizationPercent}% utilized
- {formatCents(data.summary.monthSpendCents)}{" "} + {formatCents(data.summary.spendCents)}{" "} - / {formatCents(data.summary.monthBudgetCents)} + / {formatCents(data.summary.budgetCents)}
No cost events yet.
) : (