Adapt delete confirmation to profile redesign

Co-authored-by: Atish Patel <atish@squareup.com>
Signed-off-by: Atish Patel <atish@squareup.com>
Signed-off-by: Brainy Bumble <0ed7657b57c0e8a9f5288390dd6c8d5d0a3a06abe9b01b9006814f52077d6cdf@buzz.block.builderlab.xyz>
This commit is contained in:
Brainy Bumble
2026-08-12 11:12:02 -05:00
parent 25e18db4af
commit f3496807a6
4 changed files with 112 additions and 22 deletions

View File

@@ -11,6 +11,7 @@ import {
import type { IdentityArchiveActions } from "@/features/identity-archive/hooks";
import { ArchiveConfirmDialog } from "@/features/profile/ui/ArchiveConfirmDialog";
import { useAgentMemoryQuery } from "@/features/agent-memory/hooks";
import type { ManagedAgent } from "@/shared/api/types";
import {
AlertDialog,
@@ -31,6 +32,7 @@ export function UserProfileAgentManagementRows({
canDeleteAgent,
isDeletePending,
managedAgent,
channelCount,
onCreateCard,
onDeleteAgent,
onDuplicateAgent,
@@ -41,6 +43,7 @@ export function UserProfileAgentManagementRows({
canDeleteAgent: boolean;
isDeletePending: boolean;
managedAgent?: ManagedAgent;
channelCount: number;
/** Mint an agent trading card. Present only for owner-managed personas. */
onCreateCard?: () => void;
onDeleteAgent: () => void;
@@ -91,9 +94,11 @@ export function UserProfileAgentManagementRows({
) : null}
{canDeleteAgent ? (
<ProfileDeleteAgentRow
channelCount={channelCount}
isPending={isDeletePending}
managedAgent={managedAgent}
onDelete={onDeleteAgent}
onExport={onExportAgent}
/>
) : null}
</PanelSectionGroup>
@@ -194,15 +199,25 @@ function ProfileArchiveAgentRow({
}
function ProfileDeleteAgentRow({
channelCount,
isPending,
managedAgent,
onDelete,
onExport,
}: {
channelCount: number;
isPending: boolean;
managedAgent?: ManagedAgent;
onDelete: () => void;
onExport?: () => void;
}) {
const [confirmOpen, setConfirmOpen] = React.useState(false);
const memoryQuery = useAgentMemoryQuery(managedAgent?.pubkey, {
enabled: confirmOpen && managedAgent !== undefined,
});
const memoryCount =
memoryQuery.data &&
(memoryQuery.data.core ? 1 : 0) + memoryQuery.data.memories.length;
return (
<>
@@ -223,11 +238,15 @@ function ProfileDeleteAgentRow({
{managedAgent ? (
<AgentDeleteConfirmDialog
agent={managedAgent}
channelCount={channelCount}
isPending={isPending}
memoryCount={memoryCount}
memoriesLoading={memoryQuery.isLoading}
onConfirm={() => {
setConfirmOpen(false);
onDelete();
}}
onExport={onExport}
onOpenChange={setConfirmOpen}
open={confirmOpen}
/>
@@ -238,44 +257,82 @@ function ProfileDeleteAgentRow({
function AgentDeleteConfirmDialog({
agent,
channelCount,
isPending,
memoryCount,
memoriesLoading,
onConfirm,
onExport,
onOpenChange,
open,
}: {
agent: ManagedAgent;
channelCount: number;
isPending: boolean;
memoryCount?: number;
memoriesLoading: boolean;
onConfirm: () => void;
onExport?: () => void;
onOpenChange: (open: boolean) => void;
open: boolean;
}) {
const isProviderAgent = agent.backend.type === "provider";
const showExportRecommendation =
onExport !== undefined && memoryCount !== undefined && memoryCount > 0;
return (
<AlertDialog onOpenChange={onOpenChange} open={open}>
<AlertDialogContent data-testid="agent-delete-confirm-dialog">
<AlertDialogHeader>
<AlertDialogTitle>Delete this agent?</AlertDialogTitle>
<AlertDialogTitle>Delete {agent.name}?</AlertDialogTitle>
<AlertDialogDescription>
Deleting this agent stops and removes the agent from this community.
This permanently removes the agent, its saved key, and its channel
access.
</AlertDialogDescription>
</AlertDialogHeader>
<ul className="list-disc space-y-1.5 pl-5 text-sm text-muted-foreground">
<li>Removes the local management record and saved agent key</li>
<li>Removes the agent from every channel it belongs to</li>
<li>
Archives the agent&apos;s identity on the relay so it no longer
appears in member lists or mention suggestions
</li>
<li>
{isProviderAgent
? "Requests remote deletion; if it is online, Buzz first sends a shutdown command when possible. If the deployment cannot be reached through a channel, the remote process may keep running without local management."
: "Stops any local agent process before deleting the record"}
</li>
</ul>
<p className="text-sm text-muted-foreground">
Archive this agent if you want to hide it instead of removing it.
</p>
<div className="grid grid-cols-2 gap-3">
<div className="rounded-lg border bg-muted/30 px-4 py-3">
<p className="text-xs text-muted-foreground">Memories</p>
<p
className="mt-1 text-2xl font-semibold tabular-nums"
data-testid="agent-delete-memory-count"
>
{memoriesLoading ? "…" : (memoryCount ?? "—")}
</p>
</div>
<div className="rounded-lg border bg-muted/30 px-4 py-3">
<p className="text-xs text-muted-foreground">Channels</p>
<p
className="mt-1 text-2xl font-semibold tabular-nums"
data-testid="agent-delete-channel-count"
>
{channelCount}
</p>
</div>
</div>
{showExportRecommendation ? (
<p className="text-sm text-muted-foreground">
Want a backup?{" "}
<Button
className="h-auto p-0 align-baseline font-medium underline underline-offset-2"
data-testid="agent-delete-export-action"
onClick={() => {
onOpenChange(false);
onExport();
}}
type="button"
variant="link"
>
Export this agent
</Button>
.
</p>
) : null}
{isProviderAgent ? (
<p className="text-sm text-muted-foreground">
The remote process may keep running if Buzz can&apos;t reach it.
</p>
) : null}
<AlertDialogFooter>
<AlertDialogCancel asChild>
<Button type="button" variant="outline">

View File

@@ -511,6 +511,7 @@ export function ProfileSummaryView({
archiveActions={archiveActions}
canArchiveAgent={isBot && archiveActions.canArchive}
canDeleteAgent={canDeleteAgent}
channelCount={channelCount}
channelIdToName={channelIdToName}
isArchived={isArchived}
isDeleteAgentPending={isAgentActionPending}

View File

@@ -203,6 +203,7 @@ export function ProfileInfoTabContent({
archiveActions,
canArchiveAgent,
canDeleteAgent,
channelCount,
channelIdToName,
isArchived,
isDeleteAgentPending,
@@ -223,6 +224,7 @@ export function ProfileInfoTabContent({
archiveActions: IdentityArchiveActions;
canArchiveAgent: boolean;
canDeleteAgent: boolean;
channelCount: number;
channelIdToName: Record<string, string>;
isArchived: boolean;
isDeleteAgentPending: boolean;
@@ -311,6 +313,7 @@ export function ProfileInfoTabContent({
canDeleteAgent={canDeleteAgent}
isDeletePending={isDeleteAgentPending}
managedAgent={managedAgent}
channelCount={channelCount}
onCreateCard={onCreateCard}
onDeleteAgent={onDeleteAgent}
onDuplicateAgent={onDuplicateAgent}

View File

@@ -7,7 +7,11 @@ import type { RelayEvent } from "@/shared/api/types";
import { emojiAvatarDataUrl } from "@/features/profile/ui/ProfileAvatarEditor.utils";
import { waitForAnimations } from "../helpers/animations";
import { installMockBridge, TEST_IDENTITIES } from "../helpers/bridge";
import {
createMockAgentMemoryListing,
installMockBridge,
TEST_IDENTITIES,
} from "../helpers/bridge";
import { seedActiveIdentity } from "../helpers/onboarding";
function createCatalogEvent(input: {
@@ -2695,8 +2699,9 @@ test("duplicate instances stay visible in the gallery and agent profile", async
}) => {
const personaId = "custom:duplicate-auditor";
const primaryPubkey = TEST_IDENTITIES.alice.pubkey;
const additionalPubkey = TEST_IDENTITIES.charlie.pubkey;
const additionalPubkey = TEST_IDENTITIES.outsider.pubkey;
await installMockBridge(page, {
agentMemory: createMockAgentMemoryListing(),
personas: [
{
id: personaId,
@@ -2716,6 +2721,14 @@ test("duplicate instances stay visible in the gallery and agent profile", async
name: "Duplicate Auditor",
personaId,
status: "stopped",
channelNames: ["general", "random"],
},
],
relayAgents: [
{
pubkey: additionalPubkey,
name: "Duplicate Auditor",
channelNames: ["general", "random"],
},
],
});
@@ -2728,8 +2741,10 @@ test("duplicate instances stay visible in the gallery and agent profile", async
).toBeVisible();
await page.getByTestId(`managed-agent-${primaryPubkey}`).click();
await page.getByTestId("user-profile-tab-runtime").click();
await page.getByTestId("user-profile-instances").click();
await page.getByTestId(`user-profile-instance-${additionalPubkey}`).click();
await page.getByTestId("user-profile-tab-info").click();
await expect(page.getByTestId("user-profile-panel")).toBeVisible();
await expect(page.getByTestId("user-profile-agent-status")).toContainText(
@@ -2739,6 +2754,20 @@ test("duplicate instances stay visible in the gallery and agent profile", async
page.getByTestId("user-profile-settings-menu-trigger"),
).toHaveCount(0);
await expect(
page.getByTestId(`user-profile-agent-delete-${additionalPubkey}`),
).toHaveCount(0);
page.getByTestId("user-profile-duplicate-agent-row"),
).toBeVisible();
await expect(page.getByTestId("user-profile-export-agent-row")).toBeVisible();
await page.getByTestId("user-profile-delete-agent-row").click();
const deleteDialog = page.getByTestId("agent-delete-confirm-dialog");
await expect(deleteDialog).toBeVisible();
await expect(deleteDialog.getByRole("heading")).toHaveText(
"Delete Duplicate Auditor?",
);
await expect(page.getByTestId("agent-delete-memory-count")).toHaveText("9");
await expect(page.getByTestId("agent-delete-channel-count")).toHaveText("2");
await page.getByTestId("agent-delete-export-action").click();
await expect(deleteDialog).toHaveCount(0);
await expect(page.getByTestId("agent-snapshot-export-dialog")).toBeVisible();
});