import { useEffect, useMemo, useRef, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Inbox, LoaderCircle, Save } from "lucide-react"; import type { InboxAgentPolicy, InboxAgentPolicyMode } from "@paperclipai/shared"; import { agentsApi } from "@/api/agents"; import { describeError } from "@/api/errors"; import { inboxAgentPolicyApi } from "@/api/inbox-agent-policy"; import { queryKeys } from "@/lib/queryKeys"; import { QueryErrorState, useQueryView } from "@/components/QueryView"; import { isAgentTaskTarget } from "@/lib/company-members"; import { AgentMultiSelect } from "@/components/AgentMultiSelect"; import { Button } from "@/components/ui/button"; import { RadioCardGroup, type RadioCardOption } from "@/components/ui/radio-card"; const MODE_OPTIONS: RadioCardOption[] = [ { value: "open", title: "Any of my agents", description: "Let any agent you manage archive tasks out of your inbox.", }, { value: "allowlist", title: "Only chosen agents", description: "Restrict inbox tidying to the agents you pick below.", }, { value: "disabled", title: "Off", description: "Agents can never archive tasks from your inbox.", }, ]; function policyKey(mode: InboxAgentPolicyMode, allowedAgentIds: string[]): string { return `${mode}:${[...allowedAgentIds].sort().join(",")}`; } interface Draft { mode: InboxAgentPolicyMode; allowedAgentIds: string[]; } /** * "Let agents tidy my inbox" user-settings control. A single * three-state policy — `open` / `allowlist` / `disabled` — round-tripped through * the per-user endpoints. When `allowlist` is selected the user picks which * of their agents may archive. The one-click Undo/Unarchive affordance and the * "Archived by …" attribution live elsewhere (inbox rows / properties pane). */ export function InboxAgentPolicyControl({ companyId }: { companyId: string | null | undefined }) { const queryClient = useQueryClient(); const [draft, setDraft] = useState(null); const lastServerKeyRef = useRef(null); const policyQuery = useQuery({ queryKey: companyId ? queryKeys.inboxAgentPolicy.mine(companyId) : ["inbox-agent-policy", "none"], queryFn: () => inboxAgentPolicyApi.getMine(companyId!), enabled: !!companyId, }); const policyView = useQueryView(policyQuery); const policy = policyQuery.data; const agentsQuery = useQuery({ queryKey: companyId ? queryKeys.agents.list(companyId) : ["agents", "none"], queryFn: () => agentsApi.list(companyId!), enabled: !!companyId, }); const selectableAgents = useMemo( () => (agentsQuery.data ?? []).filter(isAgentTaskTarget), [agentsQuery.data], ); const agentOptions = useMemo( () => selectableAgents.map((agent) => ({ id: agent.id, name: agent.name, title: agent.title ?? agent.role, icon: agent.icon, })), [selectableAgents], ); const selectedAgentIds = useMemo( () => new Set(draft?.allowedAgentIds ?? []), [draft?.allowedAgentIds], ); // Adopt server state on first load, or on refetch when the user has not // diverged from the previously-synced snapshot (so a background refetch never // clobbers pending edits). useEffect(() => { if (!policy) return; const serverKey = policyKey(policy.mode, policy.allowedAgentIds); setDraft((current) => { if (current === null || policyKey(current.mode, current.allowedAgentIds) === lastServerKeyRef.current) { return { mode: policy.mode, allowedAgentIds: policy.allowedAgentIds }; } return current; }); lastServerKeyRef.current = serverKey; }, [policy]); const updateMutation = useMutation({ mutationFn: (next: Draft) => inboxAgentPolicyApi.updateMine(companyId!, { mode: next.mode, allowedAgentIds: next.mode === "allowlist" ? next.allowedAgentIds : [], }), onSuccess: (saved) => { queryClient.setQueryData(queryKeys.inboxAgentPolicy.mine(companyId!), saved); }, }); const isDirty = Boolean( draft && policy && policyKey(draft.mode, draft.allowedAgentIds) !== policyKey(policy.mode, policy.allowedAgentIds), ); // A saved policy stays on screen through a failed refetch; only a real // failure, or an outage before the first load, replaces the control. if (policyView.kind === "error") { return ( ); } if (policyQuery.isLoading || policyView.kind === "reconnecting" || !draft) { return
Loading inbox agent policy…
; } return (

Let agents tidy my inbox

Choose whether the agents you manage may archive tasks out of your inbox on your behalf. You can undo any archive, and every agent archive is attributed in the task's properties.

setDraft((current) => (current ? { ...current, mode: value as InboxAgentPolicyMode } : current))} options={MODE_OPTIONS} className="max-w-2xl" /> {draft.mode === "allowlist" ? (
Agents allowed to tidy my inbox
setDraft((current) => current ? { ...current, allowedAgentIds: [...next] } : current, ) } triggerLabel={ selectedAgentIds.size === 0 ? "Select agents" : `${selectedAgentIds.size} ${selectedAgentIds.size === 1 ? "agent" : "agents"} selected` } triggerFullWidth={false} showSelectionPreview={false} emptyMessage="You don’t manage any agents yet." />
) : null} {updateMutation.error ? (
{describeError(updateMutation.error, { action: "save the inbox agent policy" }).body}
) : null}
{updateMutation.isSuccess && !isDirty ? ( Saved ) : null}
); }