@burdenoff/microfe-botmanor 2026.1027.1 → 2026.1027.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -119,7 +119,7 @@ var j = ({ kind: t, loading: n, onClose: r, onConfirm: i }) => {
|
|
|
119
119
|
children: [
|
|
120
120
|
b.loading && b.data.length === 0 ? /* @__PURE__ */ S("div", { className: "h-20 bg-bg-sunken motion-safe:animate-vc-shimmer rounded-lg" }) : null,
|
|
121
121
|
L && /* @__PURE__ */ C("div", {
|
|
122
|
-
className: "p-4 border border-status-warning-border bg-
|
|
122
|
+
className: "p-4 border border-status-warning-border bg-bg-sunken rounded-lg space-y-2",
|
|
123
123
|
"data-testid": "agent-approval-pending",
|
|
124
124
|
children: [
|
|
125
125
|
/* @__PURE__ */ C("div", {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AgentApprovalPanel.js","names":[],"sources":["../../../src/botlit/components/AgentApprovalPanel.tsx"],"sourcesContent":["/**\n * Agent Activation Approval Panel (BOFF-9682)\n * @module botlit/components\n *\n * Reviewer gate on the agent detail page. Builders request activation (the\n * request pins the agent's current config); reviewers with\n * `botmanor-agent:approve` approve it, which publishes the draft and takes the\n * agent live, or reject it with a reason. The requester cannot decide their\n * own request, and a request whose agent changed afterwards is flagged stale\n * and cannot be approved.\n */\n\nimport { useState, type FC } from 'react';\nimport { AlertTriangle, CheckCircle2, ShieldCheck, Send, XCircle } from 'lucide-react';\nimport { toast } from 'sonner';\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n Textarea,\n} from '@burdenoff/fe-libs/ui';\nimport { useTr } from '../../shared/hooks/useTr';\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from './Card';\nimport { Button } from './Button';\nimport { Badge } from './Badge';\nimport {\n useAgentApprovalActions,\n useAgentApprovalRequests,\n useAgentApprovalSettings,\n type AgentApprovalRequestRecord,\n type AgentApprovalStatus,\n} from '../hooks/useAgentApprovals';\nimport { useBotlit } from '../providers/BotlitProvider';\nimport { formatRelativeTime } from '../utils/formatters';\n\nexport function approvalStatusVariant(\n status: AgentApprovalStatus\n): 'warning' | 'success' | 'destructive' | 'outline' {\n if (status === 'PENDING') return 'warning';\n if (status === 'APPROVED') return 'success';\n if (status === 'REJECTED') return 'destructive';\n return 'outline';\n}\n\ntype DecisionKind = 'request' | 'approve' | 'reject';\n\ninterface DecisionDialogProps {\n kind: DecisionKind | null;\n loading: boolean;\n onClose: () => void;\n onConfirm: (text: string) => Promise<void>;\n}\n\n/** Note / reason dialog shared by the detail panel and the approvals page. */\nexport const ApprovalDecisionDialog: FC<DecisionDialogProps> = ({\n kind,\n loading,\n onClose,\n onConfirm,\n}) => {\n const t = useTr();\n const [text, setText] = useState('');\n const reasonRequired = kind === 'reject';\n\n const copy: Record<DecisionKind, { title: string; description: string; confirm: string }> = {\n request: {\n title: t('botlit.agentApproval.dialog.request.title', 'Request activation'),\n description: t(\n 'botlit.agentApproval.dialog.request.description',\n \"A reviewer will check the agent's current prompt, model, tools and limits before it goes live. Changing the agent afterwards makes this request stale.\"\n ),\n confirm: t('botlit.agentApproval.dialog.request.confirm', 'Submit for review'),\n },\n approve: {\n title: t('botlit.agentApproval.dialog.approve.title', 'Approve and go live'),\n description: t(\n 'botlit.agentApproval.dialog.approve.description',\n \"Approving publishes the agent's draft and makes it ACTIVE immediately.\"\n ),\n confirm: t('botlit.agentApproval.dialog.approve.confirm', 'Approve'),\n },\n reject: {\n title: t('botlit.agentApproval.dialog.reject.title', 'Reject activation'),\n description: t(\n 'botlit.agentApproval.dialog.reject.description',\n 'Tell the requester what needs to change. The agent stays as it is.'\n ),\n confirm: t('botlit.agentApproval.dialog.reject.confirm', 'Reject'),\n },\n };\n const current = kind ? copy[kind] : null;\n\n return (\n <Dialog\n open={kind !== null}\n onOpenChange={(open) => {\n if (!open) {\n setText('');\n onClose();\n }\n }}\n >\n <DialogContent>\n <DialogHeader>\n <DialogTitle>{current?.title}</DialogTitle>\n <DialogDescription>{current?.description}</DialogDescription>\n </DialogHeader>\n <Textarea\n aria-label={\n kind === 'request'\n ? t('botlit.agentApproval.dialog.noteLabel', 'Note for the reviewer')\n : t('botlit.agentApproval.dialog.reasonLabel', 'Reason')\n }\n value={text}\n maxLength={2000}\n onChange={(event) => setText(event.target.value)}\n placeholder={\n kind === 'request'\n ? t('botlit.agentApproval.dialog.notePlaceholder', 'What changed and why (optional)')\n : reasonRequired\n ? t('botlit.agentApproval.dialog.reasonRequiredPlaceholder', 'Reason (required)')\n : t('botlit.agentApproval.dialog.reasonPlaceholder', 'Reason (optional)')\n }\n rows={4}\n />\n <DialogFooter>\n <Button\n variant=\"outline\"\n onClick={() => {\n setText('');\n onClose();\n }}\n >\n {t('botlit.common.cancel', 'Cancel')}\n </Button>\n <Button\n variant={kind === 'reject' ? 'destructive' : 'default'}\n isLoading={loading}\n disabled={reasonRequired && text.trim().length === 0}\n onClick={async () => {\n await onConfirm(text);\n setText('');\n }}\n >\n {current?.confirm}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n );\n};\n\ninterface AgentApprovalPanelProps {\n agentId: string;\n agentStatus: string;\n /** Called after a decision that changes the agent so the page can refetch it. */\n onChanged?: () => void;\n}\n\nexport const AgentApprovalPanel: FC<AgentApprovalPanelProps> = ({\n agentId,\n agentStatus,\n onChanged,\n}) => {\n const t = useTr();\n const { currentUser } = useBotlit();\n const settings = useAgentApprovalSettings();\n const requests = useAgentApprovalRequests({ agentId });\n const { run, pending } = useAgentApprovalActions();\n const [dialog, setDialog] = useState<{\n kind: DecisionKind;\n request?: AgentApprovalRequestRecord;\n } | null>(null);\n\n const required = settings.data?.requireActivationApproval === true;\n const open = requests.data.find((request) => request.status === 'PENDING') ?? null;\n const history = requests.data.filter((request) => request.status !== 'PENDING').slice(0, 10);\n const isRequester = open !== null && open.requestedById === currentUser?.id;\n\n const who = (id?: string | null): string =>\n id && id === currentUser?.id ? t('botlit.agentApproval.you', 'you') : (id ?? '—');\n\n const confirm = async (text: string): Promise<void> => {\n if (!dialog) return;\n try {\n if (dialog.kind === 'request') {\n await run({ kind: 'request', agentId, note: text });\n toast.success(t('botlit.agentApproval.toast.requested', 'Activation requested'));\n } else if (dialog.kind === 'approve' && dialog.request) {\n await run({ kind: 'approve', id: dialog.request.id, reason: text });\n toast.success(t('botlit.agentApproval.toast.approved', 'Approved — the agent is live'));\n onChanged?.();\n } else if (dialog.kind === 'reject' && dialog.request) {\n await run({ kind: 'reject', id: dialog.request.id, reason: text });\n toast.success(t('botlit.agentApproval.toast.rejected', 'Activation rejected'));\n }\n setDialog(null);\n await requests.refetch();\n } catch (err: unknown) {\n toast.error(\n err instanceof Error\n ? err.message\n : t('botlit.agentApproval.toast.error', 'Approval action failed')\n );\n }\n };\n\n const cancel = async (request: AgentApprovalRequestRecord): Promise<void> => {\n try {\n await run({ kind: 'cancel', id: request.id });\n toast.success(t('botlit.agentApproval.toast.cancelled', 'Request cancelled'));\n await requests.refetch();\n } catch (err: unknown) {\n toast.error(\n err instanceof Error\n ? err.message\n : t('botlit.agentApproval.toast.error', 'Approval action failed')\n );\n }\n };\n\n return (\n <Card data-testid=\"agent-approval-panel\">\n <CardHeader className=\"flex flex-row items-start justify-between gap-4\">\n <div>\n <CardTitle className=\"flex items-center gap-2\">\n <ShieldCheck size={18} />\n {t('botlit.agentApproval.title', 'Activation approval')}\n <Badge variant={required ? 'warning' : 'outline'}>\n {required\n ? t('botlit.agentApproval.required', 'Required in this workspace')\n : t('botlit.agentApproval.optional', 'Optional')}\n </Badge>\n </CardTitle>\n <CardDescription>\n {required\n ? t(\n 'botlit.agentApproval.descriptionRequired',\n 'This workspace requires a reviewer to approve an agent before it goes live or before a new version is published.'\n )\n : t(\n 'botlit.agentApproval.descriptionOptional',\n 'You can ask a reviewer to check this agent before it goes live. A workspace admin can make review mandatory on the Approvals page.'\n )}\n </CardDescription>\n </div>\n {!open && agentStatus !== 'ARCHIVED' && (\n <Button leftIcon={<Send size={16} />} onClick={() => setDialog({ kind: 'request' })}>\n {t('botlit.agentApproval.requestAction', 'Request activation')}\n </Button>\n )}\n </CardHeader>\n <CardContent className=\"space-y-4\">\n {requests.loading && requests.data.length === 0 ? (\n <div className=\"h-20 bg-bg-sunken motion-safe:animate-vc-shimmer rounded-lg\" />\n ) : null}\n\n {open && (\n <div\n className=\"p-4 border border-status-warning-border bg-status-warning-bg rounded-lg space-y-2\"\n data-testid=\"agent-approval-pending\"\n >\n <div className=\"flex items-center gap-2 flex-wrap\">\n <Badge variant=\"warning\">{open.status}</Badge>\n <span className=\"text-sm text-text-secondary\">\n {t('botlit.agentApproval.requestedBy', 'Requested by')} {who(open.requestedById)}{' '}\n {formatRelativeTime(open.createdAt)}\n </span>\n </div>\n {open.requestNote && <p className=\"text-sm break-words\">{open.requestNote}</p>}\n {open.isStale && (\n <p className=\"flex items-center gap-2 text-sm text-status-warning-text\">\n <AlertTriangle size={14} />\n {t(\n 'botlit.agentApproval.stale',\n 'The agent changed after this request. Submit a new request so the reviewer sees the current configuration.'\n )}\n </p>\n )}\n <div className=\"flex gap-2 flex-wrap\">\n {isRequester ? (\n <>\n <Button\n variant=\"outline\"\n size=\"sm\"\n isLoading={pending === 'cancel'}\n onClick={() => void cancel(open)}\n >\n {t('botlit.agentApproval.cancelAction', 'Cancel request')}\n </Button>\n {open.isStale && (\n <Button size=\"sm\" onClick={() => setDialog({ kind: 'request' })}>\n {t('botlit.agentApproval.resubmitAction', 'Submit new request')}\n </Button>\n )}\n </>\n ) : (\n <>\n <Button\n size=\"sm\"\n leftIcon={<CheckCircle2 size={14} />}\n disabled={open.isStale}\n onClick={() => setDialog({ kind: 'approve', request: open })}\n >\n {t('botlit.agentApproval.approveAction', 'Approve')}\n </Button>\n <Button\n size=\"sm\"\n variant=\"destructive\"\n leftIcon={<XCircle size={14} />}\n onClick={() => setDialog({ kind: 'reject', request: open })}\n >\n {t('botlit.agentApproval.rejectAction', 'Reject')}\n </Button>\n </>\n )}\n </div>\n </div>\n )}\n\n {history.length > 0 && (\n <div className=\"space-y-2\">\n <p className=\"text-sm font-medium\">\n {t('botlit.agentApproval.history', 'Decision history')}\n </p>\n <ul className=\"space-y-2\" data-testid=\"agent-approval-history\">\n {history.map((request) => (\n <li\n key={request.id}\n className=\"p-3 border border-border-subtle rounded-lg text-sm space-y-1\"\n >\n <div className=\"flex items-center gap-2 flex-wrap\">\n <Badge variant={approvalStatusVariant(request.status)}>{request.status}</Badge>\n <span className=\"text-text-secondary\">\n {t('botlit.agentApproval.decidedBy', 'by')} {who(request.reviewerId)}{' '}\n {request.decidedAt ? formatRelativeTime(request.decidedAt) : ''}\n </span>\n </div>\n {request.decisionReason && (\n <p className=\"break-words\">{request.decisionReason}</p>\n )}\n </li>\n ))}\n </ul>\n </div>\n )}\n\n {!open && history.length === 0 && !requests.loading && (\n <p className=\"text-sm text-text-secondary\">\n {t('botlit.agentApproval.empty', 'No activation requests for this agent yet.')}\n </p>\n )}\n </CardContent>\n\n <ApprovalDecisionDialog\n kind={dialog?.kind ?? null}\n loading={pending !== null}\n onClose={() => setDialog(null)}\n onConfirm={confirm}\n />\n </Card>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AgentApprovalPanel.js","names":[],"sources":["../../../src/botlit/components/AgentApprovalPanel.tsx"],"sourcesContent":["/**\n * Agent Activation Approval Panel (BOFF-9682)\n * @module botlit/components\n *\n * Reviewer gate on the agent detail page. Builders request activation (the\n * request pins the agent's current config); reviewers with\n * `botmanor-agent:approve` approve it, which publishes the draft and takes the\n * agent live, or reject it with a reason. The requester cannot decide their\n * own request, and a request whose agent changed afterwards is flagged stale\n * and cannot be approved.\n */\n\nimport { useState, type FC } from 'react';\nimport { AlertTriangle, CheckCircle2, ShieldCheck, Send, XCircle } from 'lucide-react';\nimport { toast } from 'sonner';\nimport {\n Dialog,\n DialogContent,\n DialogDescription,\n DialogFooter,\n DialogHeader,\n DialogTitle,\n Textarea,\n} from '@burdenoff/fe-libs/ui';\nimport { useTr } from '../../shared/hooks/useTr';\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from './Card';\nimport { Button } from './Button';\nimport { Badge } from './Badge';\nimport {\n useAgentApprovalActions,\n useAgentApprovalRequests,\n useAgentApprovalSettings,\n type AgentApprovalRequestRecord,\n type AgentApprovalStatus,\n} from '../hooks/useAgentApprovals';\nimport { useBotlit } from '../providers/BotlitProvider';\nimport { formatRelativeTime } from '../utils/formatters';\n\nexport function approvalStatusVariant(\n status: AgentApprovalStatus\n): 'warning' | 'success' | 'destructive' | 'outline' {\n if (status === 'PENDING') return 'warning';\n if (status === 'APPROVED') return 'success';\n if (status === 'REJECTED') return 'destructive';\n return 'outline';\n}\n\ntype DecisionKind = 'request' | 'approve' | 'reject';\n\ninterface DecisionDialogProps {\n kind: DecisionKind | null;\n loading: boolean;\n onClose: () => void;\n onConfirm: (text: string) => Promise<void>;\n}\n\n/** Note / reason dialog shared by the detail panel and the approvals page. */\nexport const ApprovalDecisionDialog: FC<DecisionDialogProps> = ({\n kind,\n loading,\n onClose,\n onConfirm,\n}) => {\n const t = useTr();\n const [text, setText] = useState('');\n const reasonRequired = kind === 'reject';\n\n const copy: Record<DecisionKind, { title: string; description: string; confirm: string }> = {\n request: {\n title: t('botlit.agentApproval.dialog.request.title', 'Request activation'),\n description: t(\n 'botlit.agentApproval.dialog.request.description',\n \"A reviewer will check the agent's current prompt, model, tools and limits before it goes live. Changing the agent afterwards makes this request stale.\"\n ),\n confirm: t('botlit.agentApproval.dialog.request.confirm', 'Submit for review'),\n },\n approve: {\n title: t('botlit.agentApproval.dialog.approve.title', 'Approve and go live'),\n description: t(\n 'botlit.agentApproval.dialog.approve.description',\n \"Approving publishes the agent's draft and makes it ACTIVE immediately.\"\n ),\n confirm: t('botlit.agentApproval.dialog.approve.confirm', 'Approve'),\n },\n reject: {\n title: t('botlit.agentApproval.dialog.reject.title', 'Reject activation'),\n description: t(\n 'botlit.agentApproval.dialog.reject.description',\n 'Tell the requester what needs to change. The agent stays as it is.'\n ),\n confirm: t('botlit.agentApproval.dialog.reject.confirm', 'Reject'),\n },\n };\n const current = kind ? copy[kind] : null;\n\n return (\n <Dialog\n open={kind !== null}\n onOpenChange={(open) => {\n if (!open) {\n setText('');\n onClose();\n }\n }}\n >\n <DialogContent>\n <DialogHeader>\n <DialogTitle>{current?.title}</DialogTitle>\n <DialogDescription>{current?.description}</DialogDescription>\n </DialogHeader>\n <Textarea\n aria-label={\n kind === 'request'\n ? t('botlit.agentApproval.dialog.noteLabel', 'Note for the reviewer')\n : t('botlit.agentApproval.dialog.reasonLabel', 'Reason')\n }\n value={text}\n maxLength={2000}\n onChange={(event) => setText(event.target.value)}\n placeholder={\n kind === 'request'\n ? t('botlit.agentApproval.dialog.notePlaceholder', 'What changed and why (optional)')\n : reasonRequired\n ? t('botlit.agentApproval.dialog.reasonRequiredPlaceholder', 'Reason (required)')\n : t('botlit.agentApproval.dialog.reasonPlaceholder', 'Reason (optional)')\n }\n rows={4}\n />\n <DialogFooter>\n <Button\n variant=\"outline\"\n onClick={() => {\n setText('');\n onClose();\n }}\n >\n {t('botlit.common.cancel', 'Cancel')}\n </Button>\n <Button\n variant={kind === 'reject' ? 'destructive' : 'default'}\n isLoading={loading}\n disabled={reasonRequired && text.trim().length === 0}\n onClick={async () => {\n await onConfirm(text);\n setText('');\n }}\n >\n {current?.confirm}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n );\n};\n\ninterface AgentApprovalPanelProps {\n agentId: string;\n agentStatus: string;\n /** Called after a decision that changes the agent so the page can refetch it. */\n onChanged?: () => void;\n}\n\nexport const AgentApprovalPanel: FC<AgentApprovalPanelProps> = ({\n agentId,\n agentStatus,\n onChanged,\n}) => {\n const t = useTr();\n const { currentUser } = useBotlit();\n const settings = useAgentApprovalSettings();\n const requests = useAgentApprovalRequests({ agentId });\n const { run, pending } = useAgentApprovalActions();\n const [dialog, setDialog] = useState<{\n kind: DecisionKind;\n request?: AgentApprovalRequestRecord;\n } | null>(null);\n\n const required = settings.data?.requireActivationApproval === true;\n const open = requests.data.find((request) => request.status === 'PENDING') ?? null;\n const history = requests.data.filter((request) => request.status !== 'PENDING').slice(0, 10);\n const isRequester = open !== null && open.requestedById === currentUser?.id;\n\n const who = (id?: string | null): string =>\n id && id === currentUser?.id ? t('botlit.agentApproval.you', 'you') : (id ?? '—');\n\n const confirm = async (text: string): Promise<void> => {\n if (!dialog) return;\n try {\n if (dialog.kind === 'request') {\n await run({ kind: 'request', agentId, note: text });\n toast.success(t('botlit.agentApproval.toast.requested', 'Activation requested'));\n } else if (dialog.kind === 'approve' && dialog.request) {\n await run({ kind: 'approve', id: dialog.request.id, reason: text });\n toast.success(t('botlit.agentApproval.toast.approved', 'Approved — the agent is live'));\n onChanged?.();\n } else if (dialog.kind === 'reject' && dialog.request) {\n await run({ kind: 'reject', id: dialog.request.id, reason: text });\n toast.success(t('botlit.agentApproval.toast.rejected', 'Activation rejected'));\n }\n setDialog(null);\n await requests.refetch();\n } catch (err: unknown) {\n toast.error(\n err instanceof Error\n ? err.message\n : t('botlit.agentApproval.toast.error', 'Approval action failed')\n );\n }\n };\n\n const cancel = async (request: AgentApprovalRequestRecord): Promise<void> => {\n try {\n await run({ kind: 'cancel', id: request.id });\n toast.success(t('botlit.agentApproval.toast.cancelled', 'Request cancelled'));\n await requests.refetch();\n } catch (err: unknown) {\n toast.error(\n err instanceof Error\n ? err.message\n : t('botlit.agentApproval.toast.error', 'Approval action failed')\n );\n }\n };\n\n return (\n <Card data-testid=\"agent-approval-panel\">\n <CardHeader className=\"flex flex-row items-start justify-between gap-4\">\n <div>\n <CardTitle className=\"flex items-center gap-2\">\n <ShieldCheck size={18} />\n {t('botlit.agentApproval.title', 'Activation approval')}\n <Badge variant={required ? 'warning' : 'outline'}>\n {required\n ? t('botlit.agentApproval.required', 'Required in this workspace')\n : t('botlit.agentApproval.optional', 'Optional')}\n </Badge>\n </CardTitle>\n <CardDescription>\n {required\n ? t(\n 'botlit.agentApproval.descriptionRequired',\n 'This workspace requires a reviewer to approve an agent before it goes live or before a new version is published.'\n )\n : t(\n 'botlit.agentApproval.descriptionOptional',\n 'You can ask a reviewer to check this agent before it goes live. A workspace admin can make review mandatory on the Approvals page.'\n )}\n </CardDescription>\n </div>\n {!open && agentStatus !== 'ARCHIVED' && (\n <Button leftIcon={<Send size={16} />} onClick={() => setDialog({ kind: 'request' })}>\n {t('botlit.agentApproval.requestAction', 'Request activation')}\n </Button>\n )}\n </CardHeader>\n <CardContent className=\"space-y-4\">\n {requests.loading && requests.data.length === 0 ? (\n <div className=\"h-20 bg-bg-sunken motion-safe:animate-vc-shimmer rounded-lg\" />\n ) : null}\n\n {open && (\n <div\n className=\"p-4 border border-status-warning-border bg-bg-sunken rounded-lg space-y-2\"\n data-testid=\"agent-approval-pending\"\n >\n <div className=\"flex items-center gap-2 flex-wrap\">\n <Badge variant=\"warning\">{open.status}</Badge>\n <span className=\"text-sm text-text-secondary\">\n {t('botlit.agentApproval.requestedBy', 'Requested by')} {who(open.requestedById)}{' '}\n {formatRelativeTime(open.createdAt)}\n </span>\n </div>\n {open.requestNote && <p className=\"text-sm break-words\">{open.requestNote}</p>}\n {open.isStale && (\n <p className=\"flex items-center gap-2 text-sm text-status-warning-text\">\n <AlertTriangle size={14} />\n {t(\n 'botlit.agentApproval.stale',\n 'The agent changed after this request. Submit a new request so the reviewer sees the current configuration.'\n )}\n </p>\n )}\n <div className=\"flex gap-2 flex-wrap\">\n {isRequester ? (\n <>\n <Button\n variant=\"outline\"\n size=\"sm\"\n isLoading={pending === 'cancel'}\n onClick={() => void cancel(open)}\n >\n {t('botlit.agentApproval.cancelAction', 'Cancel request')}\n </Button>\n {open.isStale && (\n <Button size=\"sm\" onClick={() => setDialog({ kind: 'request' })}>\n {t('botlit.agentApproval.resubmitAction', 'Submit new request')}\n </Button>\n )}\n </>\n ) : (\n <>\n <Button\n size=\"sm\"\n leftIcon={<CheckCircle2 size={14} />}\n disabled={open.isStale}\n onClick={() => setDialog({ kind: 'approve', request: open })}\n >\n {t('botlit.agentApproval.approveAction', 'Approve')}\n </Button>\n <Button\n size=\"sm\"\n variant=\"destructive\"\n leftIcon={<XCircle size={14} />}\n onClick={() => setDialog({ kind: 'reject', request: open })}\n >\n {t('botlit.agentApproval.rejectAction', 'Reject')}\n </Button>\n </>\n )}\n </div>\n </div>\n )}\n\n {history.length > 0 && (\n <div className=\"space-y-2\">\n <p className=\"text-sm font-medium\">\n {t('botlit.agentApproval.history', 'Decision history')}\n </p>\n <ul className=\"space-y-2\" data-testid=\"agent-approval-history\">\n {history.map((request) => (\n <li\n key={request.id}\n className=\"p-3 border border-border-subtle rounded-lg text-sm space-y-1\"\n >\n <div className=\"flex items-center gap-2 flex-wrap\">\n <Badge variant={approvalStatusVariant(request.status)}>{request.status}</Badge>\n <span className=\"text-text-secondary\">\n {t('botlit.agentApproval.decidedBy', 'by')} {who(request.reviewerId)}{' '}\n {request.decidedAt ? formatRelativeTime(request.decidedAt) : ''}\n </span>\n </div>\n {request.decisionReason && (\n <p className=\"break-words\">{request.decisionReason}</p>\n )}\n </li>\n ))}\n </ul>\n </div>\n )}\n\n {!open && history.length === 0 && !requests.loading && (\n <p className=\"text-sm text-text-secondary\">\n {t('botlit.agentApproval.empty', 'No activation requests for this agent yet.')}\n </p>\n )}\n </CardContent>\n\n <ApprovalDecisionDialog\n kind={dialog?.kind ?? null}\n loading={pending !== null}\n onClose={() => setDialog(null)}\n onConfirm={confirm}\n />\n </Card>\n );\n};\n"],"mappings":"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"}
|
|
@@ -55,6 +55,7 @@ var k = () => {
|
|
|
55
55
|
children: H ? k("botlit.agentApprovals.settings.on", "On") : k("botlit.agentApprovals.settings.off", "Off")
|
|
56
56
|
})]
|
|
57
57
|
}), /* @__PURE__ */ x(s, { children: k("botlit.agentApprovals.settings.description", "When on, activating an agent, publishing a new version, or editing the prompt, model or tools of a live agent needs an approved request. Rolling a live agent back to an earlier version stays available for emergencies.") })] }), /* @__PURE__ */ x(u, {
|
|
58
|
+
className: "shrink-0 whitespace-nowrap",
|
|
58
59
|
variant: H ? "outline" : "default",
|
|
59
60
|
isLoading: M.saving,
|
|
60
61
|
disabled: M.loading || !M.data,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"AgentApprovalsPage.js","names":[],"sources":["../../../src/botlit/pages/AgentApprovalsPage.tsx"],"sourcesContent":["/**\n * Agent Activation Approvals Page (BOFF-9682)\n * @module botlit/pages\n *\n * Reviewer queue for the workspace: every pending request to take an agent\n * live, with approve / reject, the recent decisions, and the workspace switch\n * that makes review mandatory. Approving publishes the agent's draft and sets\n * it ACTIVE; a request whose agent changed after it was submitted is flagged\n * stale and cannot be approved.\n */\n\nimport { useMemo, useState, type FC } from 'react';\nimport { AlertTriangle, ArrowLeft, CheckCircle2, ShieldCheck, XCircle } from 'lucide-react';\nimport { toast } from 'sonner';\nimport { PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { useTr } from '../../shared/hooks/useTr';\nimport { PageLayout } from '../components/PageLayout';\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../components/Card';\nimport { Button } from '../components/Button';\nimport { Badge } from '../components/Badge';\nimport { ErrorState } from '../components/ErrorState';\nimport { ApprovalDecisionDialog, approvalStatusVariant } from '../components/AgentApprovalPanel';\nimport { useAgents } from '../hooks/useAgents';\nimport {\n useAgentApprovalActions,\n useAgentApprovalRequests,\n useAgentApprovalSettings,\n type AgentApprovalRequestRecord,\n} from '../hooks/useAgentApprovals';\nimport { useBotlit } from '../providers/BotlitProvider';\nimport { formatRelativeTime } from '../utils/formatters';\n\nexport const AgentApprovalsPage: FC = () => {\n const t = useTr();\n const { navigateTo, currentUser } = useBotlit();\n const settings = useAgentApprovalSettings();\n const pendingRequests = useAgentApprovalRequests({ status: 'PENDING' });\n const allRequests = useAgentApprovalRequests({});\n const { data: agents } = useAgents();\n const { run, pending } = useAgentApprovalActions();\n const [dialog, setDialog] = useState<{\n kind: 'approve' | 'reject';\n request: AgentApprovalRequestRecord;\n } | null>(null);\n\n const agentNames = useMemo(\n () => new Map(agents.map((agent) => [agent.id, agent.name])),\n [agents]\n );\n const decided = allRequests.data.filter((request) => request.status !== 'PENDING').slice(0, 20);\n const required = settings.data?.requireActivationApproval === true;\n const who = (id?: string | null): string =>\n id && id === currentUser?.id ? t('botlit.agentApproval.you', 'you') : (id ?? '—');\n\n const refreshAll = async (): Promise<void> => {\n await Promise.all([pendingRequests.refetch(), allRequests.refetch()]);\n };\n\n const toggleRequired = async (): Promise<void> => {\n try {\n const next = await settings.update(!required);\n toast.success(\n next.requireActivationApproval\n ? t('botlit.agentApprovals.toast.enabled', 'Agents now need approval before going live')\n : t('botlit.agentApprovals.toast.disabled', 'Activation approval is now optional')\n );\n } catch (err: unknown) {\n toast.error(\n err instanceof Error\n ? err.message\n : t('botlit.agentApprovals.toast.settingsError', 'Failed to update the setting')\n );\n }\n };\n\n const confirm = async (text: string): Promise<void> => {\n if (!dialog) return;\n try {\n if (dialog.kind === 'approve') {\n await run({ kind: 'approve', id: dialog.request.id, reason: text });\n toast.success(t('botlit.agentApproval.toast.approved', 'Approved — the agent is live'));\n } else {\n await run({ kind: 'reject', id: dialog.request.id, reason: text });\n toast.success(t('botlit.agentApproval.toast.rejected', 'Activation rejected'));\n }\n setDialog(null);\n await refreshAll();\n } catch (err: unknown) {\n toast.error(\n err instanceof Error\n ? err.message\n : t('botlit.agentApproval.toast.error', 'Approval action failed')\n );\n }\n };\n\n return (\n <PageLayout\n title={\n <div className=\"flex items-center gap-3\">\n <Button variant=\"ghost\" size=\"icon\" onClick={() => navigateTo('agents')}>\n <ArrowLeft size={20} />\n </Button>\n <span>{t('botlit.agentApprovals.title', 'Agent approvals')}</span>\n </div>\n }\n description={t('botlit.agentApprovals.description', 'Review requests to take agents live')}\n >\n <div className=\"space-y-6\">\n <PagePurpose icon={<ShieldCheck size={18} />}>\n {t(\n 'botlit.agentApprovals.purpose',\n \"A reviewer checks an agent's prompt, model, tools and limits before it goes live. The person who requested activation cannot approve their own request, and every decision is kept in the history below.\"\n )}\n </PagePurpose>\n\n <Card data-testid=\"agent-approvals-settings\">\n <CardHeader className=\"flex flex-row items-start justify-between gap-4\">\n <div>\n <CardTitle className=\"flex items-center gap-2\">\n {t(\n 'botlit.agentApprovals.settings.title',\n 'Require approval before agents go live'\n )}\n <Badge variant={required ? 'warning' : 'outline'}>\n {required\n ? t('botlit.agentApprovals.settings.on', 'On')\n : t('botlit.agentApprovals.settings.off', 'Off')}\n </Badge>\n </CardTitle>\n <CardDescription>\n {t(\n 'botlit.agentApprovals.settings.description',\n 'When on, activating an agent, publishing a new version, or editing the prompt, model or tools of a live agent needs an approved request. Rolling a live agent back to an earlier version stays available for emergencies.'\n )}\n </CardDescription>\n </div>\n <Button\n variant={required ? 'outline' : 'default'}\n isLoading={settings.saving}\n disabled={settings.loading || !settings.data}\n onClick={() => void toggleRequired()}\n >\n {required\n ? t('botlit.agentApprovals.settings.turnOff', 'Turn off')\n : t('botlit.agentApprovals.settings.turnOn', 'Turn on')}\n </Button>\n </CardHeader>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>{t('botlit.agentApprovals.pending.title', 'Waiting for review')}</CardTitle>\n </CardHeader>\n <CardContent>\n {pendingRequests.error ? (\n <ErrorState\n resource={t('botlit.agentApprovals.resource', 'activation requests')}\n error={pendingRequests.error}\n onRetry={() => void pendingRequests.refetch()}\n />\n ) : pendingRequests.loading && pendingRequests.data.length === 0 ? (\n <div className=\"h-24 bg-bg-sunken motion-safe:animate-vc-shimmer rounded-lg\" />\n ) : pendingRequests.data.length === 0 ? (\n <p className=\"text-sm text-text-secondary\">\n {t('botlit.agentApprovals.pending.empty', 'No agents are waiting for review.')}\n </p>\n ) : (\n <ul className=\"space-y-3\" data-testid=\"agent-approvals-pending\">\n {pendingRequests.data.map((request) => {\n const own = request.requestedById === currentUser?.id;\n return (\n <li\n key={request.id}\n className=\"p-4 border border-border-subtle rounded-lg space-y-2\"\n >\n <div className=\"flex items-center gap-2 flex-wrap\">\n <button\n type=\"button\"\n className=\"font-medium text-primary hover:underline\"\n onClick={() => navigateTo(`agents/${request.agentId}`)}\n >\n {agentNames.get(request.agentId) ?? request.agentId}\n </button>\n <Badge variant=\"warning\">{request.status}</Badge>\n <span className=\"text-sm text-text-secondary\">\n {t('botlit.agentApproval.requestedBy', 'Requested by')}{' '}\n {who(request.requestedById)} {formatRelativeTime(request.createdAt)}\n </span>\n </div>\n {request.requestNote && (\n <p className=\"text-sm break-words\">{request.requestNote}</p>\n )}\n {request.isStale && (\n <p className=\"flex items-center gap-2 text-sm text-status-warning-text\">\n <AlertTriangle size={14} />\n {t(\n 'botlit.agentApproval.stale',\n 'The agent changed after this request. Submit a new request so the reviewer sees the current configuration.'\n )}\n </p>\n )}\n {own ? (\n <p className=\"text-xs text-text-secondary\">\n {t(\n 'botlit.agentApprovals.ownRequest',\n 'You requested this, so another reviewer has to decide it.'\n )}\n </p>\n ) : (\n <div className=\"flex gap-2\">\n <Button\n size=\"sm\"\n leftIcon={<CheckCircle2 size={14} />}\n disabled={request.isStale}\n onClick={() => setDialog({ kind: 'approve', request })}\n >\n {t('botlit.agentApproval.approveAction', 'Approve')}\n </Button>\n <Button\n size=\"sm\"\n variant=\"destructive\"\n leftIcon={<XCircle size={14} />}\n onClick={() => setDialog({ kind: 'reject', request })}\n >\n {t('botlit.agentApproval.rejectAction', 'Reject')}\n </Button>\n </div>\n )}\n </li>\n );\n })}\n </ul>\n )}\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>{t('botlit.agentApprovals.history.title', 'Recent decisions')}</CardTitle>\n </CardHeader>\n <CardContent>\n {decided.length === 0 ? (\n <p className=\"text-sm text-text-secondary\">\n {t('botlit.agentApprovals.history.empty', 'No decisions yet.')}\n </p>\n ) : (\n <ul className=\"space-y-2\" data-testid=\"agent-approvals-history\">\n {decided.map((request) => (\n <li\n key={request.id}\n className=\"p-3 border border-border-subtle rounded-lg text-sm space-y-1\"\n >\n <div className=\"flex items-center gap-2 flex-wrap\">\n <span className=\"font-medium\">\n {agentNames.get(request.agentId) ?? request.agentId}\n </span>\n <Badge variant={approvalStatusVariant(request.status)}>\n {request.status}\n </Badge>\n <span className=\"text-text-secondary\">\n {t('botlit.agentApproval.decidedBy', 'by')} {who(request.reviewerId)}{' '}\n {request.decidedAt ? formatRelativeTime(request.decidedAt) : ''}\n </span>\n </div>\n {request.decisionReason && (\n <p className=\"break-words\">{request.decisionReason}</p>\n )}\n </li>\n ))}\n </ul>\n )}\n </CardContent>\n </Card>\n </div>\n\n <ApprovalDecisionDialog\n kind={dialog?.kind ?? null}\n loading={pending !== null}\n onClose={() => setDialog(null)}\n onConfirm={confirm}\n />\n </PageLayout>\n );\n};\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"AgentApprovalsPage.js","names":[],"sources":["../../../src/botlit/pages/AgentApprovalsPage.tsx"],"sourcesContent":["/**\n * Agent Activation Approvals Page (BOFF-9682)\n * @module botlit/pages\n *\n * Reviewer queue for the workspace: every pending request to take an agent\n * live, with approve / reject, the recent decisions, and the workspace switch\n * that makes review mandatory. Approving publishes the agent's draft and sets\n * it ACTIVE; a request whose agent changed after it was submitted is flagged\n * stale and cannot be approved.\n */\n\nimport { useMemo, useState, type FC } from 'react';\nimport { AlertTriangle, ArrowLeft, CheckCircle2, ShieldCheck, XCircle } from 'lucide-react';\nimport { toast } from 'sonner';\nimport { PagePurpose } from '@burdenoff/fe-libs/ui';\nimport { useTr } from '../../shared/hooks/useTr';\nimport { PageLayout } from '../components/PageLayout';\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../components/Card';\nimport { Button } from '../components/Button';\nimport { Badge } from '../components/Badge';\nimport { ErrorState } from '../components/ErrorState';\nimport { ApprovalDecisionDialog, approvalStatusVariant } from '../components/AgentApprovalPanel';\nimport { useAgents } from '../hooks/useAgents';\nimport {\n useAgentApprovalActions,\n useAgentApprovalRequests,\n useAgentApprovalSettings,\n type AgentApprovalRequestRecord,\n} from '../hooks/useAgentApprovals';\nimport { useBotlit } from '../providers/BotlitProvider';\nimport { formatRelativeTime } from '../utils/formatters';\n\nexport const AgentApprovalsPage: FC = () => {\n const t = useTr();\n const { navigateTo, currentUser } = useBotlit();\n const settings = useAgentApprovalSettings();\n const pendingRequests = useAgentApprovalRequests({ status: 'PENDING' });\n const allRequests = useAgentApprovalRequests({});\n const { data: agents } = useAgents();\n const { run, pending } = useAgentApprovalActions();\n const [dialog, setDialog] = useState<{\n kind: 'approve' | 'reject';\n request: AgentApprovalRequestRecord;\n } | null>(null);\n\n const agentNames = useMemo(\n () => new Map(agents.map((agent) => [agent.id, agent.name])),\n [agents]\n );\n const decided = allRequests.data.filter((request) => request.status !== 'PENDING').slice(0, 20);\n const required = settings.data?.requireActivationApproval === true;\n const who = (id?: string | null): string =>\n id && id === currentUser?.id ? t('botlit.agentApproval.you', 'you') : (id ?? '—');\n\n const refreshAll = async (): Promise<void> => {\n await Promise.all([pendingRequests.refetch(), allRequests.refetch()]);\n };\n\n const toggleRequired = async (): Promise<void> => {\n try {\n const next = await settings.update(!required);\n toast.success(\n next.requireActivationApproval\n ? t('botlit.agentApprovals.toast.enabled', 'Agents now need approval before going live')\n : t('botlit.agentApprovals.toast.disabled', 'Activation approval is now optional')\n );\n } catch (err: unknown) {\n toast.error(\n err instanceof Error\n ? err.message\n : t('botlit.agentApprovals.toast.settingsError', 'Failed to update the setting')\n );\n }\n };\n\n const confirm = async (text: string): Promise<void> => {\n if (!dialog) return;\n try {\n if (dialog.kind === 'approve') {\n await run({ kind: 'approve', id: dialog.request.id, reason: text });\n toast.success(t('botlit.agentApproval.toast.approved', 'Approved — the agent is live'));\n } else {\n await run({ kind: 'reject', id: dialog.request.id, reason: text });\n toast.success(t('botlit.agentApproval.toast.rejected', 'Activation rejected'));\n }\n setDialog(null);\n await refreshAll();\n } catch (err: unknown) {\n toast.error(\n err instanceof Error\n ? err.message\n : t('botlit.agentApproval.toast.error', 'Approval action failed')\n );\n }\n };\n\n return (\n <PageLayout\n title={\n <div className=\"flex items-center gap-3\">\n <Button variant=\"ghost\" size=\"icon\" onClick={() => navigateTo('agents')}>\n <ArrowLeft size={20} />\n </Button>\n <span>{t('botlit.agentApprovals.title', 'Agent approvals')}</span>\n </div>\n }\n description={t('botlit.agentApprovals.description', 'Review requests to take agents live')}\n >\n <div className=\"space-y-6\">\n <PagePurpose icon={<ShieldCheck size={18} />}>\n {t(\n 'botlit.agentApprovals.purpose',\n \"A reviewer checks an agent's prompt, model, tools and limits before it goes live. The person who requested activation cannot approve their own request, and every decision is kept in the history below.\"\n )}\n </PagePurpose>\n\n <Card data-testid=\"agent-approvals-settings\">\n <CardHeader className=\"flex flex-row items-start justify-between gap-4\">\n <div>\n <CardTitle className=\"flex items-center gap-2\">\n {t(\n 'botlit.agentApprovals.settings.title',\n 'Require approval before agents go live'\n )}\n <Badge variant={required ? 'warning' : 'outline'}>\n {required\n ? t('botlit.agentApprovals.settings.on', 'On')\n : t('botlit.agentApprovals.settings.off', 'Off')}\n </Badge>\n </CardTitle>\n <CardDescription>\n {t(\n 'botlit.agentApprovals.settings.description',\n 'When on, activating an agent, publishing a new version, or editing the prompt, model or tools of a live agent needs an approved request. Rolling a live agent back to an earlier version stays available for emergencies.'\n )}\n </CardDescription>\n </div>\n <Button\n className=\"shrink-0 whitespace-nowrap\"\n variant={required ? 'outline' : 'default'}\n isLoading={settings.saving}\n disabled={settings.loading || !settings.data}\n onClick={() => void toggleRequired()}\n >\n {required\n ? t('botlit.agentApprovals.settings.turnOff', 'Turn off')\n : t('botlit.agentApprovals.settings.turnOn', 'Turn on')}\n </Button>\n </CardHeader>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>{t('botlit.agentApprovals.pending.title', 'Waiting for review')}</CardTitle>\n </CardHeader>\n <CardContent>\n {pendingRequests.error ? (\n <ErrorState\n resource={t('botlit.agentApprovals.resource', 'activation requests')}\n error={pendingRequests.error}\n onRetry={() => void pendingRequests.refetch()}\n />\n ) : pendingRequests.loading && pendingRequests.data.length === 0 ? (\n <div className=\"h-24 bg-bg-sunken motion-safe:animate-vc-shimmer rounded-lg\" />\n ) : pendingRequests.data.length === 0 ? (\n <p className=\"text-sm text-text-secondary\">\n {t('botlit.agentApprovals.pending.empty', 'No agents are waiting for review.')}\n </p>\n ) : (\n <ul className=\"space-y-3\" data-testid=\"agent-approvals-pending\">\n {pendingRequests.data.map((request) => {\n const own = request.requestedById === currentUser?.id;\n return (\n <li\n key={request.id}\n className=\"p-4 border border-border-subtle rounded-lg space-y-2\"\n >\n <div className=\"flex items-center gap-2 flex-wrap\">\n <button\n type=\"button\"\n className=\"font-medium text-primary hover:underline\"\n onClick={() => navigateTo(`agents/${request.agentId}`)}\n >\n {agentNames.get(request.agentId) ?? request.agentId}\n </button>\n <Badge variant=\"warning\">{request.status}</Badge>\n <span className=\"text-sm text-text-secondary\">\n {t('botlit.agentApproval.requestedBy', 'Requested by')}{' '}\n {who(request.requestedById)} {formatRelativeTime(request.createdAt)}\n </span>\n </div>\n {request.requestNote && (\n <p className=\"text-sm break-words\">{request.requestNote}</p>\n )}\n {request.isStale && (\n <p className=\"flex items-center gap-2 text-sm text-status-warning-text\">\n <AlertTriangle size={14} />\n {t(\n 'botlit.agentApproval.stale',\n 'The agent changed after this request. Submit a new request so the reviewer sees the current configuration.'\n )}\n </p>\n )}\n {own ? (\n <p className=\"text-xs text-text-secondary\">\n {t(\n 'botlit.agentApprovals.ownRequest',\n 'You requested this, so another reviewer has to decide it.'\n )}\n </p>\n ) : (\n <div className=\"flex gap-2\">\n <Button\n size=\"sm\"\n leftIcon={<CheckCircle2 size={14} />}\n disabled={request.isStale}\n onClick={() => setDialog({ kind: 'approve', request })}\n >\n {t('botlit.agentApproval.approveAction', 'Approve')}\n </Button>\n <Button\n size=\"sm\"\n variant=\"destructive\"\n leftIcon={<XCircle size={14} />}\n onClick={() => setDialog({ kind: 'reject', request })}\n >\n {t('botlit.agentApproval.rejectAction', 'Reject')}\n </Button>\n </div>\n )}\n </li>\n );\n })}\n </ul>\n )}\n </CardContent>\n </Card>\n\n <Card>\n <CardHeader>\n <CardTitle>{t('botlit.agentApprovals.history.title', 'Recent decisions')}</CardTitle>\n </CardHeader>\n <CardContent>\n {decided.length === 0 ? (\n <p className=\"text-sm text-text-secondary\">\n {t('botlit.agentApprovals.history.empty', 'No decisions yet.')}\n </p>\n ) : (\n <ul className=\"space-y-2\" data-testid=\"agent-approvals-history\">\n {decided.map((request) => (\n <li\n key={request.id}\n className=\"p-3 border border-border-subtle rounded-lg text-sm space-y-1\"\n >\n <div className=\"flex items-center gap-2 flex-wrap\">\n <span className=\"font-medium\">\n {agentNames.get(request.agentId) ?? request.agentId}\n </span>\n <Badge variant={approvalStatusVariant(request.status)}>\n {request.status}\n </Badge>\n <span className=\"text-text-secondary\">\n {t('botlit.agentApproval.decidedBy', 'by')} {who(request.reviewerId)}{' '}\n {request.decidedAt ? formatRelativeTime(request.decidedAt) : ''}\n </span>\n </div>\n {request.decisionReason && (\n <p className=\"break-words\">{request.decisionReason}</p>\n )}\n </li>\n ))}\n </ul>\n )}\n </CardContent>\n </Card>\n </div>\n\n <ApprovalDecisionDialog\n kind={dialog?.kind ?? null}\n loading={pending !== null}\n onClose={() => setDialog(null)}\n onConfirm={confirm}\n />\n </PageLayout>\n );\n};\n"],"mappings":"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"}
|
package/package.json
CHANGED