diff --git a/desktop/src/features/agents/lib/agentAutocompleteEligibility.test.mjs b/desktop/src/features/agents/lib/agentAutocompleteEligibility.test.mjs index 4e02b7bd681..ae1290dd9fb 100644 --- a/desktop/src/features/agents/lib/agentAutocompleteEligibility.test.mjs +++ b/desktop/src/features/agents/lib/agentAutocompleteEligibility.test.mjs @@ -5,7 +5,7 @@ import { coalesceAgentAutocompleteCandidates, getMentionableAgentPubkeys, getSharedChannelIds, - isAgentIdentityInManagedList, + isKnownLiveAgentIdentity, relayAgentIsSharedWithUser, shouldHideAgentFromMentions, } from "./agentAutocompleteEligibility.ts"; @@ -136,27 +136,44 @@ test("getMentionableAgentPubkeys: keeps managed agents and shared relay agents", assert.deepEqual(result, new Set([PUB_A, PUB_B, PUB_C])); }); -test("isAgentIdentityInManagedList: keeps people and only current managed agent identities", () => { +test("isKnownLiveAgentIdentity: keeps people, managed agents, and directory-listed foreign agents", () => { const managedAgentPubkeys = new Set([PUB_A]); + const directoryAgentPubkeys = new Set([PUB_C]); + // People always pass. assert.equal( - isAgentIdentityInManagedList( + isKnownLiveAgentIdentity( { isAgent: false, pubkey: PUB_B }, managedAgentPubkeys, + directoryAgentPubkeys, ), true, ); + // Locally managed agent passes (pubkey normalization included). assert.equal( - isAgentIdentityInManagedList( + isKnownLiveAgentIdentity( { isAgent: true, pubkey: PUB_A.toUpperCase() }, managedAgentPubkeys, + directoryAgentPubkeys, ), true, ); + // Foreign agent with a live relay directory entry (kind:10100) passes — + // e.g. an agent managed by another desktop that is a channel member. assert.equal( - isAgentIdentityInManagedList( + isKnownLiveAgentIdentity( + { isAgent: true, pubkey: PUB_C }, + managedAgentPubkeys, + directoryAgentPubkeys, + ), + true, + ); + // Stale agent identity — neither managed nor in the directory — is dropped. + assert.equal( + isKnownLiveAgentIdentity( { isAgent: true, pubkey: PUB_B }, managedAgentPubkeys, + directoryAgentPubkeys, ), false, ); diff --git a/desktop/src/features/agents/lib/agentAutocompleteEligibility.ts b/desktop/src/features/agents/lib/agentAutocompleteEligibility.ts index e4afe7fea4a..c7a452c1120 100644 --- a/desktop/src/features/agents/lib/agentAutocompleteEligibility.ts +++ b/desktop/src/features/agents/lib/agentAutocompleteEligibility.ts @@ -54,13 +54,26 @@ export function getMentionableAgentPubkeys({ return pubkeys; } -export function isAgentIdentityInManagedList( +/** + * A concrete agent identity is kept in autocomplete only when there is live + * evidence for it: the current user's managed Agents list, or a relay agent + * directory entry (kind:10100) published by its owning desktop. Stale + * identities — old p-tag/channel-member keys with no directory entry — are + * dropped. People are always kept. Whether a surviving foreign agent is + * ultimately shown is decided by `shouldHideAgentFromMentions`, which hides + * explicitly non-invocable ones. + */ +export function isKnownLiveAgentIdentity( candidate: { isAgent?: boolean; pubkey: string }, managedAgentPubkeys: ReadonlySet, + directoryAgentPubkeys: ReadonlySet, ) { + if (candidate.isAgent !== true) { + return true; + } + const normalized = normalizePubkey(candidate.pubkey); return ( - candidate.isAgent !== true || - managedAgentPubkeys.has(normalizePubkey(candidate.pubkey)) + managedAgentPubkeys.has(normalized) || directoryAgentPubkeys.has(normalized) ); } diff --git a/desktop/src/features/channels/ui/MembersSidebar.tsx b/desktop/src/features/channels/ui/MembersSidebar.tsx index c6349546a23..68aeff65c98 100644 --- a/desktop/src/features/channels/ui/MembersSidebar.tsx +++ b/desktop/src/features/channels/ui/MembersSidebar.tsx @@ -9,7 +9,7 @@ import { import { attachManagedAgentToChannel } from "@/features/agents/channelAgents"; import { coalesceAgentAutocompleteCandidates, - isAgentIdentityInManagedList, + isKnownLiveAgentIdentity, } from "@/features/agents/lib/agentAutocompleteEligibility"; import { useIsArchivedPredicate } from "@/features/identity-archive/hooks"; import { useClassifiedMembers } from "@/features/channels/lib/useClassifiedMembers"; @@ -272,6 +272,11 @@ export function MembersSidebar({ .filter((label): label is string => Boolean(label)), ); const managedAgentPubkeys = new Set(managedAgentsByPubkey.keys()); + const directoryAgentPubkeys = new Set( + (relayAgentsQuery.data ?? []).map((agent) => + normalizePubkey(agent.pubkey), + ), + ); const addCandidate = (candidate: AddMemberSearchCandidate) => { const pubkey = normalizePubkey(candidate.pubkey); @@ -282,7 +287,11 @@ export function MembersSidebar({ )) || memberPubkeys.has(pubkey) || isArchivedDiscovery(pubkey) || - !isAgentIdentityInManagedList(candidate, managedAgentPubkeys) + !isKnownLiveAgentIdentity( + candidate, + managedAgentPubkeys, + directoryAgentPubkeys, + ) ) { return; } diff --git a/desktop/src/features/messages/lib/useMentions.ts b/desktop/src/features/messages/lib/useMentions.ts index 0c73b753390..ec98b7f378a 100644 --- a/desktop/src/features/messages/lib/useMentions.ts +++ b/desktop/src/features/messages/lib/useMentions.ts @@ -16,7 +16,7 @@ import { coalesceAutocompleteCandidatesByKey, getMentionableAgentPubkeys, getSharedChannelIds, - isAgentIdentityInManagedList, + isKnownLiveAgentIdentity, shouldHideAgentFromMentions, } from "@/features/agents/lib/agentAutocompleteEligibility"; import { @@ -246,7 +246,13 @@ export function useMentions( if (isArchivedDiscovery(pubkey)) { return; } - if (!isAgentIdentityInManagedList(candidate, managedAgentPubkeys)) { + if ( + !isKnownLiveAgentIdentity( + candidate, + managedAgentPubkeys, + directoryAgentPubkeys, + ) + ) { return; } if (