Hide non-invocable member agents from @-mention autocomplete (#1611)

This commit is contained in:
Atish Patel
2026-07-08 12:57:33 -04:00
committed by GitHub
parent fa816d5bfa
commit f929aa09e6
3 changed files with 133 additions and 3 deletions
@@ -6,6 +6,7 @@ import {
getMentionableAgentPubkeys,
getSharedChannelIds,
relayAgentIsSharedWithUser,
shouldHideAgentFromMentions,
} from "./agentAutocompleteEligibility.ts";
const CURRENT_PUBKEY = "a".repeat(64);
@@ -134,6 +135,87 @@ test("getMentionableAgentPubkeys: keeps managed agents and shared relay agents",
assert.deepEqual(result, new Set([PUB_A, PUB_B, PUB_C]));
});
test("shouldHideAgentFromMentions: never hides non-agents", () => {
assert.equal(
shouldHideAgentFromMentions({
isAgent: false,
isMember: false,
pubkey: PUB_A,
mentionableAgentPubkeys: new Set(),
directoryAgentPubkeys: new Set([PUB_A]),
}),
false,
);
});
test("shouldHideAgentFromMentions: shows invocable agents even when non-member", () => {
assert.equal(
shouldHideAgentFromMentions({
isAgent: true,
isMember: false,
pubkey: PUB_A,
mentionableAgentPubkeys: new Set([PUB_A]),
directoryAgentPubkeys: new Set([PUB_A]),
}),
false,
);
});
test("shouldHideAgentFromMentions: hides non-member non-invocable agents", () => {
assert.equal(
shouldHideAgentFromMentions({
isAgent: true,
isMember: false,
pubkey: PUB_A,
mentionableAgentPubkeys: new Set(),
directoryAgentPubkeys: new Set(),
}),
true,
);
});
test("shouldHideAgentFromMentions: hides member agents with an explicit not-invocable directory entry (Fizz)", () => {
assert.equal(
shouldHideAgentFromMentions({
isAgent: true,
isMember: true,
pubkey: PUB_A,
mentionableAgentPubkeys: new Set(),
directoryAgentPubkeys: new Set([PUB_A]),
}),
true,
);
});
test("shouldHideAgentFromMentions: shows member agents with unknown invocability (not in directory)", () => {
assert.equal(
shouldHideAgentFromMentions({
isAgent: true,
isMember: true,
pubkey: PUB_A,
mentionableAgentPubkeys: new Set(),
directoryAgentPubkeys: new Set(),
}),
false,
);
});
test("shouldHideAgentFromMentions: normalizes the pubkey before lookup", () => {
const mixedCase = "Ab".repeat(32);
const normalized = mixedCase.toLowerCase();
assert.equal(
shouldHideAgentFromMentions({
isAgent: true,
isMember: true,
pubkey: mixedCase,
mentionableAgentPubkeys: new Set(),
directoryAgentPubkeys: new Set([normalized]),
}),
true,
);
});
test("coalesceAgentAutocompleteCandidates: merges agents with the same persona id", () => {
const first = makeAgent({ pubkey: PUB_A, personaId: "pinky" });
const second = makeAgent({
@@ -54,6 +54,39 @@ export function getMentionableAgentPubkeys({
return pubkeys;
}
export function shouldHideAgentFromMentions({
isAgent,
isMember,
pubkey,
mentionableAgentPubkeys,
directoryAgentPubkeys,
}: {
isAgent: boolean;
isMember: boolean;
pubkey: string;
mentionableAgentPubkeys: ReadonlySet<string>;
directoryAgentPubkeys: ReadonlySet<string>;
}) {
if (!isAgent) return false;
const normalized = normalizePubkey(pubkey);
// Invocable => always show.
if (mentionableAgentPubkeys.has(normalized)) return false;
// Non-member, non-invocable => hide (preserves prior behavior).
if (!isMember) return true;
// Member (Option B): hide only when we have an explicit not-invocable
// signal — a relay directory (kind:10100) entry that excludes us.
// Unknown invocability (not in directory) => show.
//
// NOTE: this assumes `directoryAgentPubkeys` and `mentionableAgentPubkeys`
// share the same source query (`relayAgentsQuery.data`), so directory
// presence without membership in `mentionableAgentPubkeys` is a real
// explicit-exclusion signal. If a future change sources the directory set
// from a different query, an agent that's directory-present but whose
// mentionability is still loading could be hidden prematurely — keep the
// two sets derived from the same query.
return directoryAgentPubkeys.has(normalized);
}
type AgentAutocompleteCandidate = {
pubkey?: string;
displayName?: string | null;
@@ -16,6 +16,7 @@ import {
coalesceAutocompleteCandidatesByKey,
getMentionableAgentPubkeys,
getSharedChannelIds,
shouldHideAgentFromMentions,
} from "@/features/agents/lib/agentAutocompleteEligibility";
import {
useInfiniteUserSearchQuery,
@@ -209,6 +210,15 @@ export function useMentions(
),
[relayAgentsQuery.data],
);
const directoryAgentPubkeys = React.useMemo(
() =>
new Set(
(relayAgentsQuery.data ?? []).map((agent) =>
normalizePubkey(agent.pubkey),
),
),
[relayAgentsQuery.data],
);
const sharedChannelIds = React.useMemo(
() => getSharedChannelIds(channelsQuery.data),
[channelsQuery.data],
@@ -269,9 +279,13 @@ export function useMentions(
return;
}
if (
candidate.isAgent &&
!candidate.isMember &&
!mentionableAgentPubkeys.has(pubkey)
shouldHideAgentFromMentions({
isAgent: candidate.isAgent === true,
isMember: candidate.isMember === true,
pubkey,
mentionableAgentPubkeys,
directoryAgentPubkeys,
})
) {
return;
}
@@ -425,6 +439,7 @@ export function useMentions(
userSearchResults,
canSearchGlobalUsers,
currentPubkey,
directoryAgentPubkeys,
isArchivedDiscovery,
managedAgentNamesByPubkey,
managedAgentPersonaIds,