@burdenoff/microfe-subscriberbot 2026.1030.1 → 2026.1030.3

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.
Files changed (49) hide show
  1. package/dist/subscriberbot/SubscriberBotRoutes.d.ts.map +1 -1
  2. package/dist/subscriberbot/SubscriberBotRoutes.js +16 -8
  3. package/dist/subscriberbot/SubscriberBotRoutes.js.map +1 -1
  4. package/dist/subscriberbot/components/StatusBadge.d.ts.map +1 -1
  5. package/dist/subscriberbot/components/StatusBadge.js +6 -1
  6. package/dist/subscriberbot/components/StatusBadge.js.map +1 -1
  7. package/dist/subscriberbot/components/decisions/DecisionBanners.d.ts +11 -0
  8. package/dist/subscriberbot/components/decisions/DecisionBanners.d.ts.map +1 -0
  9. package/dist/subscriberbot/components/decisions/DecisionBanners.js +62 -0
  10. package/dist/subscriberbot/components/decisions/DecisionBanners.js.map +1 -0
  11. package/dist/subscriberbot/components/decisions/DecisionCard.d.ts +22 -0
  12. package/dist/subscriberbot/components/decisions/DecisionCard.d.ts.map +1 -0
  13. package/dist/subscriberbot/components/decisions/DecisionCard.js +316 -0
  14. package/dist/subscriberbot/components/decisions/DecisionCard.js.map +1 -0
  15. package/dist/subscriberbot/components/decisions/decisionModel.d.ts +51 -0
  16. package/dist/subscriberbot/components/decisions/decisionModel.d.ts.map +1 -0
  17. package/dist/subscriberbot/components/decisions/decisionModel.js +139 -0
  18. package/dist/subscriberbot/components/decisions/decisionModel.js.map +1 -0
  19. package/dist/subscriberbot/components/decisions/index.d.ts +9 -0
  20. package/dist/subscriberbot/components/decisions/index.d.ts.map +1 -0
  21. package/dist/subscriberbot/components/decisions/index.js +3 -0
  22. package/dist/subscriberbot/hooks/index.d.ts +1 -1
  23. package/dist/subscriberbot/hooks/index.d.ts.map +1 -1
  24. package/dist/subscriberbot/hooks/useSubscriberBotApi.d.ts +19 -2
  25. package/dist/subscriberbot/hooks/useSubscriberBotApi.d.ts.map +1 -1
  26. package/dist/subscriberbot/hooks/useSubscriberBotApi.js +103 -38
  27. package/dist/subscriberbot/hooks/useSubscriberBotApi.js.map +1 -1
  28. package/dist/subscriberbot/pages/agents/AgentDetailPage.d.ts +7 -0
  29. package/dist/subscriberbot/pages/agents/AgentDetailPage.d.ts.map +1 -1
  30. package/dist/subscriberbot/pages/agents/AgentDetailPage.js +280 -175
  31. package/dist/subscriberbot/pages/agents/AgentDetailPage.js.map +1 -1
  32. package/dist/subscriberbot/pages/decisions/DecisionDetailPage.d.ts +12 -0
  33. package/dist/subscriberbot/pages/decisions/DecisionDetailPage.d.ts.map +1 -0
  34. package/dist/subscriberbot/pages/decisions/DecisionDetailPage.js +180 -0
  35. package/dist/subscriberbot/pages/decisions/DecisionDetailPage.js.map +1 -0
  36. package/dist/subscriberbot/pages/decisions/DecisionsPage.d.ts +13 -0
  37. package/dist/subscriberbot/pages/decisions/DecisionsPage.d.ts.map +1 -0
  38. package/dist/subscriberbot/pages/decisions/DecisionsPage.js +154 -0
  39. package/dist/subscriberbot/pages/decisions/DecisionsPage.js.map +1 -0
  40. package/dist/subscriberbot/pages/relationships/RelationshipDetailPage.d.ts.map +1 -1
  41. package/dist/subscriberbot/pages/relationships/RelationshipDetailPage.js +338 -316
  42. package/dist/subscriberbot/pages/relationships/RelationshipDetailPage.js.map +1 -1
  43. package/dist/subscriberbot/types/index.d.ts +66 -1
  44. package/dist/subscriberbot/types/index.d.ts.map +1 -1
  45. package/dist/subscriberbot/utils/pageContext.d.ts +1 -0
  46. package/dist/subscriberbot/utils/pageContext.d.ts.map +1 -1
  47. package/dist/subscriberbot/utils/pageContext.js +2 -0
  48. package/dist/subscriberbot/utils/pageContext.js.map +1 -1
  49. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"AgentDetailPage.js","names":[],"sources":["../../../../src/subscriberbot/pages/agents/AgentDetailPage.tsx"],"sourcesContent":["/**\n * Agent detail\n * @module subscriberbot/pages/agents\n *\n * A single AI agent: its kind, posture, last run, and the autonomous policies\n * that bound it. Reads `subAgent(id)`; changes posture via `subSetAgentStatus`.\n * The \"Run history\" timeline is derived from the agent's real `lastRunAt` and\n * each policy's real `lastTriggeredAt` — never mocked. Each policy card also\n * renders its stored `condition` JSON (the rule the agent evaluates before it\n * acts), so a condition entered in \"Add policy\" is visible after it is saved.\n */\n\nimport { useState } from 'react';\nimport { useParams } from 'react-router';\nimport {\n ArrowLeft,\n Bot,\n Search,\n RefreshCw,\n DollarSign,\n Mail,\n Building2,\n ShieldCheck,\n Handshake,\n ArrowLeftRight,\n ConciergeBell,\n Plus,\n Clock,\n Zap,\n ListChecks,\n History,\n SlidersHorizontal,\n type LucideIcon,\n} from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n StatusBadge,\n EmptyState,\n Modal,\n MetricsCard,\n Timeline,\n LoadingView,\n ErrorView,\n Section,\n type TimelineItem,\n} from '../../components';\nimport { useSubscriberBot } from '../../context/SubscriberBotContext';\nimport { useAgent, useSetAgentStatus, useCreatePolicy } from '../../hooks/useSubscriberBotApi';\nimport { useTr } from '../../hooks/useTr';\nimport { useAutonomousAgentMode } from '../../hooks/useAutonomousAgentMode';\nimport { PostureOptions } from '../../components/PostureOptions';\nimport { formatEnumLabel, formatNumber, formatRelativeTime } from '../../utils/formatters';\nimport type { SubAgentKind, SubAgentStatus, SubPolicyAction, SubPolicy } from '../../types';\n\nconst STATUS_OPTIONS: SubAgentStatus[] = ['DISABLED', 'SUGGEST_ONLY', 'AUTONOMOUS'];\n\nconst KIND_KEY: Record<SubAgentKind, string> = {\n DISCOVERY: 'agents.kinds.discovery',\n RENEWAL: 'agents.kinds.renewal',\n COST: 'agents.kinds.cost',\n NEWSLETTER: 'agents.kinds.newsletter',\n VENDOR: 'agents.kinds.vendor',\n COMPLIANCE: 'agents.kinds.compliance',\n NEGOTIATION: 'agents.kinds.negotiation',\n MIGRATION: 'agents.kinds.migration',\n CONCIERGE: 'agents.kinds.concierge',\n};\n\nconst POLICY_ACTIONS: SubPolicyAction[] = [\n 'ALERT',\n 'CREATE_TASK',\n 'RENEW',\n 'CANCEL',\n 'PAUSE',\n 'UPGRADE',\n 'DOWNGRADE',\n];\n\nconst KIND_ICON: Record<SubAgentKind, LucideIcon> = {\n DISCOVERY: Search,\n RENEWAL: RefreshCw,\n COST: DollarSign,\n NEWSLETTER: Mail,\n VENDOR: Building2,\n COMPLIANCE: ShieldCheck,\n NEGOTIATION: Handshake,\n MIGRATION: ArrowLeftRight,\n CONCIERGE: ConciergeBell,\n};\n\n/** Map a policy action to a timeline tone (semantic only). */\nfunction actionTone(action: SubPolicyAction): NonNullable<TimelineItem['tone']> {\n switch (action) {\n case 'RENEW':\n case 'UPGRADE':\n return 'positive';\n case 'CANCEL':\n case 'PAUSE':\n case 'DOWNGRADE':\n case 'ALERT':\n return 'warning';\n default:\n return 'default';\n }\n}\n\nconst inputCls =\n 'w-full rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]';\nconst labelCls = 'mb-1 block text-xs font-medium text-text-secondary';\n\n/** `unusedDays` / `min_monthly_spend` → \"Unused days\" / \"Min monthly spend\". */\nfunction humanizeConditionKey(key: string): string {\n const spaced = key\n .replace(/([a-z0-9])([A-Z])/g, '$1 $2')\n .replace(/[_-]+/g, ' ')\n .trim();\n return spaced.charAt(0).toUpperCase() + spaced.slice(1);\n}\n\n/** Render one condition value without losing information or inventing any. */\nfunction formatConditionValue(value: unknown): string {\n if (value === null || value === undefined) return '—';\n if (typeof value === 'string') return value;\n if (typeof value === 'number') return formatNumber(value);\n if (typeof value === 'boolean') return value ? 'Yes' : 'No';\n if (Array.isArray(value)) return value.map(formatConditionValue).join(', ');\n return JSON.stringify(value);\n}\n\n/**\n * Flatten a policy's real `condition` JSON into readable chips. The svc types\n * it as `JSON!` (DB default `{}`), so anything that is not a plain object —\n * or an empty one — yields no chips and the card simply omits the section.\n */\nfunction conditionEntries(\n condition: Record<string, unknown> | null | undefined\n): Array<{ key: string; value: string }> {\n if (!condition || Array.isArray(condition)) return [];\n return Object.entries(condition)\n .filter(([, value]) => value !== null && value !== undefined && value !== '')\n .map(([key, value]) => ({ key, value: formatConditionValue(value) }));\n}\n\n// ---------------------------------------------------------------------------\n// Read-only enabled indicator (there is no policy-toggle mutation on the svc\n// contract, so this reflects the real `isEnabled` state without pretending to\n// mutate it).\n// ---------------------------------------------------------------------------\nfunction EnabledPill({ enabled }: { enabled: boolean }) {\n const tr = useTr();\n return (\n <span\n className={\n enabled\n ? 'inline-flex items-center gap-1.5 rounded-full bg-status-success-bg-subtle px-2.5 py-0.5 text-xs font-medium text-status-success-text'\n : 'inline-flex items-center gap-1.5 rounded-full bg-bg-sunken px-2.5 py-0.5 text-xs font-medium text-text-secondary'\n }\n >\n <span\n aria-hidden\n className=\"size-1.5 rounded-full bg-current\"\n style={{ opacity: enabled ? 1 : 0.6 }}\n />\n {enabled ? tr('agents.detail.enabled', 'Enabled') : tr('agents.detail.disabled', 'Disabled')}\n </span>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Add policy modal → subCreatePolicy\n// ---------------------------------------------------------------------------\n\nfunction AddPolicyModal({ agentId, onClose }: { agentId: string; onClose: () => void }) {\n const tr = useTr();\n const create = useCreatePolicy();\n const [name, setName] = useState('');\n const [action, setAction] = useState<SubPolicyAction>('ALERT');\n const [description, setDescription] = useState('');\n const [condition, setCondition] = useState('');\n const [isEnabled, setIsEnabled] = useState(true);\n const [formError, setFormError] = useState<string | null>(null);\n\n const handleSubmit = (e: React.FormEvent) => {\n e.preventDefault();\n setFormError(null);\n if (!name.trim()) {\n setFormError(tr('agents.policyModal.nameRequired', 'A policy name is required.'));\n return;\n }\n let parsedCondition: Record<string, unknown> | undefined;\n if (condition.trim()) {\n try {\n parsedCondition = JSON.parse(condition) as Record<string, unknown>;\n } catch {\n setFormError(\n tr(\n 'agents.policyModal.conditionInvalid',\n 'Condition must be valid JSON (e.g. {\"amountOver\": 100}).'\n )\n );\n return;\n }\n }\n create.mutate(\n {\n agentId,\n name: name.trim(),\n action,\n isEnabled,\n ...(description.trim() ? { description: description.trim() } : {}),\n ...(parsedCondition ? { condition: parsedCondition } : {}),\n },\n {\n onSuccess: () => onClose(),\n onError: (err) =>\n setFormError(\n err instanceof Error\n ? err.message\n : tr('agents.policyModal.error', 'Could not add policy.')\n ),\n }\n );\n };\n\n return (\n <Modal\n title={tr('agents.policyModal.title', 'Add policy')}\n onClose={onClose}\n maxWidthClassName=\"max-w-xl\"\n >\n <form onSubmit={handleSubmit} className=\"space-y-4\">\n {formError && (\n <div className=\"rounded-lg border border-status-error-text/30 bg-status-error-bg-subtle px-4 py-2 text-sm text-status-error-text\">\n {formError}\n </div>\n )}\n <div>\n <label htmlFor=\"policy-name\" className={labelCls}>\n {tr('agents.policyModal.nameLabel', 'Name')}{' '}\n <span className=\"text-status-error-text\">*</span>\n </label>\n <input\n id=\"policy-name\"\n type=\"text\"\n value={name}\n onChange={(e) => setName(e.target.value)}\n placeholder={tr(\n 'agents.policyModal.namePlaceholder',\n 'e.g. Alert on price hike over 20%'\n )}\n className={inputCls}\n required\n />\n </div>\n <div>\n <label htmlFor=\"policy-action\" className={labelCls}>\n {tr('agents.policyModal.actionLabel', 'Action')}\n </label>\n <select\n id=\"policy-action\"\n value={action}\n onChange={(e) => setAction(e.target.value as SubPolicyAction)}\n className={inputCls}\n >\n {POLICY_ACTIONS.map((a) => (\n <option key={a} value={a}>\n {formatEnumLabel(a)}\n </option>\n ))}\n </select>\n </div>\n <div>\n <label htmlFor=\"policy-condition\" className={labelCls}>\n {tr('agents.policyModal.conditionLabel', 'Condition')}{' '}\n <span className=\"text-text-secondary\">\n ({tr('agents.policyModal.conditionOptional', 'optional JSON')})\n </span>\n </label>\n <textarea\n id=\"policy-condition\"\n value={condition}\n onChange={(e) => setCondition(e.target.value)}\n rows={3}\n placeholder={tr(\n 'agents.policyModal.conditionPlaceholder',\n '{ \"priceIncreasePct\": 20 }'\n )}\n className={`${inputCls} font-mono`}\n />\n </div>\n <div>\n <label htmlFor=\"policy-desc\" className={labelCls}>\n {tr('agents.policyModal.descriptionLabel', 'Description')}\n </label>\n <textarea\n id=\"policy-desc\"\n value={description}\n onChange={(e) => setDescription(e.target.value)}\n rows={2}\n className={inputCls}\n />\n </div>\n <label className=\"flex items-center gap-2 text-sm text-text-primary\">\n <input\n type=\"checkbox\"\n checked={isEnabled}\n onChange={(e) => setIsEnabled(e.target.checked)}\n className=\"size-4 rounded border-border-subtle\"\n />\n {tr('agents.policyModal.enabled', 'Enabled')}\n </label>\n <div className=\"flex justify-end gap-3\">\n <button\n type=\"button\"\n onClick={onClose}\n className=\"rounded-lg border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken\"\n >\n {tr('agents.policyModal.cancel', 'Cancel')}\n </button>\n <button\n type=\"submit\"\n disabled={create.isPending}\n className=\"rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:opacity-90 disabled:opacity-50\"\n >\n {create.isPending\n ? tr('agents.policyModal.adding', 'Adding…')\n : tr('agents.policyModal.submit', 'Add policy')}\n </button>\n </div>\n </form>\n </Modal>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Policy card\n// ---------------------------------------------------------------------------\nfunction PolicyCard({ policy }: { policy: SubPolicy }) {\n const tr = useTr();\n const conditions = conditionEntries(policy.condition);\n return (\n <GlassCard treatment=\"metric\" className=\"flex flex-col p-4\">\n <div className=\"flex items-start justify-between gap-2\">\n <p className=\"text-sm font-semibold text-text-primary\">{policy.name}</p>\n <EnabledPill enabled={policy.isEnabled} />\n </div>\n <div className=\"mt-2 flex flex-wrap items-center gap-2\">\n <span className=\"inline-flex items-center gap-1.5 rounded-md bg-[var(--color-accent-soft)] px-2 py-0.5 text-[11px] font-medium text-text-primary\">\n <ShieldCheck className=\"h-3 w-3\" aria-hidden /> {formatEnumLabel(policy.action)}\n </span>\n <span className=\"inline-flex items-center gap-1 rounded-md bg-bg-sunken px-2 py-0.5 text-[11px] font-medium text-text-secondary tabular-nums\">\n {tr('agents.detail.triggered', 'Triggered {{count}}×', { count: policy.triggerCount })}\n </span>\n </div>\n {policy.description && (\n <p className=\"mt-2 line-clamp-2 text-xs text-text-secondary\">{policy.description}</p>\n )}\n {conditions.length > 0 && (\n <div className=\"mt-3 border-t border-border-subtle pt-2.5\">\n <p className=\"flex items-center gap-1.5 text-[10px] font-medium uppercase tracking-wide text-text-secondary\">\n <SlidersHorizontal className=\"h-3 w-3\" aria-hidden />{' '}\n {tr('agents.detail.firesWhen', 'Fires when')}\n </p>\n <div className=\"mt-1.5 flex flex-wrap gap-1.5\">\n {conditions.map((entry) => (\n <span\n key={entry.key}\n className=\"inline-flex items-center gap-1.5 rounded-md border border-border-subtle bg-bg-sunken px-2 py-0.5 text-[11px] text-text-secondary\"\n >\n <span className=\"font-medium text-text-primary\">\n {humanizeConditionKey(entry.key)}\n </span>\n <span className=\"font-mono tabular-nums\">{entry.value}</span>\n </span>\n ))}\n </div>\n </div>\n )}\n <p className=\"mt-2 flex items-center gap-1.5 text-xs text-text-secondary\">\n <Clock className=\"h-3.5 w-3.5\" aria-hidden />\n {policy.lastTriggeredAt\n ? tr('agents.detail.triggered', 'Triggered {{count}}×', {\n count: policy.triggerCount,\n })\n : tr('agents.detail.notYetTriggered', 'Not yet triggered')}\n </p>\n </GlassCard>\n );\n}\n\nexport function AgentDetailPage() {\n const tr = useTr();\n const { id } = useParams<{ id: string }>();\n const { navigateTo } = useSubscriberBot();\n const { data: agent, isLoading, isError, refetch } = useAgent(id);\n const setAgentStatus = useSetAgentStatus();\n const autonomousAvailable = useAutonomousAgentMode();\n const [actionError, setActionError] = useState<string | null>(null);\n const [showAddPolicy, setShowAddPolicy] = useState(false);\n\n if (isLoading) {\n return (\n <PageLayout title={tr('agents.detail.loading', 'Loading agent…')}>\n <LoadingView label={tr('agents.detail.loading', 'Loading agent…')} />\n </PageLayout>\n );\n }\n\n if (isError) {\n return (\n <PageLayout title={tr('agents.title', 'AI Agents')}>\n <ErrorView\n message={tr('agents.detail.error', 'Could not load this agent.')}\n onRetry={() => void refetch()}\n />\n </PageLayout>\n );\n }\n\n if (!agent) {\n return (\n <PageLayout title={tr('agents.title', 'AI Agents')}>\n <EmptyState\n illustration=\"empty-data\"\n title={tr('agents.detail.notFoundTitle', 'Agent not found')}\n description={tr(\n 'agents.detail.notFoundDescription',\n 'It may have been removed, or you may not have access to it.'\n )}\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('/agents')}\n className=\"rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:opacity-90\"\n >\n {tr('agents.detail.backToAgents', 'Back to agents')}\n </button>\n }\n />\n </PageLayout>\n );\n }\n\n const a = agent;\n const Icon = KIND_ICON[a.kind] ?? Bot;\n const policies = a.policies ?? [];\n const enabledCount = policies.filter((p) => p.isEnabled).length;\n const totalTriggers = policies.reduce((s, p) => s + (p.triggerCount || 0), 0);\n\n // --- Run history: derived from real timestamps, sorted most-recent first --\n const events: Array<{ ts: number; item: TimelineItem }> = [];\n if (a.lastRunAt) {\n events.push({\n ts: new Date(a.lastRunAt).getTime(),\n item: {\n title: tr('agents.detail.agentRun', 'Agent run'),\n sub: tr('agents.detail.agentRunSub', 'Latest autonomous cycle'),\n meta: formatRelativeTime(a.lastRunAt),\n tone: 'positive',\n },\n });\n }\n for (const p of policies) {\n if (p.lastTriggeredAt) {\n events.push({\n ts: new Date(p.lastTriggeredAt).getTime(),\n item: {\n title: p.name,\n sub: `${formatEnumLabel(p.action)} · ${tr('agents.detail.triggered', 'Triggered {{count}}×', { count: p.triggerCount })}`,\n meta: formatRelativeTime(p.lastTriggeredAt),\n tone: actionTone(p.action),\n },\n });\n }\n }\n events.sort((x, y) => y.ts - x.ts);\n const timelineItems = events.map((e) => e.item);\n\n return (\n <PageLayout\n title={a.name}\n description={`${tr(KIND_KEY[a.kind] ?? '', formatEnumLabel(a.kind))} ${tr('agents.card.kindSuffix', 'agent')}`}\n actions={\n <button\n type=\"button\"\n onClick={() => navigateTo('/agents')}\n className=\"inline-flex items-center gap-2 rounded-lg border border-border-subtle px-3 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken\"\n >\n <ArrowLeft className=\"h-4 w-4\" /> {tr('agents.new.back', 'Back')}\n </button>\n }\n >\n {/* Identity + posture header */}\n <GlassCard treatment=\"glass\" glow={a.status === 'AUTONOMOUS'} className=\"p-5\">\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between\">\n <div className=\"flex items-start gap-4\">\n <span className=\"flex size-12 shrink-0 items-center justify-center rounded-xl bg-[var(--color-accent-soft)] text-text-primary\">\n <Icon className=\"h-6 w-6\" />\n </span>\n <div className=\"min-w-0\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <p className=\"text-lg font-semibold text-text-primary\">{a.name}</p>\n <StatusBadge status={a.status} />\n </div>\n <p className=\"mt-1 flex items-center gap-1.5 text-xs text-text-secondary\">\n <Clock className=\"h-3.5 w-3.5\" aria-hidden />\n {a.lastRunAt\n ? tr('agents.card.lastRun', 'Last run {{when}}', {\n when: formatRelativeTime(a.lastRunAt),\n })\n : tr('agents.card.neverRun', 'Never run')}\n </p>\n {a.description && (\n <p className=\"mt-2 max-w-2xl text-sm text-text-secondary\">{a.description}</p>\n )}\n </div>\n </div>\n\n {/* Posture control */}\n <div className=\"w-full sm:w-56 sm:shrink-0\">\n <label\n htmlFor=\"agent-posture\"\n className=\"mb-1 block text-xs font-medium text-text-secondary\"\n >\n {tr('agents.detail.operatingPosture', 'Operating posture')}\n </label>\n <select\n id=\"agent-posture\"\n value={a.status}\n disabled={setAgentStatus.isPending}\n onChange={(e) => {\n setActionError(null);\n setAgentStatus.mutate(\n { id: a.id, status: e.target.value as SubAgentStatus },\n {\n onError: (err) =>\n setActionError(\n err instanceof Error\n ? err.message\n : tr('agents.detail.error', 'Could not load this agent.')\n ),\n }\n );\n }}\n className=\"w-full rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] disabled:opacity-50\"\n >\n <PostureOptions options={STATUS_OPTIONS} currentStatus={a.status} />\n </select>\n <p className=\"mt-1.5 text-[11px] leading-snug text-text-secondary\">\n {autonomousAvailable\n ? tr(\n 'agents.detail.postureHint',\n 'Disabled never acts · Suggest-only proposes · Autonomous acts within its policies.'\n )\n : tr(\n 'agents.detail.postureHintGated',\n 'Disabled never acts · Suggest-only proposes. Autonomous mode is not available yet — agents only suggest, they never act on their own.'\n )}\n </p>\n </div>\n </div>\n </GlassCard>\n\n {actionError && (\n <div className=\"mt-4 rounded-lg border border-status-error-text/30 bg-status-error-bg-subtle px-4 py-2 text-sm text-status-error-text\">\n {actionError}\n </div>\n )}\n\n {/* Policy stats (computed) */}\n <div className=\"mt-6 grid grid-cols-1 gap-4 sm:grid-cols-3\">\n <MetricsCard\n icon={<ListChecks className=\"h-5 w-5\" />}\n value={policies.length}\n label={tr('agents.detail.policies', 'Policies')}\n />\n <MetricsCard\n icon={<ShieldCheck className=\"h-5 w-5\" />}\n value={enabledCount}\n label={tr('agents.detail.enabled', 'Enabled')}\n />\n <MetricsCard\n icon={<Zap className=\"h-5 w-5\" />}\n value={totalTriggers}\n label={tr('agents.detail.totalTriggers', 'Total triggers')}\n />\n </div>\n\n {/* Policies + Run history */}\n <div className=\"mt-8 grid grid-cols-1 gap-6 lg:grid-cols-3\">\n <div className=\"lg:col-span-2\">\n <Section\n title={tr('agents.detail.policies', 'Policies')}\n description={tr(\n 'agents.detail.policiesDescription',\n 'The autonomous rules that bound this agent'\n )}\n action={\n <button\n type=\"button\"\n onClick={() => setShowAddPolicy(true)}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-subtle px-3 py-1.5 text-xs font-medium text-text-primary hover:bg-bg-sunken\"\n >\n <Plus className=\"h-3.5 w-3.5\" /> {tr('agents.detail.addPolicy', 'Add policy')}\n </button>\n }\n >\n {policies.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n title={tr('agents.detail.noPoliciesTitle', 'No policies')}\n description={tr(\n 'agents.detail.noPoliciesDescription',\n 'Policies define what this agent may do autonomously, and what it must alert on.'\n )}\n action={\n <button\n type=\"button\"\n onClick={() => setShowAddPolicy(true)}\n className=\"rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:opacity-90\"\n >\n {tr('agents.detail.addPolicy', 'Add policy')}\n </button>\n }\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-3 sm:grid-cols-2\">\n {policies.map((policy) => (\n <PolicyCard key={policy.id} policy={policy} />\n ))}\n </div>\n )}\n </Section>\n </div>\n\n <div className=\"lg:col-span-1\">\n <Section\n title={tr('agents.detail.runHistory', 'Run history')}\n description={tr('agents.detail.runHistoryDescription', 'Recent agent activity')}\n >\n <GlassCard treatment=\"metric\" className=\"p-5\">\n {timelineItems.length === 0 ? (\n <div className=\"flex flex-col items-center justify-center gap-2 py-6 text-center\">\n <History className=\"h-8 w-8 text-text-secondary\" aria-hidden />\n <p className=\"text-sm font-medium text-text-primary\">\n {tr('agents.detail.noRunsTitle', 'No runs yet')}\n </p>\n <p className=\"text-xs text-text-secondary\">\n {tr(\n 'agents.detail.noRunsDescription',\n 'Activity appears here once the agent runs or a policy fires.'\n )}\n </p>\n </div>\n ) : (\n <Timeline items={timelineItems} />\n )}\n </GlassCard>\n </Section>\n </div>\n </div>\n\n {showAddPolicy && <AddPolicyModal agentId={a.id} onClose={() => setShowAddPolicy(false)} />}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"AgentDetailPage.js","names":[],"sources":["../../../../src/subscriberbot/pages/agents/AgentDetailPage.tsx"],"sourcesContent":["/**\n * Agent detail\n * @module subscriberbot/pages/agents\n *\n * A single AI agent: its kind, posture, last run, and the autonomous policies\n * that bound it. Reads `subAgent(id)`; changes posture via `subSetAgentStatus`.\n * The \"Run history\" timeline is derived from the agent's real `lastRunAt` and\n * each policy's real `lastTriggeredAt` — never mocked. Each policy card also\n * renders its stored `condition` JSON (the rule the agent evaluates before it\n * acts), so a condition entered in \"Add policy\" is visible after it is saved.\n *\n * BOFF-3663: the page is also the agent's operations view. \"Run agent\" calls\n * `subRunAgent` as a dry run (decisions are proposed for review, nothing\n * changes), the run history includes each real decision run, and the agent's\n * decisions are listed as cards that can be applied or dismissed. If the\n * decision engine is unreachable, only that section degrades to a banner; the\n * posture and policy management above it keep working.\n */\n\nimport { useState } from 'react';\nimport { useParams } from 'react-router';\nimport {\n ArrowLeft,\n Bot,\n Search,\n RefreshCw,\n DollarSign,\n Mail,\n Building2,\n ShieldCheck,\n Handshake,\n ArrowLeftRight,\n ConciergeBell,\n Plus,\n Clock,\n Zap,\n ListChecks,\n History,\n SlidersHorizontal,\n Play,\n ArrowRight,\n type LucideIcon,\n} from 'lucide-react';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport {\n PageLayout,\n StatusBadge,\n EmptyState,\n Modal,\n MetricsCard,\n Timeline,\n LoadingView,\n ErrorView,\n Section,\n type TimelineItem,\n} from '../../components';\nimport { useSubscriberBot } from '../../context/SubscriberBotContext';\nimport {\n useAgent,\n useSetAgentStatus,\n useCreatePolicy,\n useDecisions,\n useRunAgent,\n} from '../../hooks/useSubscriberBotApi';\nimport {\n AiDisclaimer,\n DecisionCard,\n DecisionsUnavailableBanner,\n RUNNABLE_AGENT_KINDS,\n} from '../../components/decisions';\nimport { useTr } from '../../hooks/useTr';\nimport { useAutonomousAgentMode } from '../../hooks/useAutonomousAgentMode';\nimport { PostureOptions } from '../../components/PostureOptions';\nimport { formatEnumLabel, formatNumber, formatRelativeTime } from '../../utils/formatters';\nimport type {\n SubAgentKind,\n SubAgentRunResult,\n SubAgentStatus,\n SubDecision,\n SubPolicyAction,\n SubPolicy,\n} from '../../types';\n\n/** One decision run, reconstructed from the decisions it produced. */\ninterface DecisionRunSummary {\n runId: string;\n ranAt: string;\n total: number;\n proposed: number;\n flagged: number;\n}\n\nfunction summariseRuns(decisions: SubDecision[]): DecisionRunSummary[] {\n const runs = new Map<string, DecisionRunSummary>();\n for (const d of decisions) {\n const run = runs.get(d.runId) ?? {\n runId: d.runId,\n ranAt: d.createdAt,\n total: 0,\n proposed: 0,\n flagged: 0,\n };\n run.total += 1;\n if (d.status === 'PROPOSED') run.proposed += 1;\n if (d.status === 'FLAGGED') run.flagged += 1;\n if (d.createdAt < run.ranAt) run.ranAt = d.createdAt;\n runs.set(d.runId, run);\n }\n return [...runs.values()];\n}\n\nconst STATUS_OPTIONS: SubAgentStatus[] = ['DISABLED', 'SUGGEST_ONLY', 'AUTONOMOUS'];\n\nconst KIND_KEY: Record<SubAgentKind, string> = {\n DISCOVERY: 'agents.kinds.discovery',\n RENEWAL: 'agents.kinds.renewal',\n COST: 'agents.kinds.cost',\n NEWSLETTER: 'agents.kinds.newsletter',\n VENDOR: 'agents.kinds.vendor',\n COMPLIANCE: 'agents.kinds.compliance',\n NEGOTIATION: 'agents.kinds.negotiation',\n MIGRATION: 'agents.kinds.migration',\n CONCIERGE: 'agents.kinds.concierge',\n};\n\nconst POLICY_ACTIONS: SubPolicyAction[] = [\n 'ALERT',\n 'CREATE_TASK',\n 'RENEW',\n 'CANCEL',\n 'PAUSE',\n 'UPGRADE',\n 'DOWNGRADE',\n];\n\nconst KIND_ICON: Record<SubAgentKind, LucideIcon> = {\n DISCOVERY: Search,\n RENEWAL: RefreshCw,\n COST: DollarSign,\n NEWSLETTER: Mail,\n VENDOR: Building2,\n COMPLIANCE: ShieldCheck,\n NEGOTIATION: Handshake,\n MIGRATION: ArrowLeftRight,\n CONCIERGE: ConciergeBell,\n};\n\n/** Map a policy action to a timeline tone (semantic only). */\nfunction actionTone(action: SubPolicyAction): NonNullable<TimelineItem['tone']> {\n switch (action) {\n case 'RENEW':\n case 'UPGRADE':\n return 'positive';\n case 'CANCEL':\n case 'PAUSE':\n case 'DOWNGRADE':\n case 'ALERT':\n return 'warning';\n default:\n return 'default';\n }\n}\n\nconst inputCls =\n 'w-full rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]';\nconst labelCls = 'mb-1 block text-xs font-medium text-text-secondary';\n\n/** `unusedDays` / `min_monthly_spend` → \"Unused days\" / \"Min monthly spend\". */\nfunction humanizeConditionKey(key: string): string {\n const spaced = key\n .replace(/([a-z0-9])([A-Z])/g, '$1 $2')\n .replace(/[_-]+/g, ' ')\n .trim();\n return spaced.charAt(0).toUpperCase() + spaced.slice(1);\n}\n\n/** Render one condition value without losing information or inventing any. */\nfunction formatConditionValue(value: unknown): string {\n if (value === null || value === undefined) return '—';\n if (typeof value === 'string') return value;\n if (typeof value === 'number') return formatNumber(value);\n if (typeof value === 'boolean') return value ? 'Yes' : 'No';\n if (Array.isArray(value)) return value.map(formatConditionValue).join(', ');\n return JSON.stringify(value);\n}\n\n/**\n * Flatten a policy's real `condition` JSON into readable chips. The svc types\n * it as `JSON!` (DB default `{}`), so anything that is not a plain object —\n * or an empty one — yields no chips and the card simply omits the section.\n */\nfunction conditionEntries(\n condition: Record<string, unknown> | null | undefined\n): Array<{ key: string; value: string }> {\n if (!condition || Array.isArray(condition)) return [];\n return Object.entries(condition)\n .filter(([, value]) => value !== null && value !== undefined && value !== '')\n .map(([key, value]) => ({ key, value: formatConditionValue(value) }));\n}\n\n// ---------------------------------------------------------------------------\n// Read-only enabled indicator (there is no policy-toggle mutation on the svc\n// contract, so this reflects the real `isEnabled` state without pretending to\n// mutate it).\n// ---------------------------------------------------------------------------\nfunction EnabledPill({ enabled }: { enabled: boolean }) {\n const tr = useTr();\n return (\n <span\n className={\n enabled\n ? 'inline-flex items-center gap-1.5 rounded-full bg-status-success-bg-subtle px-2.5 py-0.5 text-xs font-medium text-status-success-text'\n : 'inline-flex items-center gap-1.5 rounded-full bg-bg-sunken px-2.5 py-0.5 text-xs font-medium text-text-secondary'\n }\n >\n <span\n aria-hidden\n className=\"size-1.5 rounded-full bg-current\"\n style={{ opacity: enabled ? 1 : 0.6 }}\n />\n {enabled ? tr('agents.detail.enabled', 'Enabled') : tr('agents.detail.disabled', 'Disabled')}\n </span>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Add policy modal → subCreatePolicy\n// ---------------------------------------------------------------------------\n\nfunction AddPolicyModal({ agentId, onClose }: { agentId: string; onClose: () => void }) {\n const tr = useTr();\n const create = useCreatePolicy();\n const [name, setName] = useState('');\n const [action, setAction] = useState<SubPolicyAction>('ALERT');\n const [description, setDescription] = useState('');\n const [condition, setCondition] = useState('');\n const [isEnabled, setIsEnabled] = useState(true);\n const [formError, setFormError] = useState<string | null>(null);\n\n const handleSubmit = (e: React.FormEvent) => {\n e.preventDefault();\n setFormError(null);\n if (!name.trim()) {\n setFormError(tr('agents.policyModal.nameRequired', 'A policy name is required.'));\n return;\n }\n let parsedCondition: Record<string, unknown> | undefined;\n if (condition.trim()) {\n try {\n parsedCondition = JSON.parse(condition) as Record<string, unknown>;\n } catch {\n setFormError(\n tr(\n 'agents.policyModal.conditionInvalid',\n 'Condition must be valid JSON (e.g. {\"amountOver\": 100}).'\n )\n );\n return;\n }\n }\n create.mutate(\n {\n agentId,\n name: name.trim(),\n action,\n isEnabled,\n ...(description.trim() ? { description: description.trim() } : {}),\n ...(parsedCondition ? { condition: parsedCondition } : {}),\n },\n {\n onSuccess: () => onClose(),\n onError: (err) =>\n setFormError(\n err instanceof Error\n ? err.message\n : tr('agents.policyModal.error', 'Could not add policy.')\n ),\n }\n );\n };\n\n return (\n <Modal\n title={tr('agents.policyModal.title', 'Add policy')}\n onClose={onClose}\n maxWidthClassName=\"max-w-xl\"\n >\n <form onSubmit={handleSubmit} className=\"space-y-4\">\n {formError && (\n <div className=\"rounded-lg border border-status-error-text/30 bg-status-error-bg-subtle px-4 py-2 text-sm text-status-error-text\">\n {formError}\n </div>\n )}\n <div>\n <label htmlFor=\"policy-name\" className={labelCls}>\n {tr('agents.policyModal.nameLabel', 'Name')}{' '}\n <span className=\"text-status-error-text\">*</span>\n </label>\n <input\n id=\"policy-name\"\n type=\"text\"\n value={name}\n onChange={(e) => setName(e.target.value)}\n placeholder={tr(\n 'agents.policyModal.namePlaceholder',\n 'e.g. Alert on price hike over 20%'\n )}\n className={inputCls}\n required\n />\n </div>\n <div>\n <label htmlFor=\"policy-action\" className={labelCls}>\n {tr('agents.policyModal.actionLabel', 'Action')}\n </label>\n <select\n id=\"policy-action\"\n value={action}\n onChange={(e) => setAction(e.target.value as SubPolicyAction)}\n className={inputCls}\n >\n {POLICY_ACTIONS.map((a) => (\n <option key={a} value={a}>\n {formatEnumLabel(a)}\n </option>\n ))}\n </select>\n </div>\n <div>\n <label htmlFor=\"policy-condition\" className={labelCls}>\n {tr('agents.policyModal.conditionLabel', 'Condition')}{' '}\n <span className=\"text-text-secondary\">\n ({tr('agents.policyModal.conditionOptional', 'optional JSON')})\n </span>\n </label>\n <textarea\n id=\"policy-condition\"\n value={condition}\n onChange={(e) => setCondition(e.target.value)}\n rows={3}\n placeholder={tr(\n 'agents.policyModal.conditionPlaceholder',\n '{ \"priceIncreasePct\": 20 }'\n )}\n className={`${inputCls} font-mono`}\n />\n </div>\n <div>\n <label htmlFor=\"policy-desc\" className={labelCls}>\n {tr('agents.policyModal.descriptionLabel', 'Description')}\n </label>\n <textarea\n id=\"policy-desc\"\n value={description}\n onChange={(e) => setDescription(e.target.value)}\n rows={2}\n className={inputCls}\n />\n </div>\n <label className=\"flex items-center gap-2 text-sm text-text-primary\">\n <input\n type=\"checkbox\"\n checked={isEnabled}\n onChange={(e) => setIsEnabled(e.target.checked)}\n className=\"size-4 rounded border-border-subtle\"\n />\n {tr('agents.policyModal.enabled', 'Enabled')}\n </label>\n <div className=\"flex justify-end gap-3\">\n <button\n type=\"button\"\n onClick={onClose}\n className=\"rounded-lg border border-border-subtle px-4 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken\"\n >\n {tr('agents.policyModal.cancel', 'Cancel')}\n </button>\n <button\n type=\"submit\"\n disabled={create.isPending}\n className=\"rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:opacity-90 disabled:opacity-50\"\n >\n {create.isPending\n ? tr('agents.policyModal.adding', 'Adding…')\n : tr('agents.policyModal.submit', 'Add policy')}\n </button>\n </div>\n </form>\n </Modal>\n );\n}\n\n// ---------------------------------------------------------------------------\n// Policy card\n// ---------------------------------------------------------------------------\nfunction PolicyCard({ policy }: { policy: SubPolicy }) {\n const tr = useTr();\n const conditions = conditionEntries(policy.condition);\n return (\n <GlassCard treatment=\"metric\" className=\"flex flex-col p-4\">\n <div className=\"flex items-start justify-between gap-2\">\n <p className=\"text-sm font-semibold text-text-primary\">{policy.name}</p>\n <EnabledPill enabled={policy.isEnabled} />\n </div>\n <div className=\"mt-2 flex flex-wrap items-center gap-2\">\n <span className=\"inline-flex items-center gap-1.5 rounded-md bg-[var(--color-accent-soft)] px-2 py-0.5 text-[11px] font-medium text-text-primary\">\n <ShieldCheck className=\"h-3 w-3\" aria-hidden /> {formatEnumLabel(policy.action)}\n </span>\n <span className=\"inline-flex items-center gap-1 rounded-md bg-bg-sunken px-2 py-0.5 text-[11px] font-medium text-text-secondary tabular-nums\">\n {tr('agents.detail.triggered', 'Triggered {{count}}×', { count: policy.triggerCount })}\n </span>\n </div>\n {policy.description && (\n <p className=\"mt-2 line-clamp-2 text-xs text-text-secondary\">{policy.description}</p>\n )}\n {conditions.length > 0 && (\n <div className=\"mt-3 border-t border-border-subtle pt-2.5\">\n <p className=\"flex items-center gap-1.5 text-[10px] font-medium uppercase tracking-wide text-text-secondary\">\n <SlidersHorizontal className=\"h-3 w-3\" aria-hidden />{' '}\n {tr('agents.detail.firesWhen', 'Fires when')}\n </p>\n <div className=\"mt-1.5 flex flex-wrap gap-1.5\">\n {conditions.map((entry) => (\n <span\n key={entry.key}\n className=\"inline-flex items-center gap-1.5 rounded-md border border-border-subtle bg-bg-sunken px-2 py-0.5 text-[11px] text-text-secondary\"\n >\n <span className=\"font-medium text-text-primary\">\n {humanizeConditionKey(entry.key)}\n </span>\n <span className=\"font-mono tabular-nums\">{entry.value}</span>\n </span>\n ))}\n </div>\n </div>\n )}\n <p className=\"mt-2 flex items-center gap-1.5 text-xs text-text-secondary\">\n <Clock className=\"h-3.5 w-3.5\" aria-hidden />\n {policy.lastTriggeredAt\n ? tr('agents.detail.triggered', 'Triggered {{count}}×', {\n count: policy.triggerCount,\n })\n : tr('agents.detail.notYetTriggered', 'Not yet triggered')}\n </p>\n </GlassCard>\n );\n}\n\nexport function AgentDetailPage() {\n const tr = useTr();\n const { id } = useParams<{ id: string }>();\n const { navigateTo } = useSubscriberBot();\n const { data: agent, isLoading, isError, refetch } = useAgent(id);\n const setAgentStatus = useSetAgentStatus();\n const autonomousAvailable = useAutonomousAgentMode();\n const [actionError, setActionError] = useState<string | null>(null);\n const [showAddPolicy, setShowAddPolicy] = useState(false);\n const runAgent = useRunAgent();\n const [runResult, setRunResult] = useState<SubAgentRunResult | null>(null);\n const [runError, setRunError] = useState<string | null>(null);\n const agentDecisions = useDecisions({ agentId: id }, { enabled: Boolean(id) });\n\n if (isLoading) {\n return (\n <PageLayout title={tr('agents.detail.loading', 'Loading agent…')}>\n <LoadingView label={tr('agents.detail.loading', 'Loading agent…')} />\n </PageLayout>\n );\n }\n\n if (isError) {\n return (\n <PageLayout title={tr('agents.title', 'AI Agents')}>\n <ErrorView\n message={tr('agents.detail.error', 'Could not load this agent.')}\n onRetry={() => void refetch()}\n />\n </PageLayout>\n );\n }\n\n if (!agent) {\n return (\n <PageLayout title={tr('agents.title', 'AI Agents')}>\n <EmptyState\n illustration=\"empty-data\"\n title={tr('agents.detail.notFoundTitle', 'Agent not found')}\n description={tr(\n 'agents.detail.notFoundDescription',\n 'It may have been removed, or you may not have access to it.'\n )}\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('/agents')}\n className=\"rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:opacity-90\"\n >\n {tr('agents.detail.backToAgents', 'Back to agents')}\n </button>\n }\n />\n </PageLayout>\n );\n }\n\n const a = agent;\n const Icon = KIND_ICON[a.kind] ?? Bot;\n const policies = a.policies ?? [];\n const enabledCount = policies.filter((p) => p.isEnabled).length;\n const totalTriggers = policies.reduce((s, p) => s + (p.triggerCount || 0), 0);\n\n // --- Run history: derived from real timestamps, sorted most-recent first --\n const events: Array<{ ts: number; item: TimelineItem }> = [];\n if (a.lastRunAt) {\n events.push({\n ts: new Date(a.lastRunAt).getTime(),\n item: {\n title: tr('agents.detail.agentRun', 'Agent run'),\n sub: tr('agents.detail.agentRunSub', 'Latest autonomous cycle'),\n meta: formatRelativeTime(a.lastRunAt),\n tone: 'positive',\n },\n });\n }\n for (const p of policies) {\n if (p.lastTriggeredAt) {\n events.push({\n ts: new Date(p.lastTriggeredAt).getTime(),\n item: {\n title: p.name,\n sub: `${formatEnumLabel(p.action)} · ${tr('agents.detail.triggered', 'Triggered {{count}}×', { count: p.triggerCount })}`,\n meta: formatRelativeTime(p.lastTriggeredAt),\n tone: actionTone(p.action),\n },\n });\n }\n }\n const decisionList = agentDecisions.data?.items ?? [];\n for (const run of summariseRuns(decisionList)) {\n events.push({\n ts: new Date(run.ranAt).getTime(),\n item: {\n title: tr('agents.detail.decisionRun', 'Decision run'),\n sub: tr(\n 'agents.detail.decisionRunSub',\n '{{total}} recommendations · {{proposed}} open · {{flagged}} flagged',\n { total: run.total, proposed: run.proposed, flagged: run.flagged }\n ),\n meta: formatRelativeTime(run.ranAt),\n tone: run.flagged > 0 ? 'warning' : 'positive',\n },\n });\n }\n events.sort((x, y) => y.ts - x.ts);\n\n const runnable = RUNNABLE_AGENT_KINDS.has(a.kind);\n const runBlockedReason = !runnable\n ? tr(\n 'agents.detail.runUnsupported',\n 'This agent kind does not make decisions yet. Cost, renewal, vendor and discovery agents do.'\n )\n : a.status === 'DISABLED'\n ? tr('agents.detail.runDisabled', 'Set the posture to Suggest-only to run this agent.')\n : null;\n const handleRun = () => {\n setRunError(null);\n setRunResult(null);\n runAgent.mutate(a.id, {\n onSuccess: (result) => setRunResult(result),\n onError: (err) =>\n setRunError(\n err instanceof Error && err.message\n ? err.message\n : tr('agents.detail.runError', 'The agent run failed.')\n ),\n });\n };\n const timelineItems = events.map((e) => e.item);\n\n return (\n <PageLayout\n title={a.name}\n description={`${tr(KIND_KEY[a.kind] ?? '', formatEnumLabel(a.kind))} ${tr('agents.card.kindSuffix', 'agent')}`}\n actions={\n <div className=\"flex flex-wrap gap-2\">\n <button\n type=\"button\"\n onClick={() => navigateTo('/agents')}\n className=\"inline-flex items-center gap-2 rounded-lg border border-border-subtle px-3 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken\"\n >\n <ArrowLeft className=\"h-4 w-4\" /> {tr('agents.new.back', 'Back')}\n </button>\n <button\n type=\"button\"\n onClick={handleRun}\n disabled={runAgent.isPending || runBlockedReason !== null}\n title={runBlockedReason ?? undefined}\n data-testid=\"run-agent\"\n className=\"inline-flex items-center gap-2 rounded-lg bg-action-primary-bg px-3 py-2 text-sm font-medium text-action-primary-text hover:opacity-90 disabled:cursor-not-allowed disabled:opacity-50\"\n >\n <Play className=\"h-4 w-4\" aria-hidden />\n {runAgent.isPending\n ? tr('agents.detail.running', 'Running…')\n : tr('agents.detail.run', 'Run agent')}\n </button>\n </div>\n }\n >\n {/* Identity + posture header */}\n <GlassCard treatment=\"glass\" glow={a.status === 'AUTONOMOUS'} className=\"p-5\">\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between\">\n <div className=\"flex items-start gap-4\">\n <span className=\"flex size-12 shrink-0 items-center justify-center rounded-xl bg-[var(--color-accent-soft)] text-text-primary\">\n <Icon className=\"h-6 w-6\" />\n </span>\n <div className=\"min-w-0\">\n <div className=\"flex flex-wrap items-center gap-2\">\n <p className=\"text-lg font-semibold text-text-primary\">{a.name}</p>\n <StatusBadge status={a.status} />\n </div>\n <p className=\"mt-1 flex items-center gap-1.5 text-xs text-text-secondary\">\n <Clock className=\"h-3.5 w-3.5\" aria-hidden />\n {a.lastRunAt\n ? tr('agents.card.lastRun', 'Last run {{when}}', {\n when: formatRelativeTime(a.lastRunAt),\n })\n : tr('agents.card.neverRun', 'Never run')}\n </p>\n {a.description && (\n <p className=\"mt-2 max-w-2xl text-sm text-text-secondary\">{a.description}</p>\n )}\n </div>\n </div>\n\n {/* Posture control */}\n <div className=\"w-full sm:w-56 sm:shrink-0\">\n <label\n htmlFor=\"agent-posture\"\n className=\"mb-1 block text-xs font-medium text-text-secondary\"\n >\n {tr('agents.detail.operatingPosture', 'Operating posture')}\n </label>\n <select\n id=\"agent-posture\"\n value={a.status}\n disabled={setAgentStatus.isPending}\n onChange={(e) => {\n setActionError(null);\n setAgentStatus.mutate(\n { id: a.id, status: e.target.value as SubAgentStatus },\n {\n onError: (err) =>\n setActionError(\n err instanceof Error\n ? err.message\n : tr('agents.detail.error', 'Could not load this agent.')\n ),\n }\n );\n }}\n className=\"w-full rounded-lg border border-border-subtle bg-bg-surface px-3 py-2 text-sm text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)] disabled:opacity-50\"\n >\n <PostureOptions options={STATUS_OPTIONS} currentStatus={a.status} />\n </select>\n <p className=\"mt-1.5 text-[11px] leading-snug text-text-secondary\">\n {autonomousAvailable\n ? tr(\n 'agents.detail.postureHint',\n 'Disabled never acts · Suggest-only proposes · Autonomous acts within its policies.'\n )\n : tr(\n 'agents.detail.postureHintGated',\n 'Disabled never acts · Suggest-only proposes. Autonomous mode is not available yet — agents only suggest, they never act on their own.'\n )}\n </p>\n </div>\n </div>\n </GlassCard>\n\n {actionError && (\n <div className=\"mt-4 rounded-lg border border-status-error-text/30 bg-status-error-bg-subtle px-4 py-2 text-sm text-status-error-text\">\n {actionError}\n </div>\n )}\n\n {/* Decision run outcome */}\n {runAgent.isPending && (\n <div\n role=\"status\"\n className=\"mt-4 rounded-lg border border-border-subtle bg-bg-sunken px-4 py-2 text-sm text-text-secondary\"\n >\n {tr(\n 'agents.detail.runProgress',\n 'Evaluating your active and trial subscriptions. Nothing is changed by this run.'\n )}\n </div>\n )}\n {runResult && (\n <div\n role=\"status\"\n data-testid=\"run-result\"\n className=\"mt-4 rounded-lg border border-status-success-text/30 bg-status-success-bg-subtle px-4 py-2 text-sm text-status-success-text\"\n >\n {tr(\n 'agents.detail.runResult',\n 'Run finished: {{evaluated}} subscriptions evaluated, {{proposed}} recommendations to review, {{flagged}} held back by validation.',\n {\n evaluated: runResult.evaluated,\n proposed: runResult.proposed,\n flagged: runResult.flagged,\n }\n )}\n {runResult.truncated &&\n ` ${tr('agents.detail.runTruncated', 'Only part of the portfolio was evaluated in this run.')}`}\n </div>\n )}\n {runError && (\n <div className=\"mt-4 rounded-lg border border-status-error-text/30 bg-status-error-bg-subtle px-4 py-2 text-sm text-status-error-text\">\n {runError}\n </div>\n )}\n {runBlockedReason && <p className=\"mt-3 text-xs text-text-secondary\">{runBlockedReason}</p>}\n\n {/* Policy stats (computed) */}\n <div className=\"mt-6 grid grid-cols-1 gap-4 sm:grid-cols-3\">\n <MetricsCard\n icon={<ListChecks className=\"h-5 w-5\" />}\n value={policies.length}\n label={tr('agents.detail.policies', 'Policies')}\n />\n <MetricsCard\n icon={<ShieldCheck className=\"h-5 w-5\" />}\n value={enabledCount}\n label={tr('agents.detail.enabled', 'Enabled')}\n />\n <MetricsCard\n icon={<Zap className=\"h-5 w-5\" />}\n value={totalTriggers}\n label={tr('agents.detail.totalTriggers', 'Total triggers')}\n />\n </div>\n\n {/* Policies + Run history */}\n <div className=\"mt-8 grid grid-cols-1 gap-6 lg:grid-cols-3\">\n <div className=\"lg:col-span-2\">\n <Section\n title={tr('agents.detail.policies', 'Policies')}\n description={tr(\n 'agents.detail.policiesDescription',\n 'The autonomous rules that bound this agent'\n )}\n action={\n <button\n type=\"button\"\n onClick={() => setShowAddPolicy(true)}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-subtle px-3 py-1.5 text-xs font-medium text-text-primary hover:bg-bg-sunken\"\n >\n <Plus className=\"h-3.5 w-3.5\" /> {tr('agents.detail.addPolicy', 'Add policy')}\n </button>\n }\n >\n {policies.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n title={tr('agents.detail.noPoliciesTitle', 'No policies')}\n description={tr(\n 'agents.detail.noPoliciesDescription',\n 'Policies define what this agent may do autonomously, and what it must alert on.'\n )}\n action={\n <button\n type=\"button\"\n onClick={() => setShowAddPolicy(true)}\n className=\"rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:opacity-90\"\n >\n {tr('agents.detail.addPolicy', 'Add policy')}\n </button>\n }\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-3 sm:grid-cols-2\">\n {policies.map((policy) => (\n <PolicyCard key={policy.id} policy={policy} />\n ))}\n </div>\n )}\n </Section>\n </div>\n\n <div className=\"lg:col-span-1\">\n <Section\n title={tr('agents.detail.runHistory', 'Run history')}\n description={tr('agents.detail.runHistoryDescription', 'Recent agent activity')}\n >\n <GlassCard treatment=\"metric\" className=\"p-5\">\n {timelineItems.length === 0 ? (\n <div className=\"flex flex-col items-center justify-center gap-2 py-6 text-center\">\n <History className=\"h-8 w-8 text-text-secondary\" aria-hidden />\n <p className=\"text-sm font-medium text-text-primary\">\n {tr('agents.detail.noRunsTitle', 'No runs yet')}\n </p>\n <p className=\"text-xs text-text-secondary\">\n {tr(\n 'agents.detail.noRunsDescription',\n 'Activity appears here once the agent runs or a policy fires.'\n )}\n </p>\n </div>\n ) : (\n <Timeline items={timelineItems} />\n )}\n </GlassCard>\n </Section>\n </div>\n </div>\n\n {/* Decisions this agent made (BOFF-3663) */}\n <Section\n title={tr('agents.detail.decisions', 'Decisions')}\n description={tr(\n 'agents.detail.decisionsDescription',\n 'Recommendations from this agent. Review, apply or dismiss each one.'\n )}\n action={\n <button\n type=\"button\"\n onClick={() => navigateTo('/decisions')}\n className=\"inline-flex items-center gap-1.5 rounded-lg border border-border-subtle px-3 py-1.5 text-xs font-medium text-text-primary hover:bg-bg-sunken\"\n >\n {tr('agents.detail.allDecisions', 'All decisions')}\n <ArrowRight className=\"h-3.5 w-3.5\" aria-hidden />\n </button>\n }\n >\n <AiDisclaimer className=\"mb-3\" />\n {agentDecisions.isLoading ? (\n <LoadingView\n className=\"min-h-[120px]\"\n label={tr('decisions.loading', 'Loading decisions…')}\n />\n ) : agentDecisions.isError ? (\n <DecisionsUnavailableBanner\n message={\n agentDecisions.error instanceof Error ? agentDecisions.error.message : undefined\n }\n onRetry={() => void agentDecisions.refetch()}\n />\n ) : decisionList.length === 0 ? (\n <EmptyState\n illustration=\"empty-data\"\n title={tr('agents.detail.noDecisionsTitle', 'No decisions yet')}\n description={\n runBlockedReason ??\n tr(\n 'agents.detail.noDecisionsDescription',\n 'Run the agent to get recommendations. A run only proposes; nothing changes until you apply.'\n )\n }\n />\n ) : (\n <div className=\"grid grid-cols-1 gap-3 lg:grid-cols-2\" data-testid=\"agent-decisions\">\n {decisionList.slice(0, 6).map((d) => (\n <DecisionCard key={d.id} decision={d} hideAgent />\n ))}\n </div>\n )}\n </Section>\n\n {showAddPolicy && <AddPolicyModal agentId={a.id} onClose={() => setShowAddPolicy(false)} />}\n </PageLayout>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Decision detail
3
+ * @module subscriberbot/pages/decisions
4
+ *
5
+ * `/subscriberbot/decisions/:id` — one recommendation in full: what it
6
+ * proposes, why, every source row it reasoned over (each a link to the page
7
+ * that shows it), the exact change applying it makes, and its provenance
8
+ * (agent, run, ledger entry). The assistant resolves this route as
9
+ * `SUBSCRIBERBOT_DECISION` so "apply this one" has an entity to act on.
10
+ */
11
+ export declare function DecisionDetailPage(): import("react").JSX.Element;
12
+ //# sourceMappingURL=DecisionDetailPage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DecisionDetailPage.d.ts","sourceRoot":"","sources":["../../../../src/subscriberbot/pages/decisions/DecisionDetailPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAoCH,wBAAgB,kBAAkB,gCAwKjC"}
@@ -0,0 +1,180 @@
1
+ import { useSubscriberBot as e } from "../../context/SubscriberBotContext.js";
2
+ import { PageLayout as t } from "../../components/PageLayout.js";
3
+ import { formatDateTime as n, formatEnumLabel as r } from "../../utils/formatters.js";
4
+ import { EmptyState as i } from "../../components/EmptyState.js";
5
+ import { useTr as a } from "../../hooks/useTr.js";
6
+ import { LoadingView as o, Section as s } from "../../components/StateViews.js";
7
+ import "../../components/index.js";
8
+ import { useDecision as c } from "../../hooks/useSubscriberBotApi.js";
9
+ import { DECISION_KIND_LABEL as l } from "../../components/decisions/decisionModel.js";
10
+ import { DecisionCard as u, DecisionSources as d } from "../../components/decisions/DecisionCard.js";
11
+ import { AiDisclaimer as f, DecisionsUnavailableBanner as p } from "../../components/decisions/DecisionBanners.js";
12
+ import "../../components/decisions/index.js";
13
+ import { ArrowLeft as m, Bot as h, ExternalLink as g } from "lucide-react";
14
+ import { useParams as _ } from "react-router";
15
+ import { jsx as v, jsxs as y } from "react/jsx-runtime";
16
+ import { GlassCard as b } from "@burdenoff/fe-libs/ui";
17
+ //#region src/subscriberbot/pages/decisions/DecisionDetailPage.tsx
18
+ function x(e) {
19
+ return e == null ? "—" : typeof e == "string" ? r(e) : typeof e == "number" || typeof e == "boolean" ? String(e) : JSON.stringify(e);
20
+ }
21
+ function S({ label: e, value: t, testId: n }) {
22
+ return /* @__PURE__ */ y("div", {
23
+ className: "flex items-start justify-between gap-4 py-1.5 text-sm",
24
+ children: [/* @__PURE__ */ v("span", {
25
+ className: "text-text-secondary",
26
+ children: e
27
+ }), /* @__PURE__ */ v("span", {
28
+ className: "break-all text-right text-text-primary",
29
+ "data-testid": n,
30
+ children: t
31
+ })]
32
+ });
33
+ }
34
+ function C() {
35
+ let C = a(), { id: w } = _(), { navigateTo: T } = e(), { data: E, isLoading: D, isError: O, error: k, refetch: A } = c(w), j = /* @__PURE__ */ y("button", {
36
+ type: "button",
37
+ onClick: () => T("/decisions"),
38
+ className: "inline-flex items-center gap-2 rounded-lg border border-border-subtle px-3 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken",
39
+ children: [
40
+ /* @__PURE__ */ v(m, {
41
+ className: "h-4 w-4",
42
+ "aria-hidden": !0
43
+ }),
44
+ " ",
45
+ C("decisions.detail.back", "All decisions")
46
+ ]
47
+ });
48
+ if (D) return /* @__PURE__ */ v(t, {
49
+ title: C("decisions.detail.loading", "Loading decision…"),
50
+ actions: j,
51
+ children: /* @__PURE__ */ v(o, { label: C("decisions.detail.loading", "Loading decision…") })
52
+ });
53
+ if (O) return /* @__PURE__ */ y(t, {
54
+ title: C("decisions.title", "AI Decisions"),
55
+ actions: j,
56
+ children: [/* @__PURE__ */ v(f, {}), /* @__PURE__ */ v(p, {
57
+ className: "mt-4",
58
+ message: k instanceof Error ? k.message : void 0,
59
+ onRetry: () => void A()
60
+ })]
61
+ });
62
+ if (!E) return /* @__PURE__ */ v(t, {
63
+ title: C("decisions.title", "AI Decisions"),
64
+ actions: j,
65
+ children: /* @__PURE__ */ v(i, {
66
+ illustration: "empty-data",
67
+ title: C("decisions.detail.notFoundTitle", "Decision not found"),
68
+ description: C("decisions.detail.notFoundDescription", "It may have been removed, or you may not have access to it.")
69
+ })
70
+ });
71
+ let M = l[E.kind], N = Object.entries(E.proposedChange ?? {});
72
+ return /* @__PURE__ */ y(t, {
73
+ title: C(M.key, M.fallback),
74
+ description: C("decisions.detail.description", "One recommendation, the records it used and the change applying it makes."),
75
+ actions: j,
76
+ children: [/* @__PURE__ */ v(f, {}), /* @__PURE__ */ y("div", {
77
+ className: "mt-6 grid grid-cols-1 gap-6 lg:grid-cols-3",
78
+ children: [/* @__PURE__ */ y("div", {
79
+ className: "lg:col-span-2",
80
+ children: [/* @__PURE__ */ v(u, { decision: E }), /* @__PURE__ */ v(s, {
81
+ title: C("decisions.detail.sources", "Source records"),
82
+ description: C("decisions.detail.sourcesDescription", "Every row the agent reasoned over. Open one to check it before you apply."),
83
+ children: /* @__PURE__ */ v(b, {
84
+ treatment: "metric",
85
+ className: "p-4",
86
+ children: /* @__PURE__ */ v(d, { decision: E })
87
+ })
88
+ })]
89
+ }), /* @__PURE__ */ y("div", {
90
+ className: "space-y-6 lg:col-span-1",
91
+ children: [/* @__PURE__ */ v(s, {
92
+ className: "mt-0",
93
+ title: C("decisions.detail.change", "What applying changes"),
94
+ children: /* @__PURE__ */ y(b, {
95
+ treatment: "metric",
96
+ className: "p-4",
97
+ "data-testid": "proposed-change",
98
+ children: [N.length === 0 ? /* @__PURE__ */ v("p", {
99
+ className: "text-sm text-text-secondary",
100
+ children: C("decisions.detail.noChange", "Nothing. Applying records that you agree, and the subscription renews as it is.")
101
+ }) : N.map(([e, t]) => /* @__PURE__ */ v(S, {
102
+ label: r(e),
103
+ value: x(t)
104
+ }, e)), /* @__PURE__ */ y("p", {
105
+ className: "mt-3 border-t border-border-subtle pt-3 text-xs text-text-secondary",
106
+ children: [
107
+ C("decisions.detail.manual", "You can make the same change by hand from the relationship page."),
108
+ " ",
109
+ /* @__PURE__ */ y("button", {
110
+ type: "button",
111
+ onClick: () => T(`/relationships/${E.relationshipId}`),
112
+ className: "inline-flex items-center gap-1 font-medium text-text-primary hover:underline",
113
+ children: [C("decisions.detail.openRelationship", "Open relationship"), /* @__PURE__ */ v(g, {
114
+ className: "h-3 w-3",
115
+ "aria-hidden": !0
116
+ })]
117
+ })
118
+ ]
119
+ })]
120
+ })
121
+ }), /* @__PURE__ */ v(s, {
122
+ className: "mt-0",
123
+ title: C("decisions.detail.provenance", "Provenance"),
124
+ children: /* @__PURE__ */ y(b, {
125
+ treatment: "metric",
126
+ className: "p-4",
127
+ children: [
128
+ E.agent && /* @__PURE__ */ y("button", {
129
+ type: "button",
130
+ onClick: () => T(`/agents/${E.agentId}`),
131
+ className: "mb-2 inline-flex items-center gap-1.5 text-sm font-medium text-text-primary hover:underline",
132
+ children: [
133
+ /* @__PURE__ */ v(h, {
134
+ className: "h-4 w-4",
135
+ "aria-hidden": !0
136
+ }),
137
+ " ",
138
+ E.agent.name
139
+ ]
140
+ }),
141
+ /* @__PURE__ */ v(S, {
142
+ label: C("decisions.detail.status", "Status"),
143
+ value: r(E.status)
144
+ }),
145
+ /* @__PURE__ */ v(S, {
146
+ label: C("decisions.detail.created", "Proposed"),
147
+ value: n(E.createdAt)
148
+ }),
149
+ E.appliedAt && /* @__PURE__ */ v(S, {
150
+ label: C("decisions.detail.applied", "Applied"),
151
+ value: n(E.appliedAt)
152
+ }),
153
+ E.dismissedAt && /* @__PURE__ */ v(S, {
154
+ label: C("decisions.detail.dismissed", "Dismissed"),
155
+ value: n(E.dismissedAt)
156
+ }),
157
+ /* @__PURE__ */ v(S, {
158
+ label: C("decisions.detail.run", "Run"),
159
+ value: E.runId
160
+ }),
161
+ /* @__PURE__ */ v(S, {
162
+ label: C("decisions.detail.engine", "Engine"),
163
+ value: C("decisions.detail.engineValue", "Rule-based strategy (no model tokens)")
164
+ }),
165
+ /* @__PURE__ */ v(S, {
166
+ label: C("decisions.detail.ledger", "AI ledger entry"),
167
+ value: E.invocationRef ?? C("decisions.detail.ledgerMissing", "Not recorded"),
168
+ testId: "ledger-ref"
169
+ })
170
+ ]
171
+ })
172
+ })]
173
+ })]
174
+ })]
175
+ });
176
+ }
177
+ //#endregion
178
+ export { C as DecisionDetailPage };
179
+
180
+ //# sourceMappingURL=DecisionDetailPage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DecisionDetailPage.js","names":[],"sources":["../../../../src/subscriberbot/pages/decisions/DecisionDetailPage.tsx"],"sourcesContent":["/**\n * Decision detail\n * @module subscriberbot/pages/decisions\n *\n * `/subscriberbot/decisions/:id` — one recommendation in full: what it\n * proposes, why, every source row it reasoned over (each a link to the page\n * that shows it), the exact change applying it makes, and its provenance\n * (agent, run, ledger entry). The assistant resolves this route as\n * `SUBSCRIBERBOT_DECISION` so \"apply this one\" has an entity to act on.\n */\n\nimport { ArrowLeft, Bot, ExternalLink } from 'lucide-react';\nimport { useParams } from 'react-router';\nimport { GlassCard } from '@burdenoff/fe-libs/ui';\nimport { PageLayout, EmptyState, LoadingView, Section } from '../../components';\nimport {\n AiDisclaimer,\n DecisionCard,\n DecisionSources,\n DecisionsUnavailableBanner,\n DECISION_KIND_LABEL,\n} from '../../components/decisions';\nimport { useSubscriberBot } from '../../context/SubscriberBotContext';\nimport { useDecision } from '../../hooks/useSubscriberBotApi';\nimport { useTr } from '../../hooks/useTr';\nimport { formatDateTime, formatEnumLabel } from '../../utils/formatters';\n\nfunction formatChangeValue(value: unknown): string {\n if (value === null || value === undefined) return '—';\n if (typeof value === 'string') return formatEnumLabel(value);\n if (typeof value === 'number' || typeof value === 'boolean') return String(value);\n return JSON.stringify(value);\n}\n\nfunction Row({ label, value, testId }: { label: string; value: string; testId?: string }) {\n return (\n <div className=\"flex items-start justify-between gap-4 py-1.5 text-sm\">\n <span className=\"text-text-secondary\">{label}</span>\n <span className=\"break-all text-right text-text-primary\" data-testid={testId}>\n {value}\n </span>\n </div>\n );\n}\n\nexport function DecisionDetailPage() {\n const tr = useTr();\n const { id } = useParams<{ id: string }>();\n const { navigateTo } = useSubscriberBot();\n const { data: decision, isLoading, isError, error, refetch } = useDecision(id);\n\n const back = (\n <button\n type=\"button\"\n onClick={() => navigateTo('/decisions')}\n className=\"inline-flex items-center gap-2 rounded-lg border border-border-subtle px-3 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken\"\n >\n <ArrowLeft className=\"h-4 w-4\" aria-hidden /> {tr('decisions.detail.back', 'All decisions')}\n </button>\n );\n\n if (isLoading) {\n return (\n <PageLayout title={tr('decisions.detail.loading', 'Loading decision…')} actions={back}>\n <LoadingView label={tr('decisions.detail.loading', 'Loading decision…')} />\n </PageLayout>\n );\n }\n\n if (isError) {\n return (\n <PageLayout title={tr('decisions.title', 'AI Decisions')} actions={back}>\n <AiDisclaimer />\n <DecisionsUnavailableBanner\n className=\"mt-4\"\n message={error instanceof Error ? error.message : undefined}\n onRetry={() => void refetch()}\n />\n </PageLayout>\n );\n }\n\n if (!decision) {\n return (\n <PageLayout title={tr('decisions.title', 'AI Decisions')} actions={back}>\n <EmptyState\n illustration=\"empty-data\"\n title={tr('decisions.detail.notFoundTitle', 'Decision not found')}\n description={tr(\n 'decisions.detail.notFoundDescription',\n 'It may have been removed, or you may not have access to it.'\n )}\n />\n </PageLayout>\n );\n }\n\n const kind = DECISION_KIND_LABEL[decision.kind];\n const change = Object.entries(decision.proposedChange ?? {});\n\n return (\n <PageLayout\n title={tr(kind.key, kind.fallback)}\n description={tr(\n 'decisions.detail.description',\n 'One recommendation, the records it used and the change applying it makes.'\n )}\n actions={back}\n >\n <AiDisclaimer />\n\n <div className=\"mt-6 grid grid-cols-1 gap-6 lg:grid-cols-3\">\n <div className=\"lg:col-span-2\">\n <DecisionCard decision={decision} />\n\n <Section\n title={tr('decisions.detail.sources', 'Source records')}\n description={tr(\n 'decisions.detail.sourcesDescription',\n 'Every row the agent reasoned over. Open one to check it before you apply.'\n )}\n >\n <GlassCard treatment=\"metric\" className=\"p-4\">\n <DecisionSources decision={decision} />\n </GlassCard>\n </Section>\n </div>\n\n <div className=\"space-y-6 lg:col-span-1\">\n <Section className=\"mt-0\" title={tr('decisions.detail.change', 'What applying changes')}>\n <GlassCard treatment=\"metric\" className=\"p-4\" data-testid=\"proposed-change\">\n {change.length === 0 ? (\n <p className=\"text-sm text-text-secondary\">\n {tr(\n 'decisions.detail.noChange',\n 'Nothing. Applying records that you agree, and the subscription renews as it is.'\n )}\n </p>\n ) : (\n change.map(([key, value]) => (\n <Row key={key} label={formatEnumLabel(key)} value={formatChangeValue(value)} />\n ))\n )}\n <p className=\"mt-3 border-t border-border-subtle pt-3 text-xs text-text-secondary\">\n {tr(\n 'decisions.detail.manual',\n 'You can make the same change by hand from the relationship page.'\n )}{' '}\n <button\n type=\"button\"\n onClick={() => navigateTo(`/relationships/${decision.relationshipId}`)}\n className=\"inline-flex items-center gap-1 font-medium text-text-primary hover:underline\"\n >\n {tr('decisions.detail.openRelationship', 'Open relationship')}\n <ExternalLink className=\"h-3 w-3\" aria-hidden />\n </button>\n </p>\n </GlassCard>\n </Section>\n\n <Section className=\"mt-0\" title={tr('decisions.detail.provenance', 'Provenance')}>\n <GlassCard treatment=\"metric\" className=\"p-4\">\n {decision.agent && (\n <button\n type=\"button\"\n onClick={() => navigateTo(`/agents/${decision.agentId}`)}\n className=\"mb-2 inline-flex items-center gap-1.5 text-sm font-medium text-text-primary hover:underline\"\n >\n <Bot className=\"h-4 w-4\" aria-hidden /> {decision.agent.name}\n </button>\n )}\n <Row\n label={tr('decisions.detail.status', 'Status')}\n value={formatEnumLabel(decision.status)}\n />\n <Row\n label={tr('decisions.detail.created', 'Proposed')}\n value={formatDateTime(decision.createdAt)}\n />\n {decision.appliedAt && (\n <Row\n label={tr('decisions.detail.applied', 'Applied')}\n value={formatDateTime(decision.appliedAt)}\n />\n )}\n {decision.dismissedAt && (\n <Row\n label={tr('decisions.detail.dismissed', 'Dismissed')}\n value={formatDateTime(decision.dismissedAt)}\n />\n )}\n <Row label={tr('decisions.detail.run', 'Run')} value={decision.runId} />\n {/* The BOFF-3634 engine runs deterministic strategies only, so a\n decision has no model cost. When model-assisted strategies\n land (BOFF-10263) this row must read cost/tokens from the\n shared ledger instead. */}\n <Row\n label={tr('decisions.detail.engine', 'Engine')}\n value={tr('decisions.detail.engineValue', 'Rule-based strategy (no model tokens)')}\n />\n <Row\n label={tr('decisions.detail.ledger', 'AI ledger entry')}\n value={\n decision.invocationRef ?? tr('decisions.detail.ledgerMissing', 'Not recorded')\n }\n testId=\"ledger-ref\"\n />\n </GlassCard>\n </Section>\n </div>\n </div>\n </PageLayout>\n );\n}\n"],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Portfolio decision queue
3
+ * @module subscriberbot/pages/decisions
4
+ *
5
+ * `/subscriberbot/decisions` — every recommendation the workspace's agents
6
+ * have made, newest first, from the real `subListDecisions` query. Filter by
7
+ * status and kind; each card shows the recommendation, rationale, confidence,
8
+ * estimated saving and the source rows it used, and can be applied or
9
+ * dismissed. When the decision engine is unreachable the page degrades to a
10
+ * banner that points at the manual path instead of a blank screen.
11
+ */
12
+ export declare function DecisionsPage(): import("react").JSX.Element;
13
+ //# sourceMappingURL=DecisionsPage.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"DecisionsPage.d.ts","sourceRoot":"","sources":["../../../../src/subscriberbot/pages/decisions/DecisionsPage.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AA+BH,wBAAgB,aAAa,gCA+J5B"}
@@ -0,0 +1,154 @@
1
+ import { useSubscriberBot as e } from "../../context/SubscriberBotContext.js";
2
+ import { cn as t } from "../../utils/cn.js";
3
+ import { PageLayout as n } from "../../components/PageLayout.js";
4
+ import { MetricsCard as r } from "../../components/MetricsCard.js";
5
+ import { formatCurrency as i } from "../../utils/formatters.js";
6
+ import { EmptyState as a } from "../../components/EmptyState.js";
7
+ import { useTr as o } from "../../hooks/useTr.js";
8
+ import { LoadingView as s } from "../../components/StateViews.js";
9
+ import "../../components/index.js";
10
+ import { useDecisions as c } from "../../hooks/useSubscriberBotApi.js";
11
+ import { DECISION_KINDS as l, DECISION_KIND_LABEL as u, isDestructive as d } from "../../components/decisions/decisionModel.js";
12
+ import { DecisionCard as f } from "../../components/decisions/DecisionCard.js";
13
+ import { AiDisclaimer as p, DecisionsUnavailableBanner as m } from "../../components/decisions/DecisionBanners.js";
14
+ import "../../components/decisions/index.js";
15
+ import { useMemo as h, useState as g } from "react";
16
+ import { Bot as _, ClipboardList as v, PiggyBank as y, ShieldAlert as b } from "lucide-react";
17
+ import { jsx as x, jsxs as S } from "react/jsx-runtime";
18
+ //#region src/subscriberbot/pages/decisions/DecisionsPage.tsx
19
+ var C = [
20
+ {
21
+ id: "PROPOSED",
22
+ key: "decisions.tabs.proposed",
23
+ fallback: "To review"
24
+ },
25
+ {
26
+ id: "APPLIED",
27
+ key: "decisions.tabs.applied",
28
+ fallback: "Applied"
29
+ },
30
+ {
31
+ id: "DISMISSED",
32
+ key: "decisions.tabs.dismissed",
33
+ fallback: "Dismissed"
34
+ },
35
+ {
36
+ id: "STALE",
37
+ key: "decisions.tabs.stale",
38
+ fallback: "Stale"
39
+ },
40
+ {
41
+ id: "FLAGGED",
42
+ key: "decisions.tabs.flagged",
43
+ fallback: "Flagged"
44
+ },
45
+ {
46
+ id: "ALL",
47
+ key: "decisions.tabs.all",
48
+ fallback: "All"
49
+ }
50
+ ];
51
+ function w() {
52
+ let w = o(), { navigateTo: T } = e(), [E, D] = g("PROPOSED"), [O, k] = g(""), { data: A, isLoading: j, isError: M, error: N, refetch: P } = c(h(() => ({
53
+ ...E === "ALL" ? {} : { status: E },
54
+ ...O ? { kind: O } : {}
55
+ }), [E, O])), F = A?.items ?? [], I = c({ status: "PROPOSED" }), L = I.data?.items ?? [], R = L.reduce((e, t) => (e[t.currency] = (e[t.currency] ?? 0) + t.estimatedSavings, e), {}), z = Object.entries(R).filter(([, e]) => e > 0).map(([e, t]) => i(t, e)).join(" + ") || i(0, L[0]?.currency ?? "USD"), B = L.filter((e) => d(e.recommendation)).length;
56
+ return /* @__PURE__ */ S(n, {
57
+ title: w("decisions.title", "AI Decisions"),
58
+ description: w("decisions.description", "What your agents recommend doing about your subscriptions, with the reasoning and the records behind it."),
59
+ actions: /* @__PURE__ */ S("button", {
60
+ type: "button",
61
+ onClick: () => T("/agents"),
62
+ className: "inline-flex items-center gap-2 rounded-lg border border-border-subtle px-3 py-2 text-sm font-medium text-text-primary hover:bg-bg-sunken",
63
+ children: [
64
+ /* @__PURE__ */ x(_, {
65
+ className: "h-4 w-4",
66
+ "aria-hidden": !0
67
+ }),
68
+ " ",
69
+ w("decisions.manageAgents", "Manage agents")
70
+ ]
71
+ }),
72
+ children: [
73
+ /* @__PURE__ */ x(p, {}),
74
+ !I.isError && /* @__PURE__ */ S("div", {
75
+ className: "mt-6 grid grid-cols-1 gap-4 sm:grid-cols-3",
76
+ children: [
77
+ /* @__PURE__ */ x(r, {
78
+ icon: /* @__PURE__ */ x(v, { className: "h-5 w-5" }),
79
+ value: I.isLoading ? "—" : L.length,
80
+ label: w("decisions.metrics.toReview", "Waiting for review")
81
+ }),
82
+ /* @__PURE__ */ x(r, {
83
+ icon: /* @__PURE__ */ x(y, { className: "h-5 w-5" }),
84
+ value: I.isLoading ? "—" : z,
85
+ label: w("decisions.metrics.savings", "Estimated yearly savings if applied")
86
+ }),
87
+ /* @__PURE__ */ x(r, {
88
+ icon: /* @__PURE__ */ x(b, { className: "h-5 w-5" }),
89
+ value: I.isLoading ? "—" : B,
90
+ label: w("decisions.metrics.cancellations", "Cancellations proposed")
91
+ })
92
+ ]
93
+ }),
94
+ /* @__PURE__ */ S("div", {
95
+ className: "mt-6 flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between",
96
+ children: [/* @__PURE__ */ x("div", {
97
+ role: "tablist",
98
+ "aria-label": w("decisions.tabs.label", "Decision status"),
99
+ className: "flex flex-wrap gap-1.5",
100
+ children: C.map((e) => /* @__PURE__ */ x("button", {
101
+ type: "button",
102
+ role: "tab",
103
+ "aria-selected": E === e.id,
104
+ onClick: () => D(e.id),
105
+ className: t("rounded-lg px-3 py-1.5 text-xs font-medium", E === e.id ? "bg-action-primary-bg text-action-primary-text" : "border border-border-subtle text-text-primary hover:bg-bg-sunken"),
106
+ children: w(e.key, e.fallback)
107
+ }, e.id))
108
+ }), /* @__PURE__ */ S("label", {
109
+ className: "flex items-center gap-2 text-xs text-text-secondary",
110
+ children: [w("decisions.filters.kind", "Kind"), /* @__PURE__ */ S("select", {
111
+ value: O,
112
+ onChange: (e) => k(e.target.value),
113
+ className: "rounded-lg border border-border-subtle bg-bg-surface px-2 py-1.5 text-xs text-text-primary focus:outline-none focus:ring-2 focus:ring-[var(--color-focus-ring)]",
114
+ children: [/* @__PURE__ */ x("option", {
115
+ value: "",
116
+ children: w("decisions.filters.allKinds", "All kinds")
117
+ }), l.map((e) => /* @__PURE__ */ x("option", {
118
+ value: e,
119
+ children: w(u[e].key, u[e].fallback)
120
+ }, e))]
121
+ })]
122
+ })]
123
+ }),
124
+ /* @__PURE__ */ S("div", {
125
+ className: "mt-4",
126
+ children: [j ? /* @__PURE__ */ x(s, { label: w("decisions.loading", "Loading decisions…") }) : M ? /* @__PURE__ */ x(m, {
127
+ message: N instanceof Error ? N.message : void 0,
128
+ onRetry: () => void P()
129
+ }) : F.length === 0 ? /* @__PURE__ */ x(a, {
130
+ illustration: "empty-data",
131
+ title: E === "PROPOSED" ? w("decisions.empty.proposedTitle", "Nothing waiting for review") : w("decisions.empty.title", "No decisions here"),
132
+ description: w("decisions.empty.description", "Run a cost, renewal, vendor or discovery agent to get recommendations. Running an agent never changes a subscription on its own."),
133
+ action: /* @__PURE__ */ x("button", {
134
+ type: "button",
135
+ onClick: () => T("/agents"),
136
+ className: "rounded-lg bg-action-primary-bg px-4 py-2 text-sm font-medium text-action-primary-text hover:opacity-90",
137
+ children: w("decisions.empty.action", "Go to agents")
138
+ })
139
+ }) : /* @__PURE__ */ x("div", {
140
+ className: "grid grid-cols-1 gap-3 lg:grid-cols-2",
141
+ "data-testid": "decision-queue",
142
+ children: F.map((e) => /* @__PURE__ */ x(f, { decision: e }, e.id))
143
+ }), A?.pageInfo.hasMore && /* @__PURE__ */ x("p", {
144
+ className: "mt-3 text-xs text-text-secondary",
145
+ children: w("decisions.truncated", "Showing the newest {{count}}. Filter by kind or status to narrow the list.", { count: F.length })
146
+ })]
147
+ })
148
+ ]
149
+ });
150
+ }
151
+ //#endregion
152
+ export { w as DecisionsPage };
153
+
154
+ //# sourceMappingURL=DecisionsPage.js.map