diff --git a/desktop/src/features/agents/channelAttachmentFailure.ts b/desktop/src/features/agents/channelAttachmentFailure.ts deleted file mode 100644 index e3248829af..0000000000 --- a/desktop/src/features/agents/channelAttachmentFailure.ts +++ /dev/null @@ -1,4 +0,0 @@ -export type AgentChannelAttachmentFailure = { - channelName: string; - error: string; -}; diff --git a/desktop/src/features/agents/ui/AgentManagementDialogs.tsx b/desktop/src/features/agents/ui/AgentManagementDialogs.tsx index b72669e5f6..27541889f2 100644 --- a/desktop/src/features/agents/ui/AgentManagementDialogs.tsx +++ b/desktop/src/features/agents/ui/AgentManagementDialogs.tsx @@ -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 ? ( - { - if (!open) management.dismissCreatedAgent(); - }} - onRetryAttachment={() => { - void management.retryAttachment(); - }} - /> - ) : null} {management.request?.action === "update" ? ( ) : null} - {agents.createdAgent ? ( - { - if (!open) { - agents.setCreatedAgent(null); - } - }} - /> - ) : null} - {personas.createdAgent ? ( - { - if (!open) personas.dismissCreatedAgent(); - }} - /> - ) : null} {personas.personaDialogState ? ( ) : null} - {personas.createdAgent ? ( - { - if (!open) personas.dismissCreatedAgent(); - }} - onRetryAttachment={() => { - void personas.retryAttachment(); - }} - /> - ) : null} ); } diff --git a/desktop/src/features/agents/ui/SecretRevealDialog.tsx b/desktop/src/features/agents/ui/SecretRevealDialog.tsx deleted file mode 100644 index e1826cec6e..0000000000 --- a/desktop/src/features/agents/ui/SecretRevealDialog.tsx +++ /dev/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 ( - - -
- - Agent created - - Save the private key now. The app can keep running the harness - locally, but this secret is only revealed here. - - - -
- {created ? ( - <> -
-
-
-

- Private key (nsec) -

-

- This is the agent identity used by `buzz-acp`. -

-
- -
- - {created.privateKeyNsec} - -
- - {created.profileSyncError ? ( -

- {created.profileSyncError} -

- ) : null} - - {created.spawnError ? ( -

- {created.spawnError} -

- ) : attachmentFailure ? ( -
-

- {created.agent.name} was created, but couldn’t be added to - #{attachmentFailure.channelName}. -

-

{attachmentFailure.error}

-
- ) : ( -

- {created.agent.name} is ready - {created.agent.status === "running" - ? " and running." - : created.agent.status === "deployed" - ? " and deployed." - : "."} -

- )} - - ) : null} -
- -
- {attachmentFailure && onRetryAttachment ? ( - - ) : null} - -
-
-
-
- ); -} diff --git a/desktop/src/features/agents/ui/useManagedAgentActions.ts b/desktop/src/features/agents/ui/useManagedAgentActions.ts index 6068ad1639..8270bea11f 100644 --- a/desktop/src/features/agents/ui/useManagedAgentActions.ts +++ b/desktop/src/features/agents/ui/useManagedAgentActions.ts @@ -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(null); - const [createdAgent, setCreatedAgent] = - React.useState(null); const [startingPersonaIds, setStartingPersonaIds] = React.useState< ReadonlySet >(() => 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, diff --git a/desktop/src/features/agents/ui/usePersonaActions.ts b/desktop/src/features/agents/ui/usePersonaActions.ts index 2c7668969a..d3832cdc64 100644 --- a/desktop/src/features/agents/ui/usePersonaActions.ts +++ b/desktop/src/features/agents/ui/usePersonaActions.ts @@ -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( diff --git a/desktop/src/features/agents/useCreatedAgentChannelAttachment.ts b/desktop/src/features/agents/useCreatedAgentChannelAttachment.ts index e016766c2e..a754469f69 100644 --- a/desktop/src/features/agents/useCreatedAgentChannelAttachment.ts +++ b/desktop/src/features/agents/useCreatedAgentChannelAttachment.ts @@ -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; -/** - * Keeps agent creation successful even when the follow-up channel attachment - * fails, and retries only that attachment rather than recreating the agent. - */ -export function useCreatedAgentChannelAttachment() { - const [createdAgent, setCreatedAgent] = - React.useState(null); - const [attachmentFailure, setAttachmentFailure] = - React.useState(null); - const targetChannelRef = React.useRef(null); - const [isRetryingAttachment, setIsRetryingAttachment] = React.useState(false); +async function attach( + created: CreateManagedAgentResponse, + targetChannel: TargetChannel, +) { + const attached = await attachManagedAgentToChannel(targetChannel.id, { + agent: created.agent, + role: "bot", + ensureRunning: true, + }); + created.agent = attached.agent; +} - 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.", - }); - } - } +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() { 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 }; } diff --git a/desktop/tests/e2e/smoke.spec.ts b/desktop/tests/e2e/smoke.spec.ts index 0f61de0f7c..2aa9f890f3 100644 --- a/desktop/tests/e2e/smoke.spec.ts +++ b/desktop/tests/e2e/smoke.spec.ts @@ -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, diff --git a/desktop/tests/e2e/welcome-agent-modal-screenshots.spec.ts b/desktop/tests/e2e/welcome-agent-modal-screenshots.spec.ts index c187353101..0115b608bf 100644 --- a/desktop/tests/e2e/welcome-agent-modal-screenshots.spec.ts +++ b/desktop/tests/e2e/welcome-agent-modal-screenshots.spec.ts @@ -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"); });