Fix duplicate-name user search results - #6483
Conversation
Preserve pubkey-distinct people in mention autocomplete and paginate selected-people agent access search. Co-authored-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz> Signed-off-by: Wes <wesbillman@users.noreply.github.com>
wesbillman
left a comment
There was a problem hiding this comment.
Carl, an automated reviewer, commenting via Wes’s GitHub account.
Reviewed exact head 9c0eed1234f904d121adce1842b215820d3a0e83 against base 4e3c9e619c93dd26677b392ad1f8cf0d12c8f855.
Two blocking correctness gaps remain:
-
Preserving duplicate rows does not preserve identity after selection.
insertMentionstill recordsMap<displayName, pubkey>atdesktop/src/features/messages/lib/useMentions.ts:607-617. Select two different people namedWilland the secondMap.set("Will", ...)overwrites the first;extractMentionPubkeysthen applies that surviving pubkey to every matching@Willoccurrence (desktop/src/features/messages/lib/extractMentionPubkeys.ts:52-88). Deleting the second visible token can therefore leave the first token notifying the second person. Draft/edit restoration has the same name-keyed shape. The new E2E selects only one duplicate in isolation, so it misses the broken lifecycle. Keep occurrence-bound identity (or insert stable disambiguated labels) and cover select-both, delete-one, send, draft restore, and edit across duplicate person/member/agent/persona labels. -
Selected-people pagination dead-ends when client filtering leaves no scrollable first page.
RespondToField.tsx:136-143removes already selected and archived users, but the onlyfetchNextPagetrigger is attached to the results scroll container, which renders only when filtered results are nonempty (RespondToField.tsx:417-448;features/profile/hooks.ts:481-501). If the raw first 50 matches are filtered out, or leave too few rows to overflow,nextCursorexists but page 2 is unreachable and a valid later person cannot be selected. Prefetch until the visible list can scroll or the query is exhausted (or provide an explicit load-more path), and test both an entirely filtered first page and a non-overflowing first page.
CI was still in progress when this review was submitted; these defects are source-traced and independently reproduced, not CI-derived.
Verify that selecting either same-name search result sends only the pubkey attached to that row. Co-authored-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz> Signed-off-by: Wes <wesbillman@users.noreply.github.com>
wpfleger96
left a comment
There was a problem hiding this comment.
🤖 Paul, an automated reviewer, commenting via Will's GitHub account. Consolidated feedback from three independent passes (two source reviews, one live E2E run) at head 484b1651e619b829f10548feda8e1acf5ff6908e.
Both source reviews independently reproduced the same two blockers Carl already left above, so I'm confirming rather than restating them in full:
-
Same-name selection still collapses identity after the isolated happy path.
insertMentionkeys the mention map by display name (desktop/src/features/messages/lib/useMentions.ts), so selecting a second distinct "Will" overwrites the first, andextractMentionPubkeysresolves every visible@Willtoken to the survivor. The new E2E tests select each duplicate in separate tests, so they can't expose the collision. Draft/edit restoration shares the name-keyed shape. -
Selected-people pagination can dead-end.
RespondToFieldfilters selected/archived users from each raw page, but the onlyfetchNextPagetrigger isonScrollon a container that renders only when filtered results are nonempty. A fully filtered or non-overflowing first page makes page 2 unreachable even whennextCursorexists — and the allowlist filter grows with every selection. Prefetch until the list can scroll or the query is exhausted, or add an explicit load-more.
Live E2E note: a 51-person adversarial run of the selected-people flow passed — page-2 fetch fired on scroll and respondToAllowlist preserved both same-name pubkeys. That path stores pubkeys directly, so it doesn't refute either blocker: 1 lives in the mention-map lifecycle, and 2 requires a filtered-empty/non-overflowing first page that a scroll can't reach.
Everything else checked out clean: removal of coalesceAutocompleteCandidatesByKey/globalSearchIdentityKey leaves no dangling references, the infinite-query usage matches the PersonaShareRecipients/MembersSidebar pattern, and the same-name coalescing unit test is correct.
jedwards27
left a comment
There was a problem hiding this comment.
:bot: Jude’s code review agent
Verdict: REQUEST CHANGES
Reviewed: PR base ref 4e3c9e619c93dd26677b392ad1f8cf0d12c8f855 through exact head 484b1651e619b829f10548feda8e1acf5ff6908e
Risk: high — this changes messaging recipient identity and agent-access authorization selection.
Three blocking correctness/trust gaps remain:
-
Selecting two different people with the same display name in one message drops the first identity.
desktop/src/features/messages/lib/useMentions.ts:84,607-616still stores selections asMap<displayName, pubkey>, so the secondWilloverwrites the first.extractMentionPubkeys.ts:30,34-53,81-84can consequently emit only the surviving pubkey. An adversarial E2E selected both duplicate-name rows and sent one composition: expected both pubkeys, observed only the second. The shipped tests select each duplicate only in isolation, so they do not guard the failing lifecycle. Preserve identity by occurrence (or another structure that can hold colliding display names), then add a regression selecting both same-name people in one composition and asserting both outgoingptags. -
The “Selected people” access picker can render two distinct pubkeys with identical visible and accessible identity.
RespondToField.tsx:62-77,422-446uses display name plus NIP-05 and omits the pubkey whenever NIP-05 exists. Two profiles claimingWill/will@example.comtherefore expose identical buttons even though granting access affects files, accounts, and connected tools. The NIP-05 is copied from self-authored kind-0 JSON without verification state (desktop/src-tauri/src/nostr_convert/user_search.rs:11-25), so it is not a unique or trustworthy discriminator. The new E2E covers only duplicate names without NIP-05 (agent-access-warning.spec.ts:131-174). Collision-detect the rendered tuple and include a pubkey-derived discriminator in both the row and accessible name; make the full key keyboard-accessible in line withshared/ui/PubKey.tsx:18-29. Test two pubkeys sharing both display name and NIP-05, keyboard-select each, and assert the exact persisted pubkey. -
Selected-people pagination can dead-end after client filtering.
RespondToField.tsx:136-145filters already-selected and archived people after each server page, while the only next-page trigger isonScrollon the results container (RespondToField.tsx:417-448;features/profile/hooks.ts:481-501). If page 1 is fully filtered, the container is not rendered; if too few rows remain to overflow, no scroll event reaches the threshold. A valid person on page 2 is then unreachable despitehasNextPage. Prefetch until the visible list can scroll or the query is exhausted, or provide an explicit load-more action; cover both a fully filtered first page and a non-overflowing first page.
Validation at exact clean head:
pnpm build:e2e: PASS.- Focused same-name Playwright cases: 3/3 PASS.
just desktop-ci: PASS.- Mutation removing pubkey-based duplicate preservation made the new single-selection tests fail, confirming those tests protect duplicate-row visibility but not the select-both lifecycle.
- Source traced search/coalescing through access mutation payloads; no additional confirmed tenancy/cache or stale-response contamination in the inspected paths.
- GitHub CI at submission: macOS build and Desktop integration green; Desktop Core and smoke shards 1/2/4 still running.
Manual/native evidence: no native capture; these blockers are renderer-contained and were established by executable E2E/source behavior. Residual risk: narrow-layout/theme behavior was not separately captured, and pending CI cannot resolve these untested identity/access paths.
Fetch another user-search page when selected or archived results leave the visible result viewport underfilled. Cover fully filtered and non-scrollable first pages, and capture duplicate-name UI evidence. Co-authored-by: Wes <wesbillman@users.noreply.github.com> Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz> Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Always expose the canonical pubkey for access-picker search results and include the full key in each Add action accessible name. Verify keyboard selection persists only the chosen identity. Co-authored-by: Carl <32a2e2c9d428ee08902cab75d956da2c1d235a22d4766b0dd4138bf6e2e5db1d@buzz.block.builderlab.xyz> Signed-off-by: Wes <wesbillman@users.noreply.github.com>
jedwards27
left a comment
There was a problem hiding this comment.
:bot: Jude’s code review agent
Verdict: REQUEST CHANGES
Reviewed: 4e3c9e619c93dd26677b392ad1f8cf0d12c8f855..206e6074407ebe47cebd69220a5d615afc98b4a3 (exact head 206e6074407ebe47cebd69220a5d615afc98b4a3)
Risk: high — recipient identity and agent-access identity must remain exact when display metadata collides.
Behavior/contracts traced: search pagination/filtering, duplicate-name rendering and accessible disambiguation, keyboard access selection/persistence, mention selection state, and outgoing pubkey extraction.
Blocking finding
- Two distinct same-name recipients still cannot be preserved in one composition.
desktop/src/features/messages/lib/useMentions.ts:84stores selections inMap<displayName, pubkey>;useMentions.ts:607-616sets by display name, so selecting a secondWilloverwrites the first;useMentions.ts:782-789passes the collapsed map into extraction. A clean exact-head adversarial Playwright probe selected pubkeys111…111and222…222as two@Willmentions, expected both outgoing pubkeys, and received only222…222. The shipped cases atdesktop/tests/e2e/mentions.spec.ts:690-735select either duplicate alone and therefore miss the failure. Represent mention identity by occurrence/pubkey rather than display-name key, and add an E2E selecting both identities in one composition, deleting either occurrence, and asserting the exact remaining/sent pubkeys.
Resolved prior findings: the refreshed head now handles filtered/non-scrollable pagination and always exposes pubkey-based visible/accessibility disambiguation in the agent-access picker; the hostile identical-name/NIP-05 keyboard persistence case passes.
Validation at matching clean HEAD: pnpm build:e2e PASS; focused shipped Playwright 5/5 PASS; access collision/pagination 3/3 PASS; same-name single-selection mentions 2/2 PASS; the temporary select-both probe failed causally as described and was removed. One combined Playwright attempt lost its local server and was discarded as infra-invalid.
Manual/native evidence: not run; both the resolved behaviors and remaining failure are renderer-contained and executable/source-deterministic.
Residual risk: Desktop Core and smoke shards were still pending at lane refresh. Their completion cannot repair the reproduced untested recipient-loss path.
jedwards27
left a comment
There was a problem hiding this comment.
Verdict: REQUEST CHANGES
Reviewed: 4e3c9e619c93dd26677b392ad1f8cf0d12c8f855..206e6074407ebe47cebd69220a5d615afc98b4a3 (exact head 206e6074407ebe47cebd69220a5d615afc98b4a3)
Risk: high — this changes messaging recipient identity and selected-people access behavior.
The new head resolves the previous pagination and access-picker ambiguity: filtered non-scrollable results continue fetching, each access result visibly exposes its pubkey, and the Add button's accessible name includes the full key. The hostile same-name/same-NIP-05 keyboard case correctly selects the intended pubkey.
Blocking finding
desktop/src/features/messages/lib/useMentions.ts:84,607-616,782-789still keys selected mentions bydisplayName. Selecting pubkey111…111as@Willand then pubkey222…222as another@Willoverwrites the first map entry. The composer can display both tokens, but the outgoing mention list contains only222…222; the first intended recipient is silently lost. This violates the PR's core identity-preservation contract. The added cases indesktop/tests/e2e/mentions.spec.ts:691-735select either identity separately, so they do not cover the failing two-recipient composition.
Please preserve mention identity by occurrence/pubkey rather than display-name key, then add an E2E that selects both same-name identities in one composition, verifies deletion of either occurrence leaves the correct identity, and asserts both exact outgoing pubkeys when both remain.
Validation at matching clean HEAD:
cd desktop && pnpm build:e2e— PASS independently.- Focused shipped same-name and filtered/non-scrollable pagination Playwright coverage — PASS (5/5 in one run; independently repeated as 3/3 access/pagination and 2/2 mention cases).
- Temporary adversarial Playwright selecting both same-name rows — FAIL as expected: expected
[111…111, 222…222], received[222…222]; probe removed and clean head restored. - One earlier combined Playwright attempt lost its local HTTP server after a timeout; treated as infrastructure-invalid and replaced by clean passing focused runs.
Manual/native evidence: not run. The fixed access semantics and remaining blocker are renderer-contained and established directly by source plus executable E2E evidence.
Residual risk: CI was still in progress at review time (Desktop Core/smoke jobs pending), but green CI cannot cover the missing multi-recipient case.
— :bot: Jude’s code review agent


Summary
Testing
cd desktop && pnpm build:e2ecd desktop && pnpm exec playwright test tests/e2e/mentions.spec.ts tests/e2e/agent-access-warning.spec.ts --project=smoke --grep "same-name"