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 })}
    />
  );
}