diff --git a/desktop/src/features/agents/lib/pickProfileAgent.test.mjs b/desktop/src/features/agents/lib/pickProfileAgent.test.mjs index 710b5fc4be8..e404b8c4dd5 100644 --- a/desktop/src/features/agents/lib/pickProfileAgent.test.mjs +++ b/desktop/src/features/agents/lib/pickProfileAgent.test.mjs @@ -33,6 +33,24 @@ test("the shared profile target prefers the active persona instance", () => { assert.equal(pickProfileAgent([running, stopped], NONE_ARCHIVED), running); }); +test("the active community identity wins over a running sibling", () => { + const legacy = agent({ + pubkey: "a".repeat(64), + relayUrl: "wss://legacy.example", + status: "running", + }); + const current = agent({ + pubkey: "b".repeat(64), + relayUrl: "wss://current.example", + status: "stopped", + }); + + assert.equal( + pickProfileAgent([legacy, current], NONE_ARCHIVED, "wss://current.example"), + current, + ); +}); + test("an archived instance early in file order cannot hijack the target", () => { const archived = agent({ name: "Archived instance", diff --git a/desktop/src/features/agents/lib/pickProfileAgent.ts b/desktop/src/features/agents/lib/pickProfileAgent.ts index dc2437c86ea..6e795a01bcc 100644 --- a/desktop/src/features/agents/lib/pickProfileAgent.ts +++ b/desktop/src/features/agents/lib/pickProfileAgent.ts @@ -12,15 +12,22 @@ import type { ManagedAgent } from "@/shared/api/types"; * file order can't hijack the persona target. Returns `undefined` when every * instance is archived — the card then renders in persona-only mode. The * `isArchived` predicate is fail-open (returns `false` while the relay archive - * snapshot loads), so a cold start never briefly picks nothing. + * snapshot loads), so a cold start never briefly picks nothing. When the same + * persona has identities from several communities, the active community's + * identity wins before runtime status. */ export function pickProfileAgent( agents: readonly ManagedAgent[], isArchived: (pubkey: string) => boolean, + preferredRelayUrl?: string, ) { return [...agents] .filter((agent) => !isArchived(agent.pubkey)) .sort((left, right) => { + const relayDiff = + Number(right.relayUrl === preferredRelayUrl) - + Number(left.relayUrl === preferredRelayUrl); + if (relayDiff !== 0) return relayDiff; const activeDiff = Number(isManagedAgentActive(right)) - Number(isManagedAgentActive(left)); diff --git a/desktop/src/features/agents/ui/UnifiedAgentsSection.tsx b/desktop/src/features/agents/ui/UnifiedAgentsSection.tsx index d0ff2e2738a..42349a3f684 100644 --- a/desktop/src/features/agents/ui/UnifiedAgentsSection.tsx +++ b/desktop/src/features/agents/ui/UnifiedAgentsSection.tsx @@ -9,6 +9,7 @@ import { resolveAgentCardModelLabel } from "@/features/agents/lib/agentCardModel import { friendlyAgentLastError } from "@/features/agents/lib/friendlyAgentLastError"; import { isManagedAgentActive } from "@/features/agents/lib/managedAgentControlActions"; import { pickProfileAgent } from "@/features/agents/lib/pickProfileAgent"; +import { useCommunities } from "@/features/communities/useCommunities"; import { useIsArchivedPredicate } from "@/features/identity-archive/hooks"; import { useUserProfileQuery } from "@/features/profile/hooks"; import type { AgentPersona, ManagedAgent } from "@/shared/api/types"; @@ -95,6 +96,7 @@ export function UnifiedAgentsSection(props: UnifiedAgentsSectionProps) { onDeletePersona, } = props; + const { activeCommunity } = useCommunities(); const isArchived = useIsArchivedPredicate(); const { groups, ungrouped, unknown } = React.useMemo( () => buildUnifiedGroups(personas, agents, isArchived), @@ -131,7 +133,11 @@ export function UnifiedAgentsSection(props: UnifiedAgentsSectionProps) { onClick={onOpenCatalog} /> {groups.map((group) => { - const profileAgent = pickProfileAgent(group.agents, isArchived); + const profileAgent = pickProfileAgent( + group.agents, + isArchived, + activeCommunity?.relayUrl, + ); return ( ( diff --git a/desktop/src/features/agents/ui/UnifiedAgentsSectionCardTarget.test.mjs b/desktop/src/features/agents/ui/UnifiedAgentsSectionCardTarget.test.mjs index 690a921040e..e94aab0655b 100644 --- a/desktop/src/features/agents/ui/UnifiedAgentsSectionCardTarget.test.mjs +++ b/desktop/src/features/agents/ui/UnifiedAgentsSectionCardTarget.test.mjs @@ -40,6 +40,7 @@ let screen; let createElement; let QueryClient; let QueryClientProvider; +let CommunitiesProvider; let UnifiedAgentsSection; const ipcHandlers = new Map(); @@ -118,7 +119,11 @@ function renderSection(props) { createElement( QueryClientProvider, { client }, - createElement(UnifiedAgentsSection, props), + createElement( + CommunitiesProvider, + null, + createElement(UnifiedAgentsSection, props), + ), ), ); } @@ -127,6 +132,7 @@ before(async () => { Object.assign(globalThis, { document: dom.window.document, HTMLElement: dom.window.HTMLElement, + localStorage: dom.window.localStorage, window: dom.window, IS_REACT_ACT_ENVIRONMENT: true, }); @@ -156,6 +162,9 @@ before(async () => { ({ QueryClient, QueryClientProvider } = await import( "@tanstack/react-query" )); + ({ CommunitiesProvider } = await import( + "@/features/communities/useCommunities" + )); ({ UnifiedAgentsSection } = await import("./UnifiedAgentsSection.tsx")); }); diff --git a/desktop/src/features/agents/ui/useManagedAgentActions.ts b/desktop/src/features/agents/ui/useManagedAgentActions.ts index 0627ad6ac36..caf35204d7c 100644 --- a/desktop/src/features/agents/ui/useManagedAgentActions.ts +++ b/desktop/src/features/agents/ui/useManagedAgentActions.ts @@ -17,6 +17,7 @@ import { import { useGlobalAgentConfig } from "@/features/agents/useGlobalAgentConfig"; import { useChannelsQuery } from "@/features/channels/hooks"; import { invalidateChannelMembersRosters } from "@/features/channels/rosterFreshness"; +import { useCommunities } from "@/features/communities/useCommunities"; import { usePresenceQuery } from "@/features/presence/hooks"; import type { AgentPersona, Channel, ManagedAgent } from "@/shared/api/types"; import { removeChannelMember } from "@/shared/api/tauri"; @@ -38,6 +39,7 @@ import { export function useManagedAgentActions() { const queryClient = useQueryClient(); const { globalConfig } = useGlobalAgentConfig(); + const { activeCommunity } = useCommunities(); const relayAgentsQuery = useRelayAgentsQuery(); const managedAgentsQuery = useManagedAgentsQuery(); const [shouldLoadChannels, setShouldLoadChannels] = React.useState(false); @@ -166,7 +168,12 @@ export function useManagedAgentActions() { if (!agent) return; await startManagedAgentWithRules({ agent, - startManagedAgent: startMutation.mutateAsync, + startManagedAgent: (agentPubkey) => + startMutation.mutateAsync({ + pubkey: agentPubkey, + expectedRelayUrl: activeCommunity?.relayUrl, + expectedSignerPubkey: activeCommunity?.pubkey, + }), }); } catch (error) { setActionErrorMessage( @@ -186,7 +193,12 @@ export function useManagedAgentActions() { if (!agent) return; await respawnManagedAgentWithRules({ agent, - startManagedAgent: startMutation.mutateAsync, + startManagedAgent: (agentPubkey) => + startMutation.mutateAsync({ + pubkey: agentPubkey, + expectedRelayUrl: activeCommunity?.relayUrl, + expectedSignerPubkey: activeCommunity?.pubkey, + }), stopManagedAgent: stopMutation.mutateAsync, onStopped: () => clearActiveTurnsForAgentOnStop(agent.pubkey), });