import { useEffect, useId, useRef, useState } from "react"; import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import type { DotPairing } from "@paperclipai/shared"; import { grokBotApi } from "@/api/grokBot"; import { buildGrokAccessPrompt, buildGrokRoutinePrompt } from "@/lib/grok-bot-setup-prompt"; import { AgentSetupPrompt } from "./AgentSetupPrompt"; import { CopyText } from "./CopyText"; import { Button, buttonVariants } from "./ui/button"; import { Input } from "./ui/input"; import { queryKeys } from "@/lib/queryKeys"; /** Configuration writes update the authoritative binding, never an unsaved agent form. */ export function GrokBotConnection({ companyId, agentId }: { companyId?: string; agentId?: string }) { const pairingCodeId = useId(); const cache = useQueryClient(); const key = ["grok-binding", companyId, agentId]; const state = useQuery({ queryKey: key, enabled: !!companyId && !!agentId, queryFn: ({ signal }) => grokBotApi.connection(companyId!, agentId!, signal), retry: false, refetchInterval: 5000 }); const [pairing, setPairing] = useState(null); const [replace, setReplace] = useState(false); const [botName, setBotName] = useState(""); const [webhookUrl, setWebhookUrl] = useState(""); const [webhookKey, setWebhookKey] = useState(""); const [inspected, setInspected] = useState(false); const verifyRequest = useRef(null); const binding = state.data?.binding, routine = binding?.routine, exchange = routine?.lastExchange; const refresh = async () => { await cache.invalidateQueries({ queryKey: key }); if (companyId && agentId) await cache.invalidateQueries({ queryKey: queryKeys.agents.detail(agentId) }); }; const pair = useMutation({ mutationFn: async () => { setPairing(await grokBotApi.pair(companyId!, agentId!, binding?.status === "pairing" ? binding.id : undefined)); }, onSuccess: refresh, gcTime: 0 }); const configure = useMutation({ mutationFn: async () => { await grokBotApi.configure(companyId!, agentId!, { bindingId: binding!.id, expectedRevision: routine?.revision ?? 0, botName, webhookUrl, webhookKey }); setWebhookUrl(""); setWebhookKey(""); setReplace(false); verifyRequest.current = null; }, onSuccess: refresh, gcTime: 0 }); const verify = useMutation({ mutationFn: async () => { if (["rejected", "cancelled"].includes(exchange?.status ?? "")) verifyRequest.current = null; verifyRequest.current ??= crypto.randomUUID(); await grokBotApi.verify(companyId!, agentId!, binding!.id, verifyRequest.current); }, onSuccess: refresh, gcTime: 0 }); const retryDelivery = useMutation({ mutationFn: (correlationId: string) => grokBotApi.retry(companyId!, agentId!, correlationId), onSuccess: refresh }); const disconnect = useMutation({ mutationFn: () => grokBotApi.disconnect(companyId!, agentId!), onSuccess: async () => { setPairing(null); await refresh(); } }); const recover = useMutation({ mutationFn: () => grokBotApi.recover(companyId!, agentId!), onSuccess: async () => { setInspected(false); verifyRequest.current = null; await refresh(); } }); const stop = useMutation({ mutationFn: (runId: string) => grokBotApi.stop(companyId!, agentId!, runId), onSuccess: refresh }); useEffect(() => { if (routine?.botName) setBotName(routine.botName); }, [routine?.botName]); useEffect(() => { if (!pairing) return; const timer = window.setTimeout(() => setPairing(null), Math.max(0, Date.parse(pairing.expiresAt) - Date.now())); return () => window.clearTimeout(timer); }, [pairing]); if (!agentId) return

Save the Grok Bot agent before connecting it.

; const busy = pair.isPending || configure.isPending || verify.isPending || disconnect.isPending || recover.isPending || stop.isPending || retryDelivery.isPending; const error = state.error ?? pair.error ?? configure.error ?? verify.error ?? disconnect.error ?? recover.error ?? stop.error ?? retryDelivery.error; const recovering = state.data?.recoveryNeeded || routine?.recoveryNeeded; const pending = exchange && ["prepared", "sending", "accepted"].includes(exchange.status) && Date.parse(exchange.expiresAt) > Date.now(); return

Runs in Grok Bot. The Bot retains its apps and persistent context. Paperclip supplies task coordination and first-party tools. Usage and spending are not reported.

{state.isPending &&

Loading connection…

} {error &&

{error.message}

} {state.data && !state.data.enabled &&

Enable Grok Bot, Assistant connections (MCP), and Paperclip Runner on an authenticated public HTTPS instance.

} {binding?.connected ? <>

Agent access connected. {routine?.verified && !recovering ? "Routine round trip verified." : "Assignments require a verified routine round trip."}

Bot
{routine?.botName ?? "Not configured"}
Last verified exchange
{routine?.verifiedAt ? new Date(routine.verifiedAt).toLocaleString() : "Not yet verified"}
Last authenticated contact
{routine?.lastContactAt ? new Date(routine.lastContactAt).toLocaleString() : "Not yet observed"}
{exchange &&

Assignment delivered: {["accepted", "acknowledged"].includes(exchange.status) ? "Yes" : "Unconfirmed"}. Reply received: {exchange.acknowledgedAt ? "Yes" : "No"}.

} {exchange?.status === "rejected" &&

The routine rejected this delivery. Check the routine in Grok before retrying.

} {!routine?.configured || replace ?
:
} : <> {pairing && state.data?.resourceUrl && <>
e.target.select()} /> Copy code

Copy this code into Paperclip’s authorization page. Keep it out of Bot chat.

} } {binding?.assignment &&

{binding.assignment.status === "offered" ? "Awaiting acknowledgement" : "Working in a Paperclip work session"}

Revokes this session’s Paperclip authority and stops future dispatch. Remote execution may continue. Open Grok Bot to inspect it.

} {recovering &&

Recovery needed

Paperclip cannot determine whether Grok is still working. Open Grok Bot, inspect this Bot and stop the old work before clearing the hold. The session must be stopped in Paperclip too.

This records your inspection. It does not confirm remote termination. Verify the routine again before new work.

} {binding?.connected && }
; }