mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
Remove agent creation success modal (#5063)
## Summary - remove the post-creation private-key modal - return directly to the underlying page with one “Agent created” toast - preserve failed channel-attachment retry through an actionable toast ## Validation - desktop checks and E2E build - 4,392 desktop unit tests - focused Playwright coverage for standard, customized, and attachment-retry creation flows Signed-off-by: kenny lopez <klopez4212@gmail.com>
This commit is contained in:
@@ -1,4 +0,0 @@
|
||||
export type AgentChannelAttachmentFailure = {
|
||||
channelName: string;
|
||||
error: string;
|
||||
};
|
||||
@@ -1,7 +1,6 @@
|
||||
import { useAgentManagement } from "@/features/agents/useAgentManagement";
|
||||
import { AgentCardDialogs } from "./AgentCardViewerDialog";
|
||||
import { AgentDialog } from "./AgentDialog";
|
||||
import { SecretRevealDialog } from "./SecretRevealDialog";
|
||||
|
||||
/** Global review surfaces opened by owned agents through the Buzz harness. */
|
||||
export function AgentManagementDialogs() {
|
||||
@@ -25,19 +24,6 @@ export function AgentManagementDialogs() {
|
||||
runtimeCatalogStatus={management.runtimeCatalogStatus}
|
||||
/>
|
||||
) : null}
|
||||
{management.createdAgent ? (
|
||||
<SecretRevealDialog
|
||||
attachmentFailure={management.attachmentFailure}
|
||||
created={management.createdAgent}
|
||||
isRetryingAttachment={management.isRetryingAttachment}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) management.dismissCreatedAgent();
|
||||
}}
|
||||
onRetryAttachment={() => {
|
||||
void management.retryAttachment();
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{management.request?.action === "update" ? (
|
||||
<AgentDialog
|
||||
description=""
|
||||
|
||||
@@ -16,7 +16,6 @@ import { AgentSnapshotImportDialog } from "./AgentSnapshotImportDialog";
|
||||
import { TeamSnapshotExportDialog } from "./TeamSnapshotExportDialog";
|
||||
import { TeamSnapshotImportDialog } from "./TeamSnapshotImportDialog";
|
||||
import { TeamShareDialog } from "./TeamShareDialog";
|
||||
import { SecretRevealDialog } from "./SecretRevealDialog";
|
||||
import { TeamDeleteDialog } from "./TeamDeleteDialog";
|
||||
import { TeamDialog } from "./TeamDialog";
|
||||
import { TeamsSection } from "./TeamsSection";
|
||||
@@ -344,24 +343,6 @@ export function AgentsView() {
|
||||
open={agents.agentToAddToChannel !== null}
|
||||
/>
|
||||
) : null}
|
||||
{agents.createdAgent ? (
|
||||
<SecretRevealDialog
|
||||
created={agents.createdAgent}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) {
|
||||
agents.setCreatedAgent(null);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{personas.createdAgent ? (
|
||||
<SecretRevealDialog
|
||||
created={personas.createdAgent}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) personas.dismissCreatedAgent();
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
{personas.personaDialogState ? (
|
||||
<AgentDialog
|
||||
description={personas.personaDialogState.description}
|
||||
|
||||
@@ -6,7 +6,6 @@ import {
|
||||
type OpenCreateAgentOptions,
|
||||
} from "@/features/agents/openCreateAgentEvent";
|
||||
import { AgentDialog } from "./AgentDialog";
|
||||
import { SecretRevealDialog } from "./SecretRevealDialog";
|
||||
import { usePersonaActions } from "./usePersonaActions";
|
||||
|
||||
/** App-level create flow so contextual entry points do not navigate away. */
|
||||
@@ -64,19 +63,6 @@ export function RequestedAgentCreateDialogs() {
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
{personas.createdAgent ? (
|
||||
<SecretRevealDialog
|
||||
attachmentFailure={personas.attachmentFailure}
|
||||
created={personas.createdAgent}
|
||||
isRetryingAttachment={personas.isRetryingAttachment}
|
||||
onOpenChange={(open) => {
|
||||
if (!open) personas.dismissCreatedAgent();
|
||||
}}
|
||||
onRetryAttachment={() => {
|
||||
void personas.retryAttachment();
|
||||
}}
|
||||
/>
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,121 +0,0 @@
|
||||
import type { AgentChannelAttachmentFailure } from "@/features/agents/channelAttachmentFailure";
|
||||
import type { CreateManagedAgentResponse } from "@/shared/api/types";
|
||||
import { Button } from "@/shared/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@/shared/ui/dialog";
|
||||
import { CopyButton } from "./CopyButton";
|
||||
|
||||
export function SecretRevealDialog({
|
||||
attachmentFailure,
|
||||
created,
|
||||
isRetryingAttachment = false,
|
||||
onOpenChange,
|
||||
onRetryAttachment,
|
||||
}: {
|
||||
attachmentFailure?: AgentChannelAttachmentFailure | null;
|
||||
created: CreateManagedAgentResponse | null;
|
||||
isRetryingAttachment?: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onRetryAttachment?: () => void;
|
||||
}) {
|
||||
return (
|
||||
<Dialog onOpenChange={onOpenChange} open={created !== null}>
|
||||
<DialogContent className="max-w-2xl overflow-hidden p-0">
|
||||
<div className="flex max-h-[85vh] flex-col">
|
||||
<DialogHeader className="border-b border-border/60 px-6 py-5 pr-14">
|
||||
<DialogTitle>Agent created</DialogTitle>
|
||||
<DialogDescription>
|
||||
Save the private key now. The app can keep running the harness
|
||||
locally, but this secret is only revealed here.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="flex-1 space-y-4 overflow-y-auto px-6 py-5">
|
||||
{created ? (
|
||||
<>
|
||||
<div className="rounded-2xl border border-border/70 bg-muted/20 p-4">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<div>
|
||||
<p className="text-sm font-semibold tracking-tight">
|
||||
Private key (nsec)
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
This is the agent identity used by `buzz-acp`.
|
||||
</p>
|
||||
</div>
|
||||
<CopyButton
|
||||
label="Copy key"
|
||||
value={created.privateKeyNsec}
|
||||
/>
|
||||
</div>
|
||||
<code className="mt-3 block break-all rounded-xl border border-border/70 bg-background/80 px-3 py-2 text-xs">
|
||||
{created.privateKeyNsec}
|
||||
</code>
|
||||
</div>
|
||||
|
||||
{created.profileSyncError ? (
|
||||
<p className="rounded-2xl border border-amber-500/30 bg-amber-500/10 px-4 py-3 text-sm text-warning">
|
||||
{created.profileSyncError}
|
||||
</p>
|
||||
) : null}
|
||||
|
||||
{created.spawnError ? (
|
||||
<p className="rounded-2xl border border-destructive/30 bg-destructive/10 px-4 py-3 text-sm text-destructive">
|
||||
{created.spawnError}
|
||||
</p>
|
||||
) : attachmentFailure ? (
|
||||
<div
|
||||
className="space-y-1 rounded-2xl border border-destructive/30 bg-destructive/10 px-4 py-3 text-sm text-destructive"
|
||||
role="alert"
|
||||
>
|
||||
<p>
|
||||
{created.agent.name} was created, but couldn’t be added to
|
||||
#{attachmentFailure.channelName}.
|
||||
</p>
|
||||
<p>{attachmentFailure.error}</p>
|
||||
</div>
|
||||
) : (
|
||||
<p className="rounded-2xl border border-primary/20 bg-primary/10 px-4 py-3 text-sm text-primary">
|
||||
{created.agent.name} is ready
|
||||
{created.agent.status === "running"
|
||||
? " and running."
|
||||
: created.agent.status === "deployed"
|
||||
? " and deployed."
|
||||
: "."}
|
||||
</p>
|
||||
)}
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2 border-t border-border/60 px-6 py-4">
|
||||
{attachmentFailure && onRetryAttachment ? (
|
||||
<Button
|
||||
disabled={isRetryingAttachment}
|
||||
onClick={onRetryAttachment}
|
||||
size="sm"
|
||||
type="button"
|
||||
>
|
||||
{isRetryingAttachment ? "Trying again…" : "Try again"}
|
||||
</Button>
|
||||
) : null}
|
||||
<Button
|
||||
disabled={isRetryingAttachment}
|
||||
onClick={() => onOpenChange(false)}
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="outline"
|
||||
>
|
||||
Done
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as React from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import {
|
||||
type AttachManagedAgentToChannelResult,
|
||||
@@ -15,12 +16,7 @@ import {
|
||||
import { useGlobalAgentConfig } from "@/features/agents/useGlobalAgentConfig";
|
||||
import { useChannelsQuery } from "@/features/channels/hooks";
|
||||
import { usePresenceQuery } from "@/features/presence/hooks";
|
||||
import type {
|
||||
AgentPersona,
|
||||
Channel,
|
||||
CreateManagedAgentResponse,
|
||||
ManagedAgent,
|
||||
} from "@/shared/api/types";
|
||||
import type { AgentPersona, Channel, ManagedAgent } from "@/shared/api/types";
|
||||
import { removeChannelMember } from "@/shared/api/tauri";
|
||||
import { normalizePubkey } from "@/shared/lib/pubkey";
|
||||
import {
|
||||
@@ -52,8 +48,6 @@ export function useManagedAgentActions() {
|
||||
const [isCreateOpen, setIsCreateOpen] = React.useState(false);
|
||||
const [agentToAddToChannel, setAgentToAddToChannel] =
|
||||
React.useState<ManagedAgent | null>(null);
|
||||
const [createdAgent, setCreatedAgent] =
|
||||
React.useState<CreateManagedAgentResponse | null>(null);
|
||||
const [startingPersonaIds, setStartingPersonaIds] = React.useState<
|
||||
ReadonlySet<string>
|
||||
>(() => new Set());
|
||||
@@ -229,13 +223,11 @@ export function useManagedAgentActions() {
|
||||
const input = await buildInstanceInputForDefinition(persona, runtime);
|
||||
|
||||
const created = await createAgentMutation.mutateAsync(input);
|
||||
setCreatedAgent(created);
|
||||
toast.success("Agent created");
|
||||
const notices = [...warnings];
|
||||
|
||||
if (created.spawnError) {
|
||||
setActionErrorMessage(created.spawnError);
|
||||
} else {
|
||||
notices.push(`Started ${created.agent.name}.`);
|
||||
}
|
||||
|
||||
if (created.profileSyncError) {
|
||||
@@ -440,8 +432,6 @@ export function useManagedAgentActions() {
|
||||
setIsCreateOpen,
|
||||
agentToAddToChannel,
|
||||
setAgentToAddToChannel,
|
||||
createdAgent,
|
||||
setCreatedAgent,
|
||||
logAgentPubkey,
|
||||
setLogAgentPubkey,
|
||||
actionNoticeMessage,
|
||||
|
||||
@@ -254,10 +254,6 @@ export function usePersonaActions() {
|
||||
setPersonaErrorMessage(
|
||||
`${persona.displayName} was created, but it did not start: ${created.spawnError}`,
|
||||
);
|
||||
} else {
|
||||
setPersonaNoticeMessage(
|
||||
`Created and started ${created.agent.name}.`,
|
||||
);
|
||||
}
|
||||
if (created.profileSyncError) {
|
||||
setPersonaErrorMessage(
|
||||
|
||||
@@ -1,82 +1,74 @@
|
||||
import * as React from "react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { attachManagedAgentToChannel } from "./channelAgents";
|
||||
import type { AgentChannelAttachmentFailure } from "./channelAttachmentFailure";
|
||||
import type { Channel, CreateManagedAgentResponse } from "@/shared/api/types";
|
||||
|
||||
type TargetChannel = Pick<Channel, "id" | "name">;
|
||||
|
||||
/**
|
||||
* Keeps agent creation successful even when the follow-up channel attachment
|
||||
* fails, and retries only that attachment rather than recreating the agent.
|
||||
*/
|
||||
async function attach(
|
||||
created: CreateManagedAgentResponse,
|
||||
targetChannel: TargetChannel,
|
||||
) {
|
||||
const attached = await attachManagedAgentToChannel(targetChannel.id, {
|
||||
agent: created.agent,
|
||||
role: "bot",
|
||||
ensureRunning: true,
|
||||
});
|
||||
created.agent = attached.agent;
|
||||
}
|
||||
|
||||
function showAttachmentFailure(
|
||||
created: CreateManagedAgentResponse,
|
||||
targetChannel: TargetChannel,
|
||||
cause: unknown,
|
||||
toastId?: string | number,
|
||||
) {
|
||||
const error = cause instanceof Error ? cause.message : "Failed to add agent.";
|
||||
const id = toast.warning("Agent created", {
|
||||
description: `${created.agent.name} couldn’t be added to #${targetChannel.name}. ${error}`,
|
||||
id: toastId,
|
||||
action: {
|
||||
label: "Try again",
|
||||
onClick: (event) => {
|
||||
event.preventDefault();
|
||||
toast.loading("Agent created", {
|
||||
description: `Adding ${created.agent.name} to #${targetChannel.name}…`,
|
||||
id,
|
||||
});
|
||||
void attach(created, targetChannel).then(
|
||||
() => {
|
||||
toast.success("Agent created", {
|
||||
description: `Added ${created.agent.name} to #${targetChannel.name}`,
|
||||
id,
|
||||
});
|
||||
},
|
||||
(retryCause: unknown) => {
|
||||
showAttachmentFailure(created, targetChannel, retryCause, id);
|
||||
},
|
||||
);
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/** Keeps creation successful when its optional channel attachment fails. */
|
||||
export function useCreatedAgentChannelAttachment() {
|
||||
const [createdAgent, setCreatedAgent] =
|
||||
React.useState<CreateManagedAgentResponse | null>(null);
|
||||
const [attachmentFailure, setAttachmentFailure] =
|
||||
React.useState<AgentChannelAttachmentFailure | null>(null);
|
||||
const targetChannelRef = React.useRef<TargetChannel | null>(null);
|
||||
const [isRetryingAttachment, setIsRetryingAttachment] = React.useState(false);
|
||||
|
||||
async function attach(
|
||||
created: CreateManagedAgentResponse,
|
||||
targetChannel: TargetChannel,
|
||||
) {
|
||||
targetChannelRef.current = targetChannel;
|
||||
try {
|
||||
const attached = await attachManagedAgentToChannel(targetChannel.id, {
|
||||
agent: created.agent,
|
||||
role: "bot",
|
||||
ensureRunning: true,
|
||||
});
|
||||
created.agent = attached.agent;
|
||||
targetChannelRef.current = null;
|
||||
setAttachmentFailure(null);
|
||||
} catch (cause) {
|
||||
setAttachmentFailure({
|
||||
channelName: targetChannel.name,
|
||||
error: cause instanceof Error ? cause.message : "Failed to add agent.",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async function presentCreatedAgent(
|
||||
created: CreateManagedAgentResponse,
|
||||
targetChannel?: TargetChannel | null,
|
||||
) {
|
||||
setAttachmentFailure(null);
|
||||
targetChannelRef.current = null;
|
||||
if (!created.spawnError && targetChannel) {
|
||||
await attach(created, targetChannel);
|
||||
if (created.spawnError || !targetChannel) {
|
||||
toast.success("Agent created");
|
||||
return;
|
||||
}
|
||||
setCreatedAgent({ ...created });
|
||||
}
|
||||
|
||||
async function retryAttachment() {
|
||||
const targetChannel = targetChannelRef.current;
|
||||
if (!createdAgent || !targetChannel || isRetryingAttachment) return;
|
||||
|
||||
setIsRetryingAttachment(true);
|
||||
try {
|
||||
await attach(createdAgent, targetChannel);
|
||||
setCreatedAgent({ ...createdAgent });
|
||||
} finally {
|
||||
setIsRetryingAttachment(false);
|
||||
await attach(created, targetChannel);
|
||||
toast.success("Agent created");
|
||||
} catch (cause) {
|
||||
showAttachmentFailure(created, targetChannel, cause);
|
||||
}
|
||||
}
|
||||
|
||||
function dismissCreatedAgent() {
|
||||
setCreatedAgent(null);
|
||||
setAttachmentFailure(null);
|
||||
targetChannelRef.current = null;
|
||||
}
|
||||
|
||||
return {
|
||||
attachmentFailure,
|
||||
createdAgent,
|
||||
dismissCreatedAgent,
|
||||
isRetryingAttachment,
|
||||
presentCreatedAgent,
|
||||
retryAttachment,
|
||||
};
|
||||
return { presentCreatedAgent };
|
||||
}
|
||||
|
||||
@@ -175,9 +175,12 @@ test("create agent persists Buzz shared compute with auto model", async ({
|
||||
const model = page.locator("#persona-model");
|
||||
await expect(model).toContainText("Automatic");
|
||||
await page.getByTestId("persona-dialog-submit").click();
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Agent created" }),
|
||||
).toBeVisible({ timeout: 10_000 });
|
||||
const createdToast = page
|
||||
.locator("[data-sonner-toast][data-removed='false']")
|
||||
.filter({ hasText: "Agent created" });
|
||||
await expect(createdToast).toBeVisible({ timeout: 10_000 });
|
||||
await expect(createdToast).toHaveCount(1);
|
||||
await expect(page.getByRole("dialog")).toHaveCount(0);
|
||||
|
||||
const createPayload = await page.evaluate((name) => {
|
||||
const log = (
|
||||
@@ -252,10 +255,12 @@ test("create agent supports parallelism and system prompt overrides", async ({
|
||||
// the definition (agents always start after creation).
|
||||
await page.getByTestId("persona-dialog-submit").click();
|
||||
|
||||
await expect(
|
||||
page.getByRole("heading", { name: "Agent created" }),
|
||||
).toBeVisible({ timeout: 10_000 });
|
||||
await page.getByRole("button", { name: "Done" }).click();
|
||||
const createdToast = page
|
||||
.locator("[data-sonner-toast][data-removed='false']")
|
||||
.filter({ hasText: "Agent created" });
|
||||
await expect(createdToast).toBeVisible({ timeout: 10_000 });
|
||||
await expect(createdToast).toHaveCount(1);
|
||||
await expect(page.getByRole("dialog")).toHaveCount(0);
|
||||
|
||||
await expect(page.getByTestId("agents-library-personas")).toContainText(
|
||||
agentName,
|
||||
|
||||
@@ -159,16 +159,16 @@ test.describe("welcome and channel agent entry points", () => {
|
||||
await expect(page.getByTestId("persona-dialog-submit")).toBeEnabled();
|
||||
await page.getByTestId("persona-dialog-submit").click();
|
||||
|
||||
const createdDialog = page.getByRole("dialog");
|
||||
await expect(
|
||||
createdDialog.getByRole("heading", { name: "Agent created" }),
|
||||
).toBeVisible({ timeout: 10_000 });
|
||||
await expect(createdDialog).toContainText(
|
||||
"Scout was created, but couldn’t be added to #random.",
|
||||
const createdToast = page
|
||||
.locator("[data-sonner-toast][data-removed='false']")
|
||||
.filter({ hasText: "Agent created" });
|
||||
await expect(createdToast).toBeVisible({ timeout: 10_000 });
|
||||
await expect(createdToast).toContainText(
|
||||
"Scout couldn’t be added to #random. Relay unavailable.",
|
||||
);
|
||||
await expect(createdDialog).toContainText("Relay unavailable.");
|
||||
await expect(page.getByRole("dialog")).toHaveCount(0);
|
||||
await waitForAnimations(page);
|
||||
await createdDialog.screenshot({
|
||||
await createdToast.screenshot({
|
||||
path: `${SHOTS}/05-agent-channel-attachment-failed.png`,
|
||||
});
|
||||
|
||||
@@ -179,10 +179,19 @@ test.describe("welcome and channel agent entry points", () => {
|
||||
);
|
||||
const addCount = commandCount(commandsBeforeRetry, "add_channel_members");
|
||||
|
||||
await createdDialog.getByRole("button", { name: "Try again" }).click();
|
||||
await expect(createdDialog).toContainText("Scout is ready and running.");
|
||||
await createdToast.getByRole("button", { name: "Try again" }).click();
|
||||
await expect
|
||||
.poll(async () => {
|
||||
const commands = await readCommandLog(page);
|
||||
return commandCount(commands, "add_channel_members");
|
||||
})
|
||||
.toEqual(addCount + 1);
|
||||
const attachedToast = page
|
||||
.locator("[data-sonner-toast][data-removed='false']")
|
||||
.filter({ hasText: "Added Scout to #random" });
|
||||
await expect(attachedToast).toBeVisible();
|
||||
await expect(
|
||||
createdDialog.getByRole("button", { name: "Try again" }),
|
||||
attachedToast.getByRole("button", { name: "Try again" }),
|
||||
).toHaveCount(0);
|
||||
|
||||
const commandsAfterRetry = await readCommandLog(page);
|
||||
@@ -192,7 +201,6 @@ test.describe("welcome and channel agent entry points", () => {
|
||||
expect(commandCount(commandsAfterRetry, "add_channel_members")).toEqual(
|
||||
addCount + 1,
|
||||
);
|
||||
await createdDialog.getByRole("button", { name: "Done" }).click();
|
||||
await expect(page.getByTestId("chat-title")).toHaveText("random");
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user