diff --git a/desktop/src/features/agents/lib/agentAutocompleteEligibility.test.mjs b/desktop/src/features/agents/lib/agentAutocompleteEligibility.test.mjs index 4e02b7bd68..7ccab6091a 100644 --- a/desktop/src/features/agents/lib/agentAutocompleteEligibility.test.mjs +++ b/desktop/src/features/agents/lib/agentAutocompleteEligibility.test.mjs @@ -7,6 +7,7 @@ import { getSharedChannelIds, isAgentIdentityInManagedList, relayAgentIsSharedWithUser, + shouldDropAgentMentionCandidate, shouldHideAgentFromMentions, } from "./agentAutocompleteEligibility.ts"; @@ -162,6 +163,44 @@ test("isAgentIdentityInManagedList: keeps people and only current managed agent ); }); +test("shouldDropAgentMentionCandidate: keeps channel-member agents from other owners (#2508)", () => { + const managedAgentPubkeys = new Set([PUB_A]); + + // A channel-member agent managed on someone else's machine must stay + // mentionable for every member of the channel. + assert.equal( + shouldDropAgentMentionCandidate( + { isAgent: true, isMember: true, pubkey: PUB_B }, + managedAgentPubkeys, + ), + false, + ); + // Locally managed agents stay mentionable even when not channel members. + assert.equal( + shouldDropAgentMentionCandidate( + { isAgent: true, isMember: false, pubkey: PUB_A }, + managedAgentPubkeys, + ), + false, + ); + // People are never dropped. + assert.equal( + shouldDropAgentMentionCandidate( + { isAgent: false, isMember: false, pubkey: PUB_B }, + managedAgentPubkeys, + ), + false, + ); + // Non-member agent identities from other owners remain restricted (#2149). + assert.equal( + shouldDropAgentMentionCandidate( + { isAgent: true, isMember: false, pubkey: PUB_B }, + managedAgentPubkeys, + ), + true, + ); +}); + test("shouldHideAgentFromMentions: never hides non-agents", () => { assert.equal( shouldHideAgentFromMentions({ diff --git a/desktop/src/features/agents/lib/agentAutocompleteEligibility.ts b/desktop/src/features/agents/lib/agentAutocompleteEligibility.ts index e4afe7fea4..ff2e0963d6 100644 --- a/desktop/src/features/agents/lib/agentAutocompleteEligibility.ts +++ b/desktop/src/features/agents/lib/agentAutocompleteEligibility.ts @@ -64,6 +64,25 @@ export function isAgentIdentityInManagedList( ); } +/** + * Composer drop gate for agent identities (#2508). + * + * A channel-member agent is never dropped: an agent that has been added to + * the channel must stay mentionable for every member, not only the person + * whose desktop manages it. The managed-list restriction (#2149) still + * applies to non-member agent identities (stale kind:0 discoveries, global + * search strays), which is the duplicate-noise case it was introduced for. + */ +export function shouldDropAgentMentionCandidate( + candidate: { isAgent?: boolean; isMember?: boolean; pubkey: string }, + managedAgentPubkeys: ReadonlySet, +) { + return ( + candidate.isMember !== true && + !isAgentIdentityInManagedList(candidate, managedAgentPubkeys) + ); +} + export function shouldHideAgentFromMentions({ isAgent, isMember, diff --git a/desktop/src/features/messages/lib/useMentions.ts b/desktop/src/features/messages/lib/useMentions.ts index 0c73b75339..6b4f3c4efe 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, + shouldDropAgentMentionCandidate, shouldHideAgentFromMentions, } from "@/features/agents/lib/agentAutocompleteEligibility"; import { @@ -246,7 +246,7 @@ export function useMentions( if (isArchivedDiscovery(pubkey)) { return; } - if (!isAgentIdentityInManagedList(candidate, managedAgentPubkeys)) { + if (shouldDropAgentMentionCandidate(candidate, managedAgentPubkeys)) { return; } if (