fix(desktop): restore the agent trading-card mint button (#5900)

## Problem

PR #5574's profile-panel redesign dropped `ProfileSummaryView`'s
`onCreateCard` prop — the only caller of `setCardMintTarget` — so the
entire Agent Trading Cards feature (#3278) became unreachable from the
GUI while staying fully wired underneath: mint dialog, background job
store, viewer, gallery, composer chip, and the Rust
`mint_agent_card`/`save_agent_card` commands all survive at main. `git
log -S 'setCardMintTarget('` shows exactly two commits: the feature and
the accidental removal.

## Outcome

The mint trigger returns as a management row in the agent profile's Info
tab, directly under **Export agent**, gated `isBot && canManagePersona`
exactly like Duplicate/Export. Target resolution is byte-for-byte the
original logic: prefer the live instance pubkey, fall back to the
persona/definition id, allow locking only when an instance keypair
exists.

## Shape

- `UserProfileAgentManagementRows`: new optional `onCreateCard` row
(Sparkles icon, `user-profile-create-card-row`), placed after Export.
- Prop threaded `UserProfilePanel` → `ProfileSummaryView` →
`ProfileInfoTabContent` → management rows, mirroring `onExportAgent` at
every layer.
- The mint-target state + open callback move into a `useCardMint` hook
in `UserProfilePersonaDialogs` (beside the `CardMintTarget` type it
manages). This keeps `UserProfilePanel.tsx` at 999 lines — the file sits
at the size-ratchet cap and may not grow.

## Validation

- `pnpm check` green (biome, file-size ratchet, px-text,
pubkey-truncation).
- `pnpm typecheck` green.
- Full desktop unit suite: **4888 passed, 0 failed**.
- Profile e2e spec: **32 passed**, including the updated
management-row-order assertion and a new click → mint-dialog-visible →
Escape → closed exercise of the restored row.

Verified at `bff3110a0aeb3d63683eac9ed3e587829f9436da`, one commit atop
main `01f76ec97`.

Signed-off-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
Co-authored-by: Eva <011987e296fd5006292d2f930b574be47c7801048d1983c46c425d3c95f0cffd@buzz.block.builderlab.xyz>
This commit is contained in:
Tyler
2026-08-14 17:22:42 -04:00
committed by GitHub
co-authored by Eva
parent 122a8b8988
commit 263c9bf76c
6 changed files with 61 additions and 5 deletions
@@ -4,6 +4,7 @@ import {
ArchiveRestore,
CopyPlus,
Download,
Sparkles,
Trash2,
type LucideIcon,
} from "lucide-react";
@@ -30,6 +31,7 @@ export function UserProfileAgentManagementRows({
canDeleteAgent,
isDeletePending,
managedAgent,
onCreateCard,
onDeleteAgent,
onDuplicateAgent,
onExportAgent,
@@ -39,11 +41,14 @@ export function UserProfileAgentManagementRows({
canDeleteAgent: boolean;
isDeletePending: boolean;
managedAgent?: ManagedAgent;
/** Mint an agent trading card. Present only for owner-managed personas. */
onCreateCard?: () => void;
onDeleteAgent: () => void;
onDuplicateAgent?: () => void;
onExportAgent?: () => void;
}) {
if (
!onCreateCard &&
!onDuplicateAgent &&
!onExportAgent &&
!canArchiveAgent &&
@@ -72,6 +77,15 @@ export function UserProfileAgentManagementRows({
testId="user-profile-export-agent-row"
/>
) : null}
{onCreateCard ? (
<ProfileAgentActionRow
disabled={isDeletePending}
icon={Sparkles}
label="Create trading card"
onClick={onCreateCard}
testId="user-profile-create-card-row"
/>
) : null}
{canArchiveAgent ? (
<ProfileArchiveAgentRow archiveActions={archiveActions} />
) : null}
@@ -66,7 +66,7 @@ import { useProfileAgentDeletion } from "@/features/profile/ui/UserProfilePanelD
import { useProfileFieldBuckets } from "@/features/profile/ui/UserProfilePanelFields";
import { submitProfilePersonaDialog } from "@/features/profile/ui/UserProfilePanelPersonaSubmit";
import {
type CardMintTarget,
useCardMint,
UserProfilePersonaDialogs,
} from "@/features/profile/ui/UserProfilePersonaDialogs";
import {
@@ -179,8 +179,6 @@ export function UserProfilePanel({
React.useState<AgentPersona | null>(null);
const [personaToExportSnapshot, setPersonaToExportSnapshot] =
React.useState<AgentPersona | null>(null);
const [cardMintTarget, setCardMintTarget] =
React.useState<CardMintTarget | null>(null);
const [requestedInstancePubkey, setRequestedInstancePubkey] = React.useState<
string | null
>(null);
@@ -712,6 +710,7 @@ export function UserProfilePanel({
resolvedPersona,
);
const canManagePersona = isOwner === true && resolvedPersona !== undefined;
const cardMint = useCardMint(resolvedPersona, managedAgent);
const canDeletePersona = canManagePersona && !resolvedPersona?.sourceTeam;
const canDeleteProfileAgent =
isBot &&
@@ -822,6 +821,7 @@ export function UserProfilePanel({
agentSettingsFields={agentSettingsFields}
diagnosticsFields={diagnosticsFields}
onAddToChannel={() => setAddToChannelOpen(true)}
onCreateCard={isBot && canManagePersona ? cardMint.create : undefined}
onDeleteAgent={handleDeleteProfileAgent}
onDuplicateAgent={
isBot && canManagePersona ? handleDuplicatePersona : undefined
@@ -935,7 +935,7 @@ export function UserProfilePanel({
const personaDialogs = (
<>
<UserProfilePersonaDialogs
cardMintTarget={cardMintTarget}
cardMintTarget={cardMint.target}
createError={
createPersonaMutation.error instanceof Error
? createPersonaMutation.error
@@ -961,7 +961,7 @@ export function UserProfilePanel({
? updatePersonaMutation.error
: null
}
onCloseCardMint={() => setCardMintTarget(null)}
onCloseCardMint={cardMint.close}
onCloseDelete={() => setPersonaToDelete(null)}
onCloseDialog={() => setPersonaDialogState(null)}
onCloseExportSnapshot={() => setPersonaToExportSnapshot(null)}
@@ -94,6 +94,8 @@ export type ProfileSummaryViewProps = {
agentSettingsFields: ProfileField[];
diagnosticsFields: ProfileField[];
onAddToChannel: () => void;
/** Mint an agent trading card. Present only for owner-managed personas. */
onCreateCard?: () => void;
onDeleteAgent: () => void;
onDuplicateAgent?: () => void;
onExportAgent?: () => void;
@@ -168,6 +170,7 @@ export function ProfileSummaryView({
agentSettingsFields,
diagnosticsFields,
onAddToChannel,
onCreateCard,
onDeleteAgent,
onDuplicateAgent,
onExportAgent,
@@ -512,6 +515,7 @@ export function ProfileSummaryView({
isDeleteAgentPending={isAgentActionPending}
managedAgent={managedAgent}
onEditAgent={handleEditAgent}
onCreateCard={onCreateCard}
onDeleteAgent={onDeleteAgent}
onDuplicateAgent={onDuplicateAgent}
onExportAgent={onExportAgent}
@@ -206,6 +206,7 @@ export function ProfileInfoTabContent({
isArchived,
isDeleteAgentPending,
managedAgent,
onCreateCard,
onDeleteAgent,
onDuplicateAgent,
onExportAgent,
@@ -225,6 +226,8 @@ export function ProfileInfoTabContent({
isArchived: boolean;
isDeleteAgentPending: boolean;
managedAgent?: ManagedAgent;
/** Mint an agent trading card. Present only for owner-managed personas. */
onCreateCard?: () => void;
onDeleteAgent: () => void;
onDuplicateAgent?: () => void;
onExportAgent?: () => void;
@@ -257,6 +260,7 @@ export function ProfileInfoTabContent({
!hasInfoFields &&
!showArchiveAction &&
!canDeleteAgent &&
!onCreateCard &&
!onDuplicateAgent &&
!onExportAgent &&
!showActivityIngress &&
@@ -306,6 +310,7 @@ export function ProfileInfoTabContent({
canDeleteAgent={canDeleteAgent}
isDeletePending={isDeleteAgentPending}
managedAgent={managedAgent}
onCreateCard={onCreateCard}
onDeleteAgent={onDeleteAgent}
onDuplicateAgent={onDuplicateAgent}
onExportAgent={onExportAgent}
@@ -1,7 +1,10 @@
import * as React from "react";
import type {
AcpRuntimeCatalogEntry,
AgentPersona,
CreatePersonaInput,
ManagedAgent,
UpdatePersonaInput,
} from "@/shared/api/types";
import { AgentCardMintDialog } from "@/features/agents/ui/AgentCardMintDialog";
@@ -17,6 +20,30 @@ export type CardMintTarget = {
canLock: boolean;
};
/**
* Card-mint dialog state plus the callback that opens it. `create` is
* undefined when no persona resolves; owner gating is the caller's job.
*/
export function useCardMint(
persona: AgentPersona | undefined,
managedAgent: ManagedAgent | undefined,
) {
const [target, setTarget] = React.useState<CardMintTarget | null>(null);
const close = React.useCallback(() => setTarget(null), []);
const create = persona
? () =>
setTarget({
// Prefer the live instance pubkey; fall back to the
// persona/definition id (same resolution as export).
id: managedAgent?.pubkey ?? persona.id,
name: persona.displayName,
// Locking needs an instance keypair to encrypt to.
canLock: Boolean(managedAgent?.pubkey),
})
: undefined;
return { close, create, target };
}
export function UserProfilePersonaDialogs({
cardMintTarget,
createError,
+6
View File
@@ -1466,6 +1466,7 @@ test("renders agent profile ingress subviews from the Playwright mock bridge", a
expect(managementRowOrder).toEqual([
"user-profile-duplicate-agent-row",
"user-profile-export-agent-row",
"user-profile-create-card-row",
"user-profile-archive-agent-row",
"user-profile-delete-agent-row",
]);
@@ -1486,6 +1487,11 @@ test("renders agent profile ingress subviews from the Playwright mock bridge", a
const exportDialog = page.getByTestId("agent-snapshot-export-dialog");
await expect(exportDialog).toBeVisible();
await exportDialog.getByRole("button", { name: "Cancel" }).click();
await page.getByTestId("user-profile-create-card-row").click();
const cardMintDialog = page.getByTestId("agent-card-mint-dialog");
await expect(cardMintDialog).toBeVisible();
await page.keyboard.press("Escape");
await expect(cardMintDialog).toHaveCount(0);
const archiveAgentRow = page.getByTestId("user-profile-archive-agent-row");
await expect(archiveAgentRow).toHaveText(/Archive agent/);
await archiveAgentRow.click();