AgentInviteCard.tsx4.4 KBView on GitHub 'use client';
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query';
import { useMemo } from 'react';
import { useTRPC } from '@/providers/query-provider';
import { AgentInviteScreen, type AgentInvite, type AgentSetupStep } from './AgentInviteScreen';
/**
* The invite, wired.
*
* ── EVERY LINE ON THE SCREEN IS READ, NOT COMPOSED ──────────────────────────
*
* `agentSharing.invite` reads what the agent actually does off its own document — its
* description, its configured triggers, the namespace setup will write in, and any
* onward sharing it will later ASK about. None of it is generated for the occasion,
* because a consent screen that paraphrases is the one part of the flow nobody can
* check against the thing they are consenting to.
*
* ── A FAILED SETUP IS NOT AN ERROR, AND THAT IS DELIBERATE ──────────────────
*
* `accept` answers `ok` with `status: 'failed'` and a message rather than throwing.
* A thrown error would lose the message and leave the row looking untouched, when in
* fact the share row now says `failed` and a partial run may have happened. So the
* screen keeps the reason and offers Retry, which re-dispatches the same idempotent
* setup — it converges rather than writing a second copy of every trigger.
*/
export function AgentInviteCard({ shareId, className }: { shareId: string; className?: string }) {
const trpc = useTRPC();
const queryClient = useQueryClient();
const inviteOptions = trpc.agentSharing.invite.queryOptions({ shareId });
const { data } = useQuery(inviteOptions);
const refresh = () => {
void queryClient.invalidateQueries({ queryKey=[redacted] });
void queryClient.invalidateQueries({ queryKey=[redacted] });
};
const { mutate: accept, isPending: accepting, data: accepted } = useMutation({
...trpc.agentSharing.accept.mutationOptions(),
onSuccess: refresh,
});
const { mutate: decline, isPending: declining } = useMutation({
...trpc.agentSharing.decline.mutationOptions(),
onSuccess: refresh,
});
const { mutate: retry, isPending: retrying } = useMutation({
...trpc.agentSharing.retry.mutationOptions(),
onSuccess: refresh,
});
/**
* What the run actually did, one step per thing it wrote — from the report, never
* from a client-side script. A checklist that "completes" on a timer is a lie told
* in the same shape as the truth.
*/
const steps: AgentSetupStep[] | undefined = useMemo(() => {
const report = accepted?.setup;
if (!report) return undefined;
const out: AgentSetupStep[] = [];
if (report.triggersAdded.length > 0) {
out.push({ label: `Triggers added — ${report.triggersAdded.join(', ')}`, state: 'done' });
}
for (const skipped of report.triggersSkipped) {
// Named rather than silently dropped: a trigger the setup could not copy is a
// thing that will never fire, and finding that out later is worse than reading
// one line about it now.
out.push({ label: `${skipped.label} — not copied: ${skipped.reason}`, state: 'failed' });
}
if (report.files.length > 0) {
out.push({ label: `${report.files.length} starting file(s) created`, state: 'done' });
}
return out;
}, [accepted]);
if (!data) return null;
const invite: AgentInvite = {
shareId: data.shareId,
agentId: data.agentId,
agentName: data.agentName,
sharedByName: data.sharedByName,
summary: data.whatItDoes ?? 'This agent has no description of its own.',
status: accepted?.status ?? data.status,
steps,
failureMessage: accepted?.statusMessage ?? data.blockedReason,
facts: [
...data.runsWhen.map((when) => ({ kind: 'runs' as const, text: when })),
{
kind: 'writes' as const,
text: data.writesArePrivate
? `Writes to ${data.writesTo} — private to you`
: `Writes to ${data.writesTo}`,
},
...data.willAskToShareWith.map((who) => ({
kind: 'asks' as const,
text: `Will ask whether ${who} may read what it writes`,
})),
],
};
return (
<AgentInviteScreen
className={className}
invite={invite}
busy={accepting || declining || retrying}
onAccept={() => accept({ shareId })}
onDecline={() => decline({ shareId })}
onRetry={() => retry({ shareId })}
/>
);
}