diff --git a/desktop/src/features/agents/lib/agentAutocompleteEligibility.test.mjs b/desktop/src/features/agents/lib/agentAutocompleteEligibility.test.mjs index 2cb2068d51..0f911ad41b 100644 --- a/desktop/src/features/agents/lib/agentAutocompleteEligibility.test.mjs +++ b/desktop/src/features/agents/lib/agentAutocompleteEligibility.test.mjs @@ -336,7 +336,7 @@ test("shouldHideAgentFromMentions: normalizes the pubkey before lookup", () => { ); }); -test("coalesceAgentAutocompleteCandidates: merges agents with the same persona id", () => { +test("coalesceAgentAutocompleteCandidates: keeps agents with the same persona id distinct", () => { const first = makeAgent({ pubkey: PUB_A, personaId: "pinky" }); const second = makeAgent({ pubkey: PUB_B, @@ -344,10 +344,10 @@ test("coalesceAgentAutocompleteCandidates: merges agents with the same persona i isMember: true, }); - assert.deepEqual(coalesce([first, second]), [second]); + assert.deepEqual(coalesce([first, second]), [first, second]); }); -test("coalesceAgentAutocompleteCandidates: merges agents with the same owner and name", () => { +test("coalesceAgentAutocompleteCandidates: keeps agents with the same owner and name distinct", () => { const first = makeAgent({ pubkey: PUB_A, ownerPubkey: OWNER_PUBKEY }); const second = makeAgent({ pubkey: PUB_B, @@ -355,7 +355,7 @@ test("coalesceAgentAutocompleteCandidates: merges agents with the same owner and isMember: true, }); - assert.deepEqual(coalesce([first, second]), [second]); + assert.deepEqual(coalesce([first, second]), [first, second]); }); test("coalesceAgentAutocompleteCandidates: keeps same-name agents with different owners distinct", () => { @@ -382,7 +382,7 @@ test("coalesceAgentAutocompleteCandidates: keeps owner-less managed same-name ag assert.deepEqual(coalesce([first, second]), [first, second]); }); -test("coalesceAgentAutocompleteCandidates: merges current-owner same-name agents", () => { +test("coalesceAgentAutocompleteCandidates: keeps current-owner same-name agents distinct", () => { const first = makeAgent({ pubkey: PUB_A, ownerPubkey: CURRENT_PUBKEY }); const second = makeAgent({ pubkey: PUB_B, @@ -390,6 +390,16 @@ test("coalesceAgentAutocompleteCandidates: merges current-owner same-name agents isManagedAgent: true, }); + assert.deepEqual(coalesce([first, second]), [first, second]); +}); + +test("coalesceAgentAutocompleteCandidates: coalesces repeated source rows for the same pubkey", () => { + const first = makeAgent({ pubkey: PUB_A }); + const second = makeAgent({ + pubkey: PUB_A.toUpperCase(), + isMember: true, + }); + assert.deepEqual(coalesce([first, second]), [second]); }); diff --git a/desktop/src/features/agents/lib/agentAutocompleteEligibility.ts b/desktop/src/features/agents/lib/agentAutocompleteEligibility.ts index 0abdad82fa..3fb4e23c15 100644 --- a/desktop/src/features/agents/lib/agentAutocompleteEligibility.ts +++ b/desktop/src/features/agents/lib/agentAutocompleteEligibility.ts @@ -184,75 +184,39 @@ type AgentAutocompleteCandidate = { personaId?: string | null; }; -function normalizeLabel(label: string | null | undefined) { - return label?.trim().toLowerCase() || null; -} - -function agentIdentityKey( - candidate: T, - currentPubkey: string | null | undefined, - getLabel: (candidate: T) => string | null | undefined, -) { - if (candidate.isAgent !== true) { - return null; - } - - if (candidate.personaId) { - return `persona:${candidate.personaId}`; - } - - const label = normalizeLabel(getLabel(candidate)); - if (!label) { +function agentIdentityKey(candidate: T) { + if (candidate.isAgent !== true || !candidate.pubkey) { return null; } - const ownerPubkey = candidate.ownerPubkey - ? normalizePubkey(candidate.ownerPubkey) - : null; - if (ownerPubkey) { - if (currentPubkey && ownerPubkey === normalizePubkey(currentPubkey)) { - return `local:name:${label}`; - } - return `owner:${ownerPubkey}:name:${label}`; - } - - return null; + // Pubkeys—not persona metadata or a display name—are agent identities. + // A persona may be installed more than once, and an owner may intentionally + // create multiple same-named agents. Collapsing either case makes one agent + // impossible to choose from autocomplete. + return `pubkey:${normalizePubkey(candidate.pubkey)}`; } function agentCandidateRank( candidate: T, - currentPubkey: string | null | undefined, preferredPubkeys: ReadonlySet, ) { const pubkey = candidate.pubkey ? normalizePubkey(candidate.pubkey) : null; - const ownerPubkey = candidate.ownerPubkey - ? normalizePubkey(candidate.ownerPubkey) - : null; - const normalizedCurrentPubkey = currentPubkey - ? normalizePubkey(currentPubkey) - : null; return [ candidate.isMember === true ? 0 : 1, pubkey && preferredPubkeys.has(pubkey) ? 0 : 1, candidate.isManagedAgent === true ? 0 : 1, candidate.personaId ? 0 : 1, - ownerPubkey && ownerPubkey === normalizedCurrentPubkey ? 0 : 1, ]; } function isPreferredAgentCandidate( next: T, current: T, - currentPubkey: string | null | undefined, preferredPubkeys: ReadonlySet, ) { - const nextRank = agentCandidateRank(next, currentPubkey, preferredPubkeys); - const currentRank = agentCandidateRank( - current, - currentPubkey, - preferredPubkeys, - ); + const nextRank = agentCandidateRank(next, preferredPubkeys); + const currentRank = agentCandidateRank(current, preferredPubkeys); for (let index = 0; index < nextRank.length; index++) { if (nextRank[index] !== currentRank[index]) { @@ -291,8 +255,8 @@ export function coalesceAgentAutocompleteCandidates< >( candidates: readonly T[], { - currentPubkey, - getLabel, + currentPubkey: _currentPubkey, + getLabel: _getLabel, preferredPubkeys = new Set(), }: { currentPubkey?: string | null; @@ -304,7 +268,7 @@ export function coalesceAgentAutocompleteCandidates< const indexesByKey = new Map(); for (const candidate of candidates) { - const key = agentIdentityKey(candidate, currentPubkey, getLabel); + const key = agentIdentityKey(candidate); if (!key) { output.push(candidate); continue; @@ -321,7 +285,6 @@ export function coalesceAgentAutocompleteCandidates< isPreferredAgentCandidate( candidate, output[currentIndex], - currentPubkey, preferredPubkeys, ) ) { diff --git a/desktop/src/features/channels/ui/MembersSidebar.tsx b/desktop/src/features/channels/ui/MembersSidebar.tsx index 459e8f7776..8f431fb0af 100644 --- a/desktop/src/features/channels/ui/MembersSidebar.tsx +++ b/desktop/src/features/channels/ui/MembersSidebar.tsx @@ -280,12 +280,6 @@ export function MembersSidebar({ agent, ]), ); - const memberAgentLabels = new Set( - rawMembers - .filter((member) => member.isAgent === true || member.role === "bot") - .map((member) => member.displayName?.trim().toLowerCase()) - .filter((label): label is string => Boolean(label)), - ); const sharedChannelIds = getSharedChannelIds(channelsQuery.data); const allowedAgentPubkeys = getMentionableAgentPubkeys({ currentPubkey, @@ -298,10 +292,6 @@ export function MembersSidebar({ const addCandidate = (candidate: AddMemberSearchCandidate) => { const pubkey = normalizePubkey(candidate.pubkey); if ( - (candidate.isAgent && - memberAgentLabels.has( - formatAddCandidateName(candidate).toLowerCase(), - )) || memberPubkeys.has(pubkey) || isArchivedDiscovery(pubkey) || !isAgentIdentityInAllowedList(candidate, allowedAgentPubkeys) @@ -391,7 +381,6 @@ export function MembersSidebar({ normalizedDeferredSearchQuery, relayAgentsQuery.data, userSearchResults, - rawMembers, ]); const isAddSearchLoading = userSearchQuery.isLoading || @@ -969,6 +958,9 @@ function AddMemberSearchResultRow({ agent + + {truncatePubkey(user.pubkey)} + {ownerLabel ? ( managed by {ownerLabel} diff --git a/desktop/tests/e2e/channels.spec.ts b/desktop/tests/e2e/channels.spec.ts index d58c3303df..13c6d5504a 100644 --- a/desktop/tests/e2e/channels.spec.ts +++ b/desktop/tests/e2e/channels.spec.ts @@ -3774,7 +3774,7 @@ test("channel header actions show tooltips", async ({ page }) => { } }); -test("members sidebar collapses same-persona managed agents", async ({ +test("members sidebar retains distinct same-persona managed agents", async ({ page, }) => { const inChannelAgentPubkey = @@ -3824,8 +3824,8 @@ test("members sidebar collapses same-persona managed agents", async ({ ).toHaveCount(0); await expect( page.getByTestId(`channel-user-search-result-${outOfChannelAgentPubkey}`), - ).toHaveCount(0); - await expect(page.getByText("Pinky", { exact: true })).toHaveCount(1); + ).toBeVisible(); + await expect(page.getByText("Pinky", { exact: true })).toHaveCount(2); }); test("private-channel members cannot add people without owner/admin", async ({