fix(desktop): prefer live agent mentions - #2149
Conversation
Co-authored-by: npub1rf6fvdj6ut0c4kcmjv4p5mmgh89nj58n69uu3fz3cvk3jn500hqs7emz79 <1a7496365ae2df8adb1b932a1a6f68b9cb3950f3d179c8a451c32d194e8f7dc1@sprout-oss.stage.blox.sqprod.co> Signed-off-by: npub1rf6fvdj6ut0c4kcmjv4p5mmgh89nj58n69uu3fz3cvk3jn500hqs7emz79 <1a7496365ae2df8adb1b932a1a6f68b9cb3950f3d179c8a451c32d194e8f7dc1@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1rf6fvdj6ut0c4kcmjv4p5mmgh89nj58n69uu3fz3cvk3jn500hqs7emz79 <1a7496365ae2df8adb1b932a1a6f68b9cb3950f3d179c8a451c32d194e8f7dc1@sprout-oss.stage.blox.sqprod.co> Signed-off-by: npub1rf6fvdj6ut0c4kcmjv4p5mmgh89nj58n69uu3fz3cvk3jn500hqs7emz79 <1a7496365ae2df8adb1b932a1a6f68b9cb3950f3d179c8a451c32d194e8f7dc1@sprout-oss.stage.blox.sqprod.co>
Co-authored-by: npub1rf6fvdj6ut0c4kcmjv4p5mmgh89nj58n69uu3fz3cvk3jn500hqs7emz79 <1a7496365ae2df8adb1b932a1a6f68b9cb3950f3d179c8a451c32d194e8f7dc1@sprout-oss.stage.blox.sqprod.co> Signed-off-by: npub1rf6fvdj6ut0c4kcmjv4p5mmgh89nj58n69uu3fz3cvk3jn500hqs7emz79 <1a7496365ae2df8adb1b932a1a6f68b9cb3950f3d179c8a451c32d194e8f7dc1@sprout-oss.stage.blox.sqprod.co>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 10b8ee1189
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| 0, | ||
| Math.max(MENTION_SUGGESTION_LIMIT, mentionCandidatesWithTeams.length), | ||
| ) | ||
| .slice(0, MENTION_SUGGESTION_LIMIT) |
There was a problem hiding this comment.
Preserve paged mention results after scrolling
With a broad mention query that matches more than 50 identities, MentionAutocomplete still calls fetchMoreSuggestions on scroll, but this fixed slice discards everything after the first 50 ranked items on every render. After page 2 of useInfiniteUserSearchQuery is fetched, userSearchResults grows while the dropdown keeps returning the same first 50 suggestions, so users outside the first page can never be selected from autocomplete.
Useful? React with 👍 / 👎.
Co-authored-by: npub1rf6fvdj6ut0c4kcmjv4p5mmgh89nj58n69uu3fz3cvk3jn500hqs7emz79 <1a7496365ae2df8adb1b932a1a6f68b9cb3950f3d179c8a451c32d194e8f7dc1@sprout-oss.stage.blox.sqprod.co> Signed-off-by: npub1rf6fvdj6ut0c4kcmjv4p5mmgh89nj58n69uu3fz3cvk3jn500hqs7emz79 <1a7496365ae2df8adb1b932a1a6f68b9cb3950f3d179c8a451c32d194e8f7dc1@sprout-oss.stage.blox.sqprod.co>
10b8ee1 to
60352ea
Compare
Co-authored-by: npub1rf6fvdj6ut0c4kcmjv4p5mmgh89nj58n69uu3fz3cvk3jn500hqs7emz79 <1a7496365ae2df8adb1b932a1a6f68b9cb3950f3d179c8a451c32d194e8f7dc1@sprout-oss.stage.blox.sqprod.co> Signed-off-by: npub1rf6fvdj6ut0c4kcmjv4p5mmgh89nj58n69uu3fz3cvk3jn500hqs7emz79 <1a7496365ae2df8adb1b932a1a6f68b9cb3950f3d179c8a451c32d194e8f7dc1@sprout-oss.stage.blox.sqprod.co>
wesbillman
left a comment
There was a problem hiding this comment.
Approving per Wes's product call: agent mentions are scoped to the current user's managed Agents list; cross-user agent mentions (member/allowlisted teammate agents) are intentionally excluded. The implementation does exactly what it states, the regression tests codify the new semantics, and CI is fully green.
Two non-blocking notes for the record:
- While
managedAgentsQueryis in flight (or the list is empty), all agent candidates vanish from the typeahead until it resolves — a brief flash, worth a loading guard someday. - This also reverts the paginate-beyond-50 mention search from #1418 to a hard 50-result cap, leaving
fetchMoreSuggestions/hasMoreSuggestionswiring effectively dead in the composers. Accepted as part of this PR; a follow-up could prune the dead wiring.
Reviewed-by: Brain (Buzz agent) on behalf of Wes.
…rics * origin/main: (96 commits) fix(desktop): prefer live agent mentions (#2149) fix(desktop): close focused threads on Escape (#2154) fix: skip membership lookup for open relays (#2107) fix(desktop): make invite QR downloadable (#2168) Archive managed agents when deleted (#2135) perf(relay): cache Git pack hydration (#2169) chore(deps): update rust crate rustls to v0.23.42 (#2151) chore(deps): update dependency @tanstack/react-virtual to v3.14.6 (#2153) Add Agent Config Core: harness capability model behind agent config surfaces (#2158) chore(deps): update all non-major dependencies (#2152) chore(deps): update rust crate getrandom to v0.4.3 (#2150) fix(relay): bound and observe Git read operations (#2167) fix(desktop): derive default clone URL for relay-hosted repos (#2166) fix(desktop): mask composer rounded corners (#2165) feat(desktop): add PR merge conflict recovery (#2164) fix(desktop): mask scrolled content behind channel and thread composers (#2163) feat(desktop): add inline PR diff comments (#2162) feat(desktop): add commit-scoped PR review decisions (#2161) fix(desktop): batch project work item queries (#2160) feat(desktop): make missing project checkouts actionable (#2159) ...
An agent that is a member of the current channel is now exempt from the managed-list drop gate in the mention composer, so agents managed on another machine (or by another user) stay in @-autocomplete for every channel member. The block#2149 restriction still applies to non-member agent identities, which is the duplicate-noise case it was introduced for. Fixes block#2508 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Rod Roudi <1985578+rodroudi@users.noreply.github.com>
The directory projection accepted "nobody" and forwarded it verbatim. RelayAgentInfo deserializes respond_to into RespondTo, which deliberately has no nobody variant because the desktop exposes no surface for it, so a single validly signed event carrying that mode failed the whole Vec and every other agent vanished from discovery. Project only the modes RespondTo can represent. An unrepresentable mode becomes absent, and every eligibility check treats absent as not-invocable, so the projection degrades closed rather than either crashing or over-granting. Also record two things the review of this branch surfaced. The stale-allowlist section now separates the finding from its implemented disposition instead of still reading as open. And the allowlisted-agent autocomplete change is documented as a deliberate fork divergence from block/buzz a4dfead (block#2149), which moved upstream the other way on purpose; it is excluded from the upstream series and needs a maintainer decision before it is ever proposed. Its upstream E2E test passes here only because that fixture agent is not a channel member, so green CI must not be read as upstream agreement. Co-authored-by: Mark Fenner <markfenner57@yahoo.com> Signed-off-by: Mark Fenner <markfenner57@yahoo.com>
58e8ea1 let the relay decide who may be ADDED to a channel, but the mention surface still refused to complete a non-owned agent: useMentions pre-filtered every agent candidate through the caller's managed-agents list (block#2149), and extractMentionPubkeys resolves from the same filtered candidates — so even a hand-typed @name produced no p tag. You could add a teammate's agent to a channel and then never talk to it. Scope the local-ownership gate to non-members. Membership is the relay-sanctioned signal (kind:9000 + per-user channel_add_policy), so a member agent is offered regardless of who runs it. Non-member agents keep requiring local ownership — block#2149's cleanup of search/directory ghosts stands: relay-only agents stay hidden, allowlisted or not. That makes the block#1611 member branch reachable again, so tighten its "explicit not-invocable" directory signal. Hiding on mere kind:10100 presence was built on data 58e8ea1 already established as vacuous (set-add-policy publishes neither respond_to nor channel_ids). A member now hides only when a populated allowlist leaves the viewer out — the one directory fact with trustworthy exclusion semantics. Vacuous entries, "anyone" with stale channel_ids, and owner-only (ownership is not reliably knowable client-side) all show; whether the agent answers stays the harness's respond_to decision (default owner-only). knownAgentPubkeys also grows the surviving agent candidates so a non-owned member mention classifies as an agent mention at send time (explicit agent audience, channel prep, chip styling) instead of falling back to the human path. Flips the block#2149 member-agent expectations in mentions.spec.ts and adds coverage for the other-owned-member, owner-only-entry, and allowlist-exclusion cases. Candidate merge + search-label helpers move to mentionCandidates.ts to keep useMentions.ts under the file-size guard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: kbst9 <kevinbsteiner@gmail.com>
Reverts the managed-list gate added in a4dfead (block#2149) at both call sites, so @-mention autocomplete and the add-member picker fall back to shouldHideAgentFromMentions, which already hides non-invocable agents. An agent that is a bot member of a shared channel becomes addressable again from a second machine. Local stopgap for block#3277 — not an upstream fix. It also un-hides profile-only agent identities (the `mira` fixture), which the desktop e2e suite asserts must stay hidden. A mergeable fix additionally needs the missing kind:10100 publisher, so directory-backed invocability can carry the signal the local managed list is standing in for today. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: chinglinwen <chinglinwen@gmail.com>
…licy The add-member search dropped every agent-classified candidate not in the local managed store, and the relay's actual authority for third-party adds — the agent's kind:10100 channel_add_policy, enforced server-side in handlers/side_effects.rs (owner_only checked against the stored attested owner, nobody refused, anyone allowed) — was never consulted client-side: the field was not even carried through the RelayAgentInfo conversion. Carry channel_add_policy through the wire types and gate the picker on a predicate aligned with relay enforcement: managed agents always offered (native parity, unchanged); external agents offered when their declared policy is "anyone", or "owner_only" when the viewer is the NIP-OA-verified owner; "nobody" hidden from everyone including the owner (the relay refuses those adds regardless of actor). The picker never offers an external-agent add the relay will reject. In one direction it is deliberately STRICTER than the relay: agents with no directory entry stay hidden even though the relay — whose stored policy defaults to 'anyone' — would accept the add. That tightening preserves the stale-identity suppression the managed-list gate was introduced for (block#2149): no declaration, no offer. Complements the mention-eligibility fix (block#3676): each surface reads its own declaration — respond_to for mentions, channel_add_policy for adds. Unit tests cover the matrix: people / managed / anyone / owner_only as owner, non-owner, unverified / nobody incl. owner / no entry / null policy / pubkey normalization. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Bryce Del Rio <bryce.delrio@gmail.com>
…d owner Community feedback on block#2987 surfaced two subcases the predicate left cold: an attested external agent with no kind:10100 at all (never published, or deleted), and — after the block#2508 thread's cross-machine reports — the agent's own owner on a device that doesn't manage it (block#2349, block#3277). Both reduce to the same case: a member agent with a verified NIP-OA attestation and no directory declaration. Offer that candidate to its verified owner only. This is proof-sound: the harness's inbound author gate admits the owner under every respond-to mode, so an owner's mention is never a void chip — and it needs no directory entry, which matters because nothing publishes entries for native agents yet. Strangers still see nothing: attribution without a declaration is not evidence the agent would answer them. Stale same-name identities an owner might now see fold into the live one via the existing same-label/same-owner coalescing, and NIP-IA archived identities are peeled before candidates are built — the two block#2149-era layers this change leans on, both untouched. Tests: declaration-less agent hidden from strangers and unverified attributions, offered to the verified owner. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Bryce Del Rio <bryce.delrio@gmail.com>
…licy The add-member search dropped every agent-classified candidate not in the local managed store, and the relay's actual authority for third-party adds — the agent's kind:10100 channel_add_policy, enforced server-side in handlers/side_effects.rs (owner_only checked against the stored attested owner, nobody refused, anyone allowed) — was never consulted client-side: the field was not even carried through the RelayAgentInfo conversion. Carry channel_add_policy through the wire types and gate the picker on a predicate aligned with relay enforcement: managed agents always offered (native parity, unchanged); external agents offered when their declared policy is "anyone", or "owner_only" when the viewer is the NIP-OA-verified owner; "nobody" hidden from everyone including the owner (the relay refuses those adds regardless of actor). The picker mirrors the relay's declared policy; enforcement stays server-side, so the rare divergence (the relay enforces stored DB state — first-write-wins owner mapping, last-validated policy — while the client reads the latest published declaration) surfaces as a visible add error rather than a silent gap. In one direction the picker is deliberately STRICTER than the relay: agents with no directory entry stay hidden even though the relay — whose stored policy defaults to 'anyone' — would accept the add. That tightening preserves the stale-identity suppression the managed-list gate was introduced for (block#2149): no declaration, no offer. Complements the mention-eligibility fix (block#3676): each surface reads its own declaration — respond_to for mentions, channel_add_policy for adds. Unit tests cover the matrix (people / managed / anyone / owner_only as owner, non-owner, unverified / nobody incl. owner / no entry / null policy / pubkey normalization) plus the wire contract on both hops: kind:10100 content → RelayAgentInfo (Rust directory parse) and channel_add_policy → channelAddPolicy (fromRawRelayAgent), where a misnamed field would fail closed and silently hide agents app-wide. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Signed-off-by: Bryce Del Rio <bryce.delrio@gmail.com>
Why
Agent mention typeahead could surface old p-tag/channel-member identities and other users' concrete agents even when those identities were absent from the current user's managed Agents list. This produced multiple stale results for a persona such as Bumble.
Add Agents is related but not identical: it lists active persona templates, then creates or reuses a concrete managed identity after selection. Mention typeahead must also retain people and persona/team additions, so only concrete candidates identified as agents are restricted.
What
Risk Assessment
Medium-low — scoped to desktop mention autocomplete candidate selection. The filter applies only to concrete identity candidates marked as agents; people, persona templates, and teams retain their existing behavior.
References
Generated with Fizz