From 19b449f0103427b4116a983e3e1bceba3346b91f Mon Sep 17 00:00:00 2001 From: npub1qyvc0c5kl4gqv2fd97fsk46tu378sqgy35vc83rvgfwne90sel7s0ed67d <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz> Date: Mon, 27 Jul 2026 22:41:51 -0400 Subject: [PATCH] feat(desktop): send minted agent cards to recipients via normal DM share path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the v1 scope gap Wren flagged: the mint dialog only offered Save. Card preview now includes PersonaShareRecipients + a Send action that routes through useSnapshotSendController — the exact guarded encode → upload (uploadMediaBytes) → send pipeline agent snapshots use, with the same eligibility checkpoints and DM-safety gating. No card-special relay path, per the kickoff contract. Co-authored-by: Tyler Longwell Signed-off-by: Tyler Longwell --- .../agents/ui/AgentCardMintDialog.tsx | 78 ++++++++++++++++++- 1 file changed, 74 insertions(+), 4 deletions(-) diff --git a/desktop/src/features/agents/ui/AgentCardMintDialog.tsx b/desktop/src/features/agents/ui/AgentCardMintDialog.tsx index 6e406e18f..c35f3048d 100644 --- a/desktop/src/features/agents/ui/AgentCardMintDialog.tsx +++ b/desktop/src/features/agents/ui/AgentCardMintDialog.tsx @@ -1,14 +1,19 @@ import * as React from "react"; -import { Download, RefreshCw, Sparkles } from "lucide-react"; +import { Download, RefreshCw, Send, Sparkles } from "lucide-react"; import { useMutation } from "@tanstack/react-query"; import { toast } from "sonner"; +import { + useOpenDmMutation, + useUpsertCachedChannel, +} from "@/features/channels/hooks"; import { mintAgentCard, NO_OPENAI_KEY_PREFIX, saveAgentCard, type MintedAgentCard, } from "@/shared/api/tauriPersonas"; +import type { UserSearchResult } from "@/shared/api/types"; import { Button } from "@/shared/ui/button"; import { Dialog, @@ -19,6 +24,14 @@ import { } from "@/shared/ui/dialog"; import { Textarea } from "@/shared/ui/textarea"; +import { PersonaShareRecipients } from "./PersonaShareRecipients"; +import { useSnapshotSendController } from "./useSnapshotSendController"; + +/** Decode base64 card bytes into the number[] shape the upload path expects. */ +function cardBytesFromBase64(b64: string): number[] { + return Array.from(atob(b64), (char) => char.charCodeAt(0)); +} + /** * Mint-a-trading-card dialog: optional style notes → one long Rust-side * Responses API call → preview with reroll → save as `.agent.png`. @@ -39,6 +52,11 @@ export function AgentCardMintDialog({ }) { const [styleNotes, setStyleNotes] = React.useState(""); const [card, setCard] = React.useState(null); + const [recipients, setRecipients] = React.useState([]); + + const sendController = useSnapshotSendController(card !== null); + const openDmMutation = useOpenDmMutation(); + const upsertCachedChannel = useUpsertCachedChannel(); const mintMutation = useMutation({ mutationFn: () => mintAgentCard(agentId, styleNotes.trim() || undefined), @@ -69,9 +87,38 @@ export function AgentCardMintDialog({ }); const isMinting = mintMutation.isPending; + const isSending = + sendController.state.phase !== "idle" && + sendController.state.phase !== "done" && + sendController.state.phase !== "error"; + const isBusy = isMinting || saveMutation.isPending || isSending; + + async function sendToRecipients(minted: MintedAgentCard) { + if (recipients.length === 0) return; + const sent = await sendController.beginSend( + async () => ({ + fileBytes: cardBytesFromBase64(minted.cardPngBase64), + fileName: minted.fileName, + }), + async () => { + const directMessage = await openDmMutation.mutateAsync({ + pubkeys: recipients.map((recipient) => recipient.pubkey), + }); + await upsertCachedChannel(directMessage); + return directMessage.id; + }, + agentName, + ); + if (sent) { + toast.success(`Sent ${agentName}'s card.`); + onOpenChange(false); + } else if (sent === false) { + toast.error("Couldn’t send the card. Try again."); + } + } return ( - !isMinting && onOpenChange(open)} open> + !isBusy && onOpenChange(open)} open> @@ -93,9 +140,21 @@ export function AgentCardMintDialog({ data-testid="agent-card-preview" src={`data:image/png;base64,${card.cardPngBase64}`} /> +
+ {recipients.length > 0 ? ( + + ) : null}
) : (