Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
coalesceAgentAutocompleteCandidates,
getMentionableAgentPubkeys,
getSharedChannelIds,
isAgentMentionable,
isAgentIdentityInManagedList,
relayAgentIsSharedWithUser,
shouldHideAgentFromMentions,
Expand Down Expand Up @@ -162,6 +163,39 @@ test("isAgentIdentityInManagedList: keeps people and only current managed agent
);
});

test("isAgentMentionable: keeps people, managed agents, and invocable relay agents", () => {
const mentionableAgentPubkeys = new Set([PUB_A, PUB_B]);

assert.equal(
isAgentMentionable(
{ isAgent: false, pubkey: PUB_C },
mentionableAgentPubkeys,
),
true,
);
assert.equal(
isAgentMentionable(
{ isAgent: true, pubkey: PUB_A.toUpperCase() },
mentionableAgentPubkeys,
),
true,
);
assert.equal(
isAgentMentionable(
{ isAgent: true, pubkey: PUB_B.toUpperCase() },
mentionableAgentPubkeys,
),
true,
);
assert.equal(
isAgentMentionable(
{ isAgent: true, pubkey: PUB_C },
mentionableAgentPubkeys,
),
false,
);
});

test("shouldHideAgentFromMentions: never hides non-agents", () => {
assert.equal(
shouldHideAgentFromMentions({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,14 @@ export function isAgentIdentityInManagedList(
);
}

export function isAgentMentionable(
candidate: { isAgent?: boolean; pubkey: string },
mentionableAgentPubkeys: ReadonlySet<string>,
) {
const pubkey = normalizePubkey(candidate.pubkey);
return candidate.isAgent !== true || mentionableAgentPubkeys.has(pubkey);
}

export function shouldHideAgentFromMentions({
isAgent,
isMember,
Expand Down
5 changes: 2 additions & 3 deletions desktop/src/features/messages/lib/useMentions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
coalesceAutocompleteCandidatesByKey,
getMentionableAgentPubkeys,
getSharedChannelIds,
isAgentIdentityInManagedList,
isAgentMentionable,
shouldHideAgentFromMentions,
} from "@/features/agents/lib/agentAutocompleteEligibility";
import {
Expand Down Expand Up @@ -246,7 +246,7 @@ export function useMentions(
if (isArchivedDiscovery(pubkey)) {
return;
}
if (!isAgentIdentityInManagedList(candidate, managedAgentPubkeys)) {
if (!isAgentMentionable(candidate, mentionableAgentPubkeys)) {
return;
}
if (
Expand Down Expand Up @@ -420,7 +420,6 @@ export function useMentions(
managedAgentNamesByPubkey,
managedAgentPersonaIds,
managedAgentPersonaIdsByPubkey,
managedAgentPubkeys,
managedAgentsQuery.data,
memberPubkeys,
members,
Expand Down
71 changes: 69 additions & 2 deletions desktop/tests/e2e/mentions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -826,7 +826,7 @@ test("managed relay agents are visible in channel mentions regardless of relay p
await expect(dropdown.getByText("agent")).toBeVisible();
});

test("relay-only agents stay hidden from channel mentions even when allowlisted", async ({
test("allowlisted relay-only agents can be mentioned and receive a p tag", async ({
page,
}) => {
await installMockBridge(page, {
Expand All @@ -836,17 +836,84 @@ test("relay-only agents stay hidden from channel mentions even when allowlisted"
name: "quinn",
respondTo: "allowlist",
respondToAllowlist: [MOCK_VIEWER_PUBKEY],
channelNames: ["general"],
},
],
});
await page.goto("/");

const channelId = await page
.getByTestId("channel-general")
.getAttribute("data-channel-id");
if (!channelId) {
throw new Error("General channel id missing.");
}
await page.evaluate(
async ({ agentPubkey, generalChannelId }) => {
const bridge = window as Window & {
__BUZZ_E2E_INVOKE_MOCK_COMMAND__?: (
command: string,
payload?: Record<string, unknown>,
) => Promise<unknown>;
__BUZZ_E2E_QUERY_CLIENT__?: {
invalidateQueries: () => Promise<void>;
};
};
const invoke = bridge.__BUZZ_E2E_INVOKE_MOCK_COMMAND__;
if (!invoke) {
throw new Error("Mock bridge is not installed.");
}
await invoke("add_channel_members", {
channelId: generalChannelId,
pubkeys: [agentPubkey],
role: "bot",
});
await bridge.__BUZZ_E2E_QUERY_CLIENT__?.invalidateQueries();
},
{
agentPubkey: ALLOWLIST_RELAY_AGENT_PUBKEY,
generalChannelId: channelId,
},
);

await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");

const signedEventCount = await page.evaluate(
() => window.__BUZZ_E2E_SIGNED_EVENTS__?.length ?? 0,
);
const input = page.getByTestId("message-input");
await input.fill("@quinn");

await expect(autocomplete(page)).toHaveCount(0);
const dropdown = autocomplete(page);
await expect(dropdown.getByText("quinn")).toBeVisible();
await input.press("Enter");
await page.keyboard.type(" can you help?");
await page.getByTestId("send-message").click();

await expect
.poll(() =>
page.evaluate(
({ agentPubkey, baselineCount }) => {
const event = window.__BUZZ_E2E_SIGNED_EVENTS__
?.slice(baselineCount)
.find(
(candidate) =>
candidate.kind === 9 &&
candidate.tags.some(
([tagName, pubkey]) =>
tagName === "p" && pubkey === agentPubkey,
),
);
return event?.tags ?? [];
},
{
agentPubkey: ALLOWLIST_RELAY_AGENT_PUBKEY,
baselineCount: signedEventCount,
},
),
)
.toContainEqual(["p", ALLOWLIST_RELAY_AGENT_PUBKEY]);
});

test("mentioning an in-channel stopped managed agent starts it before sending", async ({
Expand Down