From 54a87ac268ab3a456c0a47c52cef8be8d5ead645 Mon Sep 17 00:00:00 2001 From: benclink <111811630+benclink@users.noreply.github.com> Date: Wed, 22 Jul 2026 15:01:54 +1000 Subject: [PATCH] Fix shared agent mentions Co-authored-by: benclink <111811630+benclink@users.noreply.github.com> Signed-off-by: benclink <111811630+benclink@users.noreply.github.com> --- .../lib/agentAutocompleteEligibility.test.mjs | 27 +++++++++++++++ .../lib/agentAutocompleteEligibility.ts | 20 +++++++++++ .../src/features/messages/lib/useMentions.ts | 4 +-- desktop/src/testing/e2eBridge.ts | 33 +++++++++++++++++++ desktop/tests/e2e/mentions.spec.ts | 26 +++++++++++++++ desktop/tests/helpers/bridge.ts | 2 ++ 6 files changed, 110 insertions(+), 2 deletions(-) diff --git a/desktop/src/features/agents/lib/agentAutocompleteEligibility.test.mjs b/desktop/src/features/agents/lib/agentAutocompleteEligibility.test.mjs index 4e02b7bd68..6a2684b43b 100644 --- a/desktop/src/features/agents/lib/agentAutocompleteEligibility.test.mjs +++ b/desktop/src/features/agents/lib/agentAutocompleteEligibility.test.mjs @@ -6,6 +6,7 @@ import { getMentionableAgentPubkeys, getSharedChannelIds, isAgentIdentityInManagedList, + isAgentIdentityMentionable, relayAgentIsSharedWithUser, shouldHideAgentFromMentions, } from "./agentAutocompleteEligibility.ts"; @@ -162,6 +163,32 @@ test("isAgentIdentityInManagedList: keeps people and only current managed agent ); }); +test("isAgentIdentityMentionable: admits in-channel bots owned elsewhere", () => { + const managedAgentPubkeys = new Set([PUB_A]); + + assert.equal( + isAgentIdentityMentionable( + { isAgent: true, isMember: true, pubkey: PUB_B, role: "bot" }, + managedAgentPubkeys, + ), + true, + ); + assert.equal( + isAgentIdentityMentionable( + { isAgent: true, isMember: true, pubkey: PUB_B, role: "member" }, + managedAgentPubkeys, + ), + false, + ); + assert.equal( + isAgentIdentityMentionable( + { isAgent: true, pubkey: PUB_B }, + managedAgentPubkeys, + ), + false, + ); +}); + 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..505bedfdbe 100644 --- a/desktop/src/features/agents/lib/agentAutocompleteEligibility.ts +++ b/desktop/src/features/agents/lib/agentAutocompleteEligibility.ts @@ -64,6 +64,26 @@ export function isAgentIdentityInManagedList( ); } +/** + * Mention autocomplete also admits bot-role channel members owned elsewhere. + * Their runtime remains the authority for the respond-to policy; this gate + * only allows the later relay-policy check to evaluate them. + */ +export function isAgentIdentityMentionable( + candidate: { + isAgent?: boolean; + isMember?: boolean; + pubkey: string; + role?: string | null; + }, + managedAgentPubkeys: ReadonlySet, +) { + return ( + isAgentIdentityInManagedList(candidate, managedAgentPubkeys) || + (candidate.isMember === true && candidate.role === "bot") + ); +} + 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..b7f735f371 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, + isAgentIdentityMentionable, shouldHideAgentFromMentions, } from "@/features/agents/lib/agentAutocompleteEligibility"; import { @@ -246,7 +246,7 @@ export function useMentions( if (isArchivedDiscovery(pubkey)) { return; } - if (!isAgentIdentityInManagedList(candidate, managedAgentPubkeys)) { + if (!isAgentIdentityMentionable(candidate, managedAgentPubkeys)) { return; } if ( diff --git a/desktop/src/testing/e2eBridge.ts b/desktop/src/testing/e2eBridge.ts index 1ea8998313..992be015e4 100644 --- a/desktop/src/testing/e2eBridge.ts +++ b/desktop/src/testing/e2eBridge.ts @@ -90,6 +90,7 @@ type MockRelayAgentSeed = { respondToAllowlist?: string[]; channelNames?: string[]; channelIds?: string[]; + memberChannelNames?: string[]; status?: PresenceStatus; }; @@ -1962,6 +1963,38 @@ function resetMockRelayAgents(config?: E2eConfig) { respond_to: seed.respondTo ?? "owner-only", respond_to_allowlist: seed.respondToAllowlist ?? [], }); + + if (!seed.memberChannelNames?.length) { + continue; + } + applyMockDisplayName(seed.pubkey, seed.name); + mockAgentPubkeys.add(seed.pubkey); + mockProfiles.set(seed.pubkey, { + pubkey: seed.pubkey, + display_name: seed.name, + avatar_url: null, + about: null, + nip05_handle: null, + owner_pubkey: null, + is_agent: true, + has_profile_event: true, + }); + for (const channel of mockChannels) { + if ( + !seed.memberChannelNames?.includes(channel.name) || + channel.members.some((member) => member.pubkey === seed.pubkey) + ) { + continue; + } + channel.members.push({ + pubkey: seed.pubkey, + role: "bot", + is_agent: true, + joined_at: new Date().toISOString(), + display_name: seed.name, + }); + syncMockChannel(channel); + } } } diff --git a/desktop/tests/e2e/mentions.spec.ts b/desktop/tests/e2e/mentions.spec.ts index 694b5abef5..4e7b3e8f98 100644 --- a/desktop/tests/e2e/mentions.spec.ts +++ b/desktop/tests/e2e/mentions.spec.ts @@ -826,6 +826,32 @@ test("managed relay agents are visible in channel mentions regardless of relay p await expect(dropdown.getByText("agent")).toBeVisible(); }); +test("shared in-channel bot agents are visible to non-owner members", async ({ + page, +}) => { + await installMockBridge(page, { + relayAgents: [ + { + pubkey: ALLOWLIST_RELAY_AGENT_PUBKEY, + name: "Tech Goose", + respondTo: "anyone", + channelNames: ["general"], + memberChannelNames: ["general"], + }, + ], + }); + await page.goto("/"); + await page.getByTestId("channel-general").click(); + await expect(page.getByTestId("chat-title")).toHaveText("general"); + + const input = page.getByTestId("message-input"); + await input.fill("@Tech"); + + const dropdown = autocomplete(page); + await expect(dropdown.getByText("Tech Goose")).toBeVisible(); + await expect(dropdown.getByText("agent")).toBeVisible(); +}); + test("relay-only agents stay hidden from channel mentions even when allowlisted", async ({ page, }) => { diff --git a/desktop/tests/helpers/bridge.ts b/desktop/tests/helpers/bridge.ts index 8273e697a4..873306fbe7 100644 --- a/desktop/tests/helpers/bridge.ts +++ b/desktop/tests/helpers/bridge.ts @@ -79,6 +79,8 @@ type MockRelayAgentSeed = { respondToAllowlist?: string[]; channelNames?: string[]; channelIds?: string[]; + /** Also seed this relay-owned agent as a bot member of the named channels. */ + memberChannelNames?: string[]; status?: "online" | "away" | "offline"; };