import { useMemo } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { Loader2 } from "lucide-react"; import type { Agent } from "@paperclipai/shared"; import { issuesApi } from "../api/issues"; import { queryKeys } from "../lib/queryKeys"; import { interactionReadinessRefetchInterval, isIssueThreadInteraction, type AskUserQuestionsAnswer, type AskUserQuestionsInteraction, type IssueThreadInteraction, type RequestCheckboxConfirmationInteraction, type RequestConfirmationInteraction, type RequestItemVerdictsInteraction, type RequestItemVerdictValue, type SuggestTasksInteraction, } from "../lib/issue-thread-interactions"; import { IssueThreadInteractionCard } from "./IssueThreadInteractionCard"; interface AttentionInteractionResolverProps { companyId: string; issueId: string; interactionId: string; agentMap?: Map; currentUserId?: string | null; userLabelMap?: ReadonlyMap | null; /** Called after a resolution so the parent can refresh the feed. */ onResolved?: () => void; } export function replaceResolvedInteraction( current: IssueThreadInteraction[] | undefined, resolvedInteraction: IssueThreadInteraction, ): IssueThreadInteraction[] { const existing = current ?? []; const index = existing.findIndex((entry) => entry.id === resolvedInteraction.id); if (index < 0) return [...existing, resolvedInteraction]; return existing.map((entry, entryIndex) => entryIndex === index ? resolvedInteraction : entry, ); } /** * Lazily fetches the full issue-thread interaction referenced by an attention * row and renders the existing {@link IssueThreadInteractionCard} inline, so * confirmations and questions are answerable in-row without leaving the queue * (converged PAP-12628). Reviews never reach here — they deep-link. */ export function AttentionInteractionResolver({ companyId, issueId, interactionId, agentMap, currentUserId, userLabelMap, onResolved, }: AttentionInteractionResolverProps) { const queryClient = useQueryClient(); const { data: interactions, isLoading, error } = useQuery({ queryKey: queryKeys.issues.interactions(issueId), queryFn: () => issuesApi.listInteractions(issueId), enabled: !!issueId, refetchInterval: (query) => interactionReadinessRefetchInterval(query.state.data), }); const interaction = useMemo(() => { const match = (interactions ?? []).find((entry) => entry.id === interactionId); return match && isIssueThreadInteraction(match) ? match : null; }, [interactions, interactionId]); const invalidate = (resolvedInteraction?: IssueThreadInteraction) => { // The accept route returns the fully stitched interaction (including // secretProposal.executed/failed). Put that receipt in the cache before // invalidating so the attention surface never flashes a generic Accepted // state while the refetch catches up. if (resolvedInteraction) { queryClient.setQueryData( queryKeys.issues.interactions(issueId), (current) => replaceResolvedInteraction(current, resolvedInteraction), ); } queryClient.invalidateQueries({ queryKey: queryKeys.issues.interactions(issueId) }); queryClient.invalidateQueries({ queryKey: queryKeys.attention(companyId) }); onResolved?.(); }; const acceptMutation = useMutation({ mutationFn: (input: { interaction: SuggestTasksInteraction | RequestConfirmationInteraction | RequestCheckboxConfirmationInteraction; selectedClientKeys?: string[]; selectedOptionIds?: string[]; rememberAction?: boolean; }) => issuesApi.acceptInteraction(issueId, input.interaction.id, { selectedClientKeys: input.selectedClientKeys, selectedOptionIds: input.selectedOptionIds, rememberAction: input.rememberAction, }), onSuccess: invalidate, }); const rejectMutation = useMutation({ mutationFn: (input: { interactionId: string; reason?: string }) => issuesApi.rejectInteraction(issueId, input.interactionId, input.reason), onSuccess: invalidate, }); const respondMutation = useMutation({ mutationFn: (input: { interactionId: string; answers: AskUserQuestionsAnswer[] }) => issuesApi.respondToInteraction(issueId, input.interactionId, { answers: input.answers }), onSuccess: invalidate, }); const cancelMutation = useMutation({ mutationFn: (input: { interactionId: string }) => issuesApi.cancelInteraction(issueId, input.interactionId), onSuccess: invalidate, }); const verdictsMutation = useMutation({ mutationFn: (input: { interactionId: string; verdicts: { id: string; verdict: RequestItemVerdictValue; reason?: string }[]; }) => issuesApi.submitInteractionVerdicts(issueId, input.interactionId, input.verdicts), onSuccess: invalidate, }); if (isLoading) { return (
Loading decision…
); } if (error || !interaction) { return (

This decision is no longer available — it may have been resolved elsewhere.

); } return ( acceptMutation.mutateAsync({ interaction: target, selectedClientKeys, selectedOptionIds, rememberAction }).then(() => undefined) } onRejectInteraction={(target, reason) => rejectMutation.mutateAsync({ interactionId: target.id, reason }).then(() => undefined) } onSubmitInteractionAnswers={(target: AskUserQuestionsInteraction, answers) => respondMutation.mutateAsync({ interactionId: target.id, answers }).then(() => undefined) } onCancelInteraction={(target: AskUserQuestionsInteraction) => cancelMutation.mutateAsync({ interactionId: target.id }).then(() => undefined) } onSubmitInteractionVerdicts={(target: RequestItemVerdictsInteraction, verdicts) => verdictsMutation.mutateAsync({ interactionId: target.id, verdicts }).then(() => undefined) } /> ); }