diff --git a/desktop/src/features/agents/lib/managedAgentControlActions.test.mjs b/desktop/src/features/agents/lib/managedAgentControlActions.test.mjs index e6926b36d2..a55653a77a 100644 --- a/desktop/src/features/agents/lib/managedAgentControlActions.test.mjs +++ b/desktop/src/features/agents/lib/managedAgentControlActions.test.mjs @@ -2,10 +2,60 @@ import assert from "node:assert/strict"; import test from "node:test"; import { + isProviderDeployedManagedAgent, + providerDeployedAgentForPersonaDelete, startManagedAgentWithRules, respawnManagedAgentWithRules, } from "./managedAgentControlActions.ts"; +test("isProviderDeployedManagedAgent requires a provider backend and deployment id", () => { + assert.equal( + isProviderDeployedManagedAgent({ + backend: { type: "provider", id: "blox" }, + backendAgentId: "remote-1", + }), + true, + ); + assert.equal( + isProviderDeployedManagedAgent({ + backend: { type: "provider", id: "blox" }, + backendAgentId: null, + }), + false, + ); + assert.equal( + isProviderDeployedManagedAgent({ + backend: { type: "local" }, + backendAgentId: null, + }), + false, + ); +}); + +test("providerDeployedAgentForPersonaDelete returns the profile provider instance", () => { + const personaId = "persona-1"; + const local = agent({ + personaId, + backend: { type: "local" }, + }); + const provider = agent({ + personaId, + pubkey: "aa".repeat(32), + backend: { type: "provider", id: "blox" }, + backendAgentId: "remote-1", + }); + + assert.equal( + providerDeployedAgentForPersonaDelete(personaId, [local, provider], provider) + ?.pubkey, + provider.pubkey, + ); + assert.equal( + providerDeployedAgentForPersonaDelete(personaId, [local], local), + null, + ); +}); + function agent(overrides = {}) { return { pubkey: "deadbeef".repeat(8), diff --git a/desktop/src/features/agents/lib/managedAgentControlActions.ts b/desktop/src/features/agents/lib/managedAgentControlActions.ts index dbaaaba803..6f73af38ab 100644 --- a/desktop/src/features/agents/lib/managedAgentControlActions.ts +++ b/desktop/src/features/agents/lib/managedAgentControlActions.ts @@ -35,6 +35,31 @@ export function isManagedAgentActive(agent: Pick) { return agent.status === "running" || agent.status === "deployed"; } +/** Provider-backed instance with a live remote deployment id. */ +export function isProviderDeployedManagedAgent( + agent: Pick, +): boolean { + return agent.backend.type === "provider" && Boolean(agent.backendAgentId); +} + +/** + * When a persona card's primary instance is provider-deployed, Delete must + * remove the managed agent (with force_remote_delete) instead of cascading + * persona deletion, which the backend refuses for live remote deployments. + */ +export function providerDeployedAgentForPersonaDelete( + personaId: string, + managedAgents: readonly ManagedAgent[], + profileAgent?: ManagedAgent, +): ManagedAgent | null { + const candidates = managedAgents.filter((agent) => agent.personaId === personaId); + const primary = profileAgent ?? candidates[0]; + if (primary && isProviderDeployedManagedAgent(primary)) { + return primary; + } + return null; +} + export function getManagedAgentPrimaryActionLabel(agent: ManagedAgent) { if (agent.backend.type === "provider") { return isManagedAgentActive(agent) ? "Shutdown" : "Deploy"; diff --git a/desktop/src/features/agents/ui/AgentsView.tsx b/desktop/src/features/agents/ui/AgentsView.tsx index 3d1673c365..f182096688 100644 --- a/desktop/src/features/agents/ui/AgentsView.tsx +++ b/desktop/src/features/agents/ui/AgentsView.tsx @@ -21,9 +21,11 @@ import { TeamDeleteDialog } from "./TeamDeleteDialog"; import { TeamDialog } from "./TeamDialog"; import { TeamsSection } from "./TeamsSection"; import { UnifiedAgentsSection } from "./UnifiedAgentsSection"; +import { providerDeployedAgentForPersonaDelete } from "@/features/agents/lib/managedAgentControlActions"; import { useManagedAgentActions } from "./useManagedAgentActions"; import { usePersonaActions } from "./usePersonaActions"; import { useTeamActions } from "./useTeamActions"; +import { pickProfileAgent } from "./unifiedAgentGroups"; import { useProfilePanel } from "@/shared/context/ProfilePanelContext"; import { useBakedBuildEnvQuery } from "@/features/agents/hooks"; import { isManagedAgentActive } from "@/features/agents/lib/managedAgentControlActions"; @@ -130,6 +132,25 @@ export function AgentsView() { }); }, []); + const handleDeletePersonaFromLibrary = React.useCallback( + (persona: Parameters[0]) => { + const linkedAgents = (agents.managedAgents ?? []).filter( + (candidate) => candidate.personaId === persona.id, + ); + const providerAgent = providerDeployedAgentForPersonaDelete( + persona.id, + agents.managedAgents ?? [], + pickProfileAgent(linkedAgents), + ); + if (providerAgent) { + void agents.handleDelete(providerAgent.pubkey); + return; + } + personas.openDelete(persona); + }, + [agents, personas], + ); + return ( <>
@@ -265,7 +286,7 @@ export function AgentsView() { onDeactivatePersona={(persona) => { void personas.handleSetActive(persona, false, "library"); }} - onDeletePersona={personas.openDelete} + onDeletePersona={handleDeletePersonaFromLibrary} onImportSnapshotFile={(fileBytes, fileName) => { void personas.handleImportSnapshotFile(fileBytes, fileName); }}