fix(desktop): prefer live agent mentions (#2149)

Signed-off-by: npub1rf6fvdj6ut0c4kcmjv4p5mmgh89nj58n69uu3fz3cvk3jn500hqs7emz79 <1a7496365ae2df8adb1b932a1a6f68b9cb3950f3d179c8a451c32d194e8f7dc1@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1rf6fvdj6ut0c4kcmjv4p5mmgh89nj58n69uu3fz3cvk3jn500hqs7emz79 <1a7496365ae2df8adb1b932a1a6f68b9cb3950f3d179c8a451c32d194e8f7dc1@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
Bradley Axen
2026-07-20 08:58:48 -07:00
committed by GitHub
parent 9fb1a90b66
commit a4dfead21a
9 changed files with 187 additions and 64 deletions

View File

@@ -5,6 +5,7 @@ import {
coalesceAgentAutocompleteCandidates, coalesceAgentAutocompleteCandidates,
getMentionableAgentPubkeys, getMentionableAgentPubkeys,
getSharedChannelIds, getSharedChannelIds,
isAgentIdentityInManagedList,
relayAgentIsSharedWithUser, relayAgentIsSharedWithUser,
shouldHideAgentFromMentions, shouldHideAgentFromMentions,
} from "./agentAutocompleteEligibility.ts"; } from "./agentAutocompleteEligibility.ts";
@@ -135,6 +136,32 @@ test("getMentionableAgentPubkeys: keeps managed agents and shared relay agents",
assert.deepEqual(result, new Set([PUB_A, PUB_B, PUB_C])); assert.deepEqual(result, new Set([PUB_A, PUB_B, PUB_C]));
}); });
test("isAgentIdentityInManagedList: keeps people and only current managed agent identities", () => {
const managedAgentPubkeys = new Set([PUB_A]);
assert.equal(
isAgentIdentityInManagedList(
{ isAgent: false, pubkey: PUB_B },
managedAgentPubkeys,
),
true,
);
assert.equal(
isAgentIdentityInManagedList(
{ isAgent: true, pubkey: PUB_A.toUpperCase() },
managedAgentPubkeys,
),
true,
);
assert.equal(
isAgentIdentityInManagedList(
{ isAgent: true, pubkey: PUB_B },
managedAgentPubkeys,
),
false,
);
});
test("shouldHideAgentFromMentions: never hides non-agents", () => { test("shouldHideAgentFromMentions: never hides non-agents", () => {
assert.equal( assert.equal(
shouldHideAgentFromMentions({ shouldHideAgentFromMentions({

View File

@@ -54,6 +54,16 @@ export function getMentionableAgentPubkeys({
return pubkeys; return pubkeys;
} }
export function isAgentIdentityInManagedList(
candidate: { isAgent?: boolean; pubkey: string },
managedAgentPubkeys: ReadonlySet<string>,
) {
return (
candidate.isAgent !== true ||
managedAgentPubkeys.has(normalizePubkey(candidate.pubkey))
);
}
export function shouldHideAgentFromMentions({ export function shouldHideAgentFromMentions({
isAgent, isAgent,
isMember, isMember,

View File

@@ -4,12 +4,10 @@ import { Bot, UserRoundPlus, X } from "lucide-react";
import { import {
useAddChannelMembersMutation, useAddChannelMembersMutation,
useChannelMembersQuery, useChannelMembersQuery,
useChannelsQuery,
} from "@/features/channels/hooks"; } from "@/features/channels/hooks";
import { import {
coalesceAgentAutocompleteCandidates, coalesceAgentAutocompleteCandidates,
getMentionableAgentPubkeys, isAgentIdentityInManagedList,
getSharedChannelIds,
} from "@/features/agents/lib/agentAutocompleteEligibility"; } from "@/features/agents/lib/agentAutocompleteEligibility";
import { useIsArchivedPredicate } from "@/features/identity-archive/hooks"; import { useIsArchivedPredicate } from "@/features/identity-archive/hooks";
import { useClassifiedMembers } from "@/features/channels/lib/useClassifiedMembers"; import { useClassifiedMembers } from "@/features/channels/lib/useClassifiedMembers";
@@ -146,7 +144,6 @@ export function MembersSidebar({
const identityQuery = useIdentityQuery(); const identityQuery = useIdentityQuery();
const membersQuery = useChannelMembersQuery(channelId, open); const membersQuery = useChannelMembersQuery(channelId, open);
const addMembersMutation = useAddChannelMembersMutation(channelId); const addMembersMutation = useAddChannelMembersMutation(channelId);
const channelsQuery = useChannelsQuery({ enabled: open });
const changeRoleMutation = useMutation({ const changeRoleMutation = useMutation({
mutationFn: async ({ pubkey, role }: { pubkey: string; role: string }) => { mutationFn: async ({ pubkey, role }: { pubkey: string; role: string }) => {
if (!channelId) throw new Error("No channel selected."); if (!channelId) throw new Error("No channel selected.");
@@ -262,15 +259,7 @@ export function MembersSidebar({
.map((member) => member.displayName?.trim().toLowerCase()) .map((member) => member.displayName?.trim().toLowerCase())
.filter((label): label is string => Boolean(label)), .filter((label): label is string => Boolean(label)),
); );
const sharedChannelIds = getSharedChannelIds(channelsQuery.data); const managedAgentPubkeys = new Set(managedAgentsByPubkey.keys());
const eligibleAgentPubkeys = getMentionableAgentPubkeys({
currentPubkey,
managedAgentPubkeys: (managedAgentsQuery.data ?? []).map(
(agent) => agent.pubkey,
),
relayAgents: relayAgentsQuery.data,
sharedChannelIds,
});
const addCandidate = (candidate: AddMemberSearchCandidate) => { const addCandidate = (candidate: AddMemberSearchCandidate) => {
const pubkey = normalizePubkey(candidate.pubkey); const pubkey = normalizePubkey(candidate.pubkey);
@@ -281,7 +270,7 @@ export function MembersSidebar({
)) || )) ||
memberPubkeys.has(pubkey) || memberPubkeys.has(pubkey) ||
isArchivedDiscovery(pubkey) || isArchivedDiscovery(pubkey) ||
(candidate.isAgent && !eligibleAgentPubkeys.has(pubkey)) !isAgentIdentityInManagedList(candidate, managedAgentPubkeys)
) { ) {
return; return;
} }
@@ -320,10 +309,6 @@ export function MembersSidebar({
} }
for (const agent of relayAgentsQuery.data ?? []) { for (const agent of relayAgentsQuery.data ?? []) {
if (!eligibleAgentPubkeys.has(normalizePubkey(agent.pubkey))) {
continue;
}
addCandidate({ addCandidate({
pubkey: agent.pubkey, pubkey: agent.pubkey,
displayName: agent.name, displayName: agent.name,
@@ -365,7 +350,6 @@ export function MembersSidebar({
}, [ }, [
canAddMembers, canAddMembers,
isArchivedDiscovery, isArchivedDiscovery,
channelsQuery.data,
currentPubkey, currentPubkey,
managedAgentsQuery.data, managedAgentsQuery.data,
memberPubkeys, memberPubkeys,
@@ -377,8 +361,7 @@ export function MembersSidebar({
const isAddSearchLoading = const isAddSearchLoading =
userSearchQuery.isLoading || userSearchQuery.isLoading ||
managedAgentsQuery.isLoading || managedAgentsQuery.isLoading ||
relayAgentsQuery.isLoading || relayAgentsQuery.isLoading;
channelsQuery.isLoading;
const handlePeopleSearchScroll = useUserSearchFetchMoreOnScroll( const handlePeopleSearchScroll = useUserSearchFetchMoreOnScroll(
userSearchQuery, userSearchQuery,
canAddMembers && normalizedDeferredSearchQuery.length > 0, canAddMembers && normalizedDeferredSearchQuery.length > 0,

View File

@@ -16,6 +16,7 @@ import {
coalesceAutocompleteCandidatesByKey, coalesceAutocompleteCandidatesByKey,
getMentionableAgentPubkeys, getMentionableAgentPubkeys,
getSharedChannelIds, getSharedChannelIds,
isAgentIdentityInManagedList,
shouldHideAgentFromMentions, shouldHideAgentFromMentions,
} from "@/features/agents/lib/agentAutocompleteEligibility"; } from "@/features/agents/lib/agentAutocompleteEligibility";
import { import {
@@ -245,6 +246,9 @@ export function useMentions(
if (isArchivedDiscovery(pubkey)) { if (isArchivedDiscovery(pubkey)) {
return; return;
} }
if (!isAgentIdentityInManagedList(candidate, managedAgentPubkeys)) {
return;
}
if ( if (
shouldHideAgentFromMentions({ shouldHideAgentFromMentions({
isAgent: candidate.isAgent === true, isAgent: candidate.isAgent === true,
@@ -256,7 +260,6 @@ export function useMentions(
) { ) {
return; return;
} }
const current = candidatesByPubkey.get(pubkey); const current = candidatesByPubkey.get(pubkey);
if (!current) { if (!current) {
candidatesByPubkey.set(pubkey, { ...candidate, pubkey }); candidatesByPubkey.set(pubkey, { ...candidate, pubkey });
@@ -289,7 +292,6 @@ export function useMentions(
isManagedAgent: current.isManagedAgent || candidate.isManagedAgent, isManagedAgent: current.isManagedAgent || candidate.isManagedAgent,
}); });
}; };
for (const member of members ?? []) { for (const member of members ?? []) {
const pubkey = normalizePubkey(member.pubkey); const pubkey = normalizePubkey(member.pubkey);
const linkedPersonaId = activePersonaById.has(pubkey) const linkedPersonaId = activePersonaById.has(pubkey)
@@ -418,6 +420,7 @@ export function useMentions(
managedAgentNamesByPubkey, managedAgentNamesByPubkey,
managedAgentPersonaIds, managedAgentPersonaIds,
managedAgentPersonaIdsByPubkey, managedAgentPersonaIdsByPubkey,
managedAgentPubkeys,
managedAgentsQuery.data, managedAgentsQuery.data,
memberPubkeys, memberPubkeys,
members, members,
@@ -540,10 +543,7 @@ export function useMentions(
mentionQuery, mentionQuery,
activePersonaIds, activePersonaIds,
) )
.slice( .slice(0, MENTION_SUGGESTION_LIMIT)
0,
Math.max(MENTION_SUGGESTION_LIMIT, mentionCandidatesWithTeams.length),
)
.map(({ candidate, label }) => .map(({ candidate, label }) =>
mapMentionCandidateToSuggestion({ mapMentionCandidateToSuggestion({
candidate, candidate,

View File

@@ -812,10 +812,17 @@ test("routes an agent mention from an existing DM to the expanded conversation",
); );
}); });
test("routes a relay-agent mention from an existing DM to the expanded conversation", async ({ test("routes a managed relay-agent mention from an existing DM to the expanded conversation", async ({
page, page,
}) => { }) => {
await installMockBridge(page, { await installMockBridge(page, {
managedAgents: [
{
pubkey: DM_RELAY_AGENT_PUBKEY,
name: "quinn",
status: "stopped",
},
],
relayAgents: [ relayAgents: [
{ {
pubkey: DM_RELAY_AGENT_PUBKEY, pubkey: DM_RELAY_AGENT_PUBKEY,
@@ -856,15 +863,13 @@ test("routes a relay-agent mention from an existing DM to the expanded conversat
page.locator("[data-active='true'][data-channel-id]"), page.locator("[data-active='true'][data-channel-id]"),
).toHaveAttribute("data-channel-id", sentChannelId ?? ""); ).toHaveAttribute("data-channel-id", sentChannelId ?? "");
await expect(page.getByTestId("chat-title")).toContainText("alice"); await expect(page.getByTestId("chat-title")).toContainText("alice");
await expect(page.getByTestId("chat-title")).toContainText( await expect(page.getByTestId("chat-title")).toContainText("quinn");
DM_RELAY_AGENT_PUBKEY.slice(0, 8),
);
const sendCommands = (await readCommandPayloadLog(page)).slice( const sendCommands = (await readCommandPayloadLog(page)).slice(
baselineCommands.length, baselineCommands.length,
); );
expect(sendCommands.map((entry) => entry.command)).toContain("open_dm"); expect(sendCommands.map((entry) => entry.command)).toContain("open_dm");
expect(sendCommands.map((entry) => entry.command)).not.toContain( expect(sendCommands.map((entry) => entry.command)).toContain(
"start_managed_agent", "start_managed_agent",
); );
expect(sendCommands.map((entry) => entry.command)).not.toContain( expect(sendCommands.map((entry) => entry.command)).not.toContain(
@@ -2554,7 +2559,18 @@ test("home inbox manage affordance opens management without leaving home", async
await expect(page).not.toHaveURL(/#\/channels\//); await expect(page).not.toHaveURL(/#\/channels\//);
}); });
test("members sidebar can invite and remove members", async ({ page }) => { test("members sidebar can invite and remove managed agents", async ({
page,
}) => {
await installMockBridge(page, {
managedAgents: [
{
pubkey: TEST_IDENTITIES.charlie.pubkey,
name: "charlie",
status: "stopped",
},
],
});
await page.goto("/"); await page.goto("/");
await openMembersSidebar(page, "general"); await openMembersSidebar(page, "general");
const initialMemberCount = await readMembersTriggerCount(page); const initialMemberCount = await readMembersTriggerCount(page);
@@ -2847,9 +2863,18 @@ test("members sidebar collapses same-persona managed agents", async ({
await expect(page.getByText("Pinky", { exact: true })).toHaveCount(1); await expect(page.getByText("Pinky", { exact: true })).toHaveCount(1);
}); });
test("private-channel members can add members and bots without admin", async ({ test("private-channel members can add people and managed agents without admin", async ({
page, page,
}) => { }) => {
await installMockBridge(page, {
managedAgents: [
{
pubkey: TEST_IDENTITIES.charlie.pubkey,
name: "charlie",
status: "stopped",
},
],
});
await page.goto("/"); await page.goto("/");
// secret-projects is a private (non-DM) channel where the current user is a // secret-projects is a private (non-DM) channel where the current user is a
// plain member, not owner/admin. They should still be able to add members // plain member, not owner/admin. They should still be able to add members

View File

@@ -166,38 +166,38 @@ test.describe("NIP-IA hide archived from discovery", () => {
}); });
// Member-adder (ChannelMemberInviteCard) — the invite search excludes // Member-adder (ChannelMemberInviteCard) — the invite search excludes
// existing channel members, so we test in #agents where Alice is NOT a // existing channel members, so we test in #agents where Bob is NOT a
// member (only the mock identity + Charlie are): the ONLY thing that can // member (only the mock identity + Charlie are): the ONLY thing that can
// drop her from results is the archive filter. The control run (nothing // drop Bob from results is the archive filter. The control run (nothing
// archived) proves she would otherwise appear — guarding a vacuous green. // archived) proves he would otherwise appear — guarding a vacuous green.
test("member-adder: archived non-member is omitted from invite search", async ({ test("member-adder: archived non-member is omitted from invite search", async ({
page, page,
}) => { }) => {
await installMockBridge(page, { archivedIdentities: [ALICE_PUBKEY] }); await installMockBridge(page, { archivedIdentities: [BOB_PUBKEY] });
await page.goto("/"); await page.goto("/");
await page.getByTestId("channel-agents").click(); await page.getByTestId("channel-agents").click();
await page.getByTestId("channel-members-trigger").click(); await page.getByTestId("channel-members-trigger").click();
await expect(page.getByTestId("members-sidebar")).toBeVisible(); await expect(page.getByTestId("members-sidebar")).toBeVisible();
await page.getByTestId("channel-management-search-users").fill("alice"); await page.getByTestId("channel-management-search-users").fill("bob");
await expect( await expect(
page.getByTestId(`channel-user-search-result-${ALICE_PUBKEY}`), page.getByTestId(`channel-user-search-result-${BOB_PUBKEY}`),
).toHaveCount(0); ).toHaveCount(0);
}); });
test("member-adder: control — non-archived non-member IS in invite search", async ({ test("member-adder: control — non-archived non-member IS in invite search", async ({
page, page,
}) => { }) => {
// Same channel/query, nothing archived: Alice now appears. This is the // Same channel/query, nothing archived: Bob now appears. This is the
// companion that makes the test above non-vacuous (proves she was dropped // companion that makes the test above non-vacuous (proves he was dropped
// by the archive filter, not by member-exclusion or a broken search). // by the archive filter, not by member-exclusion or a broken search).
await installMockBridge(page, { archivedIdentities: [] }); await installMockBridge(page, { archivedIdentities: [] });
await page.goto("/"); await page.goto("/");
await page.getByTestId("channel-agents").click(); await page.getByTestId("channel-agents").click();
await page.getByTestId("channel-members-trigger").click(); await page.getByTestId("channel-members-trigger").click();
await expect(page.getByTestId("members-sidebar")).toBeVisible(); await expect(page.getByTestId("members-sidebar")).toBeVisible();
await page.getByTestId("channel-management-search-users").fill("alice"); await page.getByTestId("channel-management-search-users").fill("bob");
await expect( await expect(
page.getByTestId(`channel-user-search-result-${ALICE_PUBKEY}`), page.getByTestId(`channel-user-search-result-${BOB_PUBKEY}`),
).toBeVisible(); ).toBeVisible();
}); });
}); });

View File

@@ -187,11 +187,18 @@ async function expectAgentProfileActionsHidden(
).toHaveCount(0); ).toHaveCount(0);
} }
test("@ trigger prioritizes channel members before runnable personas and other agents", async ({ test("@ trigger prioritizes channel members before runnable personas and other managed agents", async ({
page, page,
}) => { }) => {
await installMockBridge(page, { await installMockBridge(page, {
activePersonaIds: ["builtin:fizz"], activePersonaIds: ["builtin:fizz"],
managedAgents: [
{
pubkey: TEST_IDENTITIES.charlie.pubkey,
name: "charlie",
status: "stopped",
},
],
}); });
await page.goto("/"); await page.goto("/");
await page.getByTestId("channel-general").click(); await page.getByTestId("channel-general").click();
@@ -202,7 +209,7 @@ test("@ trigger prioritizes channel members before runnable personas and other a
const dropdown = autocomplete(page); const dropdown = autocomplete(page);
await expect(dropdown).toBeVisible(); await expect(dropdown).toBeVisible();
await expect(dropdown.getByText("alice")).toBeVisible(); await expect(dropdown.getByText("alice")).toHaveCount(0);
await expect(dropdown.getByText("bob")).toBeVisible(); await expect(dropdown.getByText("bob")).toBeVisible();
await expect(dropdown.getByText("Fizz")).toBeVisible(); await expect(dropdown.getByText("Fizz")).toBeVisible();
await expect(dropdown.getByText("charlie")).toBeVisible(); await expect(dropdown.getByText("charlie")).toBeVisible();
@@ -219,7 +226,6 @@ test("@ trigger prioritizes channel members before runnable personas and other a
const suggestions = dropdown.locator("button"); const suggestions = dropdown.locator("button");
const suggestionText = await suggestions.allInnerTexts(); const suggestionText = await suggestions.allInnerTexts();
const fizzIndex = suggestionText.findIndex((text) => text.includes("Fizz")); const fizzIndex = suggestionText.findIndex((text) => text.includes("Fizz"));
const aliceIndex = suggestionText.findIndex((text) => text.includes("alice"));
const bobIndex = suggestionText.findIndex((text) => text.includes("bob")); const bobIndex = suggestionText.findIndex((text) => text.includes("bob"));
const charlieIndex = suggestionText.findIndex((text) => const charlieIndex = suggestionText.findIndex((text) =>
text.includes("charlie"), text.includes("charlie"),
@@ -228,11 +234,9 @@ test("@ trigger prioritizes channel members before runnable personas and other a
text.includes("outsider"), text.includes("outsider"),
); );
expect(fizzIndex).toBeGreaterThanOrEqual(0); expect(fizzIndex).toBeGreaterThanOrEqual(0);
expect(aliceIndex).toBeGreaterThanOrEqual(0);
expect(bobIndex).toBeGreaterThanOrEqual(0); expect(bobIndex).toBeGreaterThanOrEqual(0);
expect(charlieIndex).toBeGreaterThanOrEqual(0); expect(charlieIndex).toBeGreaterThanOrEqual(0);
expect(outsiderIndex).toEqual(-1); expect(outsiderIndex).toEqual(-1);
expect(aliceIndex).toBeLessThan(fizzIndex);
expect(bobIndex).toBeLessThan(fizzIndex); expect(bobIndex).toBeLessThan(fizzIndex);
expect(fizzIndex).toBeLessThan(charlieIndex); expect(fizzIndex).toBeLessThan(charlieIndex);
}); });
@@ -425,7 +429,18 @@ test("defers agent mentions until DM members finish loading", async ({
await expect(threadPanel).toContainText("before members resolve"); await expect(threadPanel).toContainText("before members resolve");
}); });
test("autocomplete filters suggestions as user types", async ({ page }) => { test("autocomplete filters managed-agent suggestions as user types", async ({
page,
}) => {
await installMockBridge(page, {
managedAgents: [
{
pubkey: TEST_IDENTITIES.alice.pubkey,
name: "alice",
status: "stopped",
},
],
});
await page.goto("/"); await page.goto("/");
await page.getByTestId("channel-general").click(); await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general"); await expect(page.getByTestId("chat-title")).toHaveText("general");
@@ -462,7 +477,7 @@ test("autocomplete searches global non-member people from the first typed charac
await expect(tessaRow.getByText("not in channel")).toBeVisible(); await expect(tessaRow.getByText("not in channel")).toBeVisible();
}); });
test("mention autocomplete pages global people search beyond the first 50 results", async ({ test("mention autocomplete caps global people search at 50 results", async ({
page, page,
}) => { }) => {
const searchProfiles = Array.from({ length: 55 }, (_, index) => ({ const searchProfiles = Array.from({ length: 55 }, (_, index) => ({
@@ -479,10 +494,8 @@ test("mention autocomplete pages global people search beyond the first 50 result
const dropdown = autocomplete(page); const dropdown = autocomplete(page);
await expect(dropdown.locator("button")).toHaveCount(50); await expect(dropdown.locator("button")).toHaveCount(50);
await dropdown.evaluate((node) => node.scrollTo(0, node.scrollHeight)); await expect(dropdown.getByText("Alex 50")).toBeVisible();
await expect(dropdown.getByText("Alex 55")).toHaveCount(0);
await expect(dropdown.locator("button")).toHaveCount(55);
await expect(dropdown.getByText("Alex 55")).toBeVisible();
await expect(dropdown.getByText("not in channel").last()).toBeVisible(); await expect(dropdown.getByText("not in channel").last()).toBeVisible();
const searchCalls = (await readCommandPayloadLog(page)).filter( const searchCalls = (await readCommandPayloadLog(page)).filter(
@@ -493,6 +506,10 @@ test("mention autocomplete pages global people search beyond the first 50 result
expect.objectContaining({ expect.objectContaining({
payload: expect.objectContaining({ cursor: null, limit: 50 }), payload: expect.objectContaining({ cursor: null, limit: 50 }),
}), }),
]),
);
expect(searchCalls).not.toEqual(
expect.arrayContaining([
expect.objectContaining({ expect.objectContaining({
payload: expect.objectContaining({ cursor: "2", limit: 50 }), payload: expect.objectContaining({ cursor: "2", limit: 50 }),
}), }),
@@ -521,9 +538,18 @@ test("selecting a person mention inserts @Name into input", async ({
await expect(mentionChip).not.toHaveClass(/agent-mention-highlight/); await expect(mentionChip).not.toHaveClass(/agent-mention-highlight/);
}); });
test("selecting an agent mention inserts @Name into input", async ({ test("selecting a managed agent mention inserts @Name into input", async ({
page, page,
}) => { }) => {
await installMockBridge(page, {
managedAgents: [
{
pubkey: TEST_IDENTITIES.alice.pubkey,
name: "alice",
status: "stopped",
},
],
});
await page.goto("/"); await page.goto("/");
await page.getByTestId("channel-general").click(); await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general"); await expect(page.getByTestId("chat-title")).toHaveText("general");
@@ -691,9 +717,18 @@ test("selecting a persona mention reuses an existing persona agent", async ({
await expect(mentionChip).toHaveText("Fizz"); await expect(mentionChip).toHaveText("Fizz");
}); });
test("relay-profile agents with member roles use the agent composer style", async ({ test("managed relay-profile agents with member roles use the agent composer style", async ({
page, page,
}) => { }) => {
await installMockBridge(page, {
managedAgents: [
{
pubkey: TEST_IDENTITIES.charlie.pubkey,
name: "charlie",
status: "stopped",
},
],
});
await page.goto("/"); await page.goto("/");
await openChannelBrowser(page); await openChannelBrowser(page);
@@ -759,10 +794,17 @@ test("own profile-only agents are hidden from channel mentions", async ({
await expect(autocomplete(page)).toHaveCount(0); await expect(autocomplete(page)).toHaveCount(0);
}); });
test("allowlisted relay agents are visible in channel mentions", async ({ test("managed relay agents are visible in channel mentions regardless of relay policy", async ({
page, page,
}) => { }) => {
await installMockBridge(page, { await installMockBridge(page, {
managedAgents: [
{
pubkey: ALLOWLIST_RELAY_AGENT_PUBKEY,
name: "quinn",
status: "stopped",
},
],
relayAgents: [ relayAgents: [
{ {
pubkey: ALLOWLIST_RELAY_AGENT_PUBKEY, pubkey: ALLOWLIST_RELAY_AGENT_PUBKEY,
@@ -784,7 +826,7 @@ test("allowlisted relay agents are visible in channel mentions", async ({
await expect(dropdown.getByText("agent")).toBeVisible(); await expect(dropdown.getByText("agent")).toBeVisible();
}); });
test("non-allowlisted relay agents stay hidden from channel mentions", async ({ test("relay-only agents stay hidden from channel mentions even when allowlisted", async ({
page, page,
}) => { }) => {
await installMockBridge(page, { await installMockBridge(page, {
@@ -793,7 +835,7 @@ test("non-allowlisted relay agents stay hidden from channel mentions", async ({
pubkey: ALLOWLIST_RELAY_AGENT_PUBKEY, pubkey: ALLOWLIST_RELAY_AGENT_PUBKEY,
name: "quinn", name: "quinn",
respondTo: "allowlist", respondTo: "allowlist",
respondToAllowlist: [TEST_IDENTITIES.outsider.pubkey], respondToAllowlist: [MOCK_VIEWER_PUBKEY],
}, },
], ],
}); });
@@ -1364,9 +1406,18 @@ test("system member-joined rows render the joined person as a plain profile name
await expect(joinedPersonName).not.toHaveAttribute("data-mention"); await expect(joinedPersonName).not.toHaveAttribute("data-mention");
}); });
test("selecting a non-member agent from a DM inserts @Name into input", async ({ test("selecting a managed non-member agent from a DM inserts @Name into input", async ({
page, page,
}) => { }) => {
await installMockBridge(page, {
managedAgents: [
{
pubkey: TEST_IDENTITIES.charlie.pubkey,
name: "charlie",
status: "stopped",
},
],
});
await page.goto("/"); await page.goto("/");
await page.getByTestId("channel-bob-tyler").click(); await page.getByTestId("channel-bob-tyler").click();
await expect(page.getByTestId("chat-title")).toHaveText("bob-tyler"); await expect(page.getByTestId("chat-title")).toHaveText("bob-tyler");
@@ -1450,9 +1501,18 @@ test("sent non-member person mention uses the normal mention style", async ({
await expect(mentionChip.locator("svg")).toHaveCount(0); await expect(mentionChip.locator("svg")).toHaveCount(0);
}); });
test("sent non-member agent mention uses the agent mention style", async ({ test("sent managed non-member agent mention uses the agent mention style", async ({
page, page,
}) => { }) => {
await installMockBridge(page, {
managedAgents: [
{
pubkey: TEST_IDENTITIES.charlie.pubkey,
name: "charlie",
status: "stopped",
},
],
});
await page.goto("/"); await page.goto("/");
await page.getByTestId("channel-bob-tyler").click(); await page.getByTestId("channel-bob-tyler").click();
await expect(page.getByTestId("chat-title")).toHaveText("bob-tyler"); await expect(page.getByTestId("chat-title")).toHaveText("bob-tyler");

View File

@@ -143,7 +143,16 @@ test("focus and split preserve reading context and interaction ownership", async
await page.addInitScript(() => { await page.addInitScript(() => {
localStorage.setItem("buzz.channels.threadViewMode", "focus"); localStorage.setItem("buzz.channels.threadViewMode", "focus");
}); });
await installMockBridge(page); await installMockBridge(page, {
managedAgents: [
{
pubkey:
"953d3363262e86b770419834c53d2446409db6d918a57f8f339d495d54ab001f",
name: "alice",
status: "stopped",
},
],
});
await page.goto("/"); await page.goto("/");
const rootId = await seedLongThread(page); const rootId = await seedLongThread(page);

View File

@@ -110,7 +110,16 @@ test.describe("list virtualization", () => {
test("03 — members search shows both sticky titles under content-visibility", async ({ test("03 — members search shows both sticky titles under content-visibility", async ({
page, page,
}) => { }) => {
await installMockBridge(page); await installMockBridge(page, {
managedAgents: [
{
pubkey:
"554cef57437abac34522ac2c9f0490d685b72c80478cf9f7ed6f9570ee8624ea",
name: "charlie",
status: "stopped",
},
],
});
await page.goto("/"); await page.goto("/");
await page.getByTestId("channel-general").click(); await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general"); await expect(page.getByTestId("chat-title")).toHaveText("general");