fix: prioritize owned agents in mention autocomplete

Co-authored-by: npub1shglkdhngx3hrnhf4gf8vhpqdrmeludctechdvpwd3988zzs7ncq2cmtxu <85d1fb36f341a371cee9aa12765c2068f79ff1b85e7176b02e6c4a738850f4f0@sprout-oss.stage.blox.sqprod.co>
Signed-off-by: npub1shglkdhngx3hrnhf4gf8vhpqdrmeludctechdvpwd3988zzs7ncq2cmtxu <85d1fb36f341a371cee9aa12765c2068f79ff1b85e7176b02e6c4a738850f4f0@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
npub1shglkdhngx3hrnhf4gf8vhpqdrmeludctechdvpwd3988zzs7ncq2cmtxu
2026-07-16 20:17:05 -07:00
parent 5d77fa5749
commit 9b211ae4d0
7 changed files with 121 additions and 13 deletions
@@ -62,6 +62,7 @@ export function flushMentionDebounce<T extends MentionCandidateWithUI>(opts: {
opts.candidates,
mention.query,
opts.activePersonaIds,
opts.currentPubkey,
);
if (ranked.length === 0) {
@@ -21,12 +21,49 @@ function rankedPubkeys(
candidates,
query = "brain",
activePersonaIds = new Set(),
currentPubkey,
) {
return rankMentionCandidates(candidates, query, activePersonaIds).map(
return rankMentionCandidates(
candidates,
query,
activePersonaIds,
currentPubkey,
).map(
(item) => item.candidate.pubkey ?? `persona:${item.candidate.personaId}`,
);
}
test("rankMentionCandidates: current user's agents rank before all other candidates", () => {
const currentPubkey = "a".repeat(64);
const ownedAgent = candidate({
displayName: "Brain",
isAgent: true,
ownerPubkey: currentPubkey.toUpperCase(),
pubkey: "7".repeat(64),
});
const channelMember = candidate({
displayName: "Brain",
isMember: true,
pubkey: CHANNEL_BRAIN_PUBKEY,
});
const remoteAgent = candidate({
displayName: "Brain",
isAgent: true,
ownerPubkey: "b".repeat(64),
pubkey: OTHER_BRAIN_PUBKEY,
});
assert.deepEqual(
rankedPubkeys(
[channelMember, remoteAgent, ownedAgent],
"brain",
new Set(),
currentPubkey,
),
["7".repeat(64), CHANNEL_BRAIN_PUBKEY, OTHER_BRAIN_PUBKEY],
);
});
test("rankMentionCandidates: channel members outrank runnable personas, people, and other agents", () => {
const persona = candidate({
kind: "persona",
@@ -9,11 +9,13 @@ export type MentionCandidateForRanking = {
personaName?: string | null;
pubkey?: string;
secondaryLabel?: string | null;
ownerPubkey?: string | null;
};
export type RankedMentionCandidate<T extends MentionCandidateForRanking> = {
candidate: T;
groupRank: number;
isOwnedByCurrentUser: boolean;
label: string;
order: number;
score: number;
@@ -55,8 +57,12 @@ export function rankMentionCandidates<T extends MentionCandidateForRanking>(
candidates: readonly T[],
query: string,
activePersonaIds: ReadonlySet<string> = new Set(),
currentPubkey?: string | null,
): RankedMentionCandidate<T>[] {
const lowerQuery = query.toLowerCase();
const normalizedCurrentPubkey = currentPubkey
? normalizePubkey(currentPubkey)
: null;
return candidates
.map((candidate, order) => {
@@ -91,12 +97,27 @@ export function rankMentionCandidates<T extends MentionCandidateForRanking>(
: null
: null;
const score = labelScore !== null ? labelScore : pubkeyScore;
const isOwnedByCurrentUser =
candidate.isAgent &&
normalizedCurrentPubkey !== null &&
candidate.ownerPubkey != null &&
normalizePubkey(candidate.ownerPubkey) === normalizedCurrentPubkey;
return { candidate, groupRank, label, order, score };
return {
candidate,
groupRank,
isOwnedByCurrentUser,
label,
order,
score,
};
})
.filter((item): item is RankedMentionCandidate<T> => item.score !== null)
.sort(
(a, b) =>
a.groupRank - b.groupRank || a.score - b.score || a.order - b.order,
Number(b.isOwnedByCurrentUser) - Number(a.isOwnedByCurrentUser) ||
a.groupRank - b.groupRank ||
a.score - b.score ||
a.order - b.order,
);
}
@@ -519,7 +519,6 @@ export function useMentions(
() => searchableNames.map((n) => n.toLowerCase()),
[searchableNames],
);
// --- Debounce infrastructure for updateMentionQuery ---
const debounceTimerRef = React.useRef<ReturnType<typeof setTimeout> | null>(
null,
@@ -552,6 +551,7 @@ export function useMentions(
mentionCandidatesWithTeams,
mentionQuery,
activePersonaIds,
currentPubkey,
)
.slice(
0,
@@ -133,5 +133,9 @@ final mentionCandidatesProvider = Provider.family
currentPubkey: currentPubkey,
);
return rankMentionCandidates(candidates, args.query);
return rankMentionCandidates(
candidates,
args.query,
currentPubkey: currentPubkey,
);
});
@@ -60,11 +60,13 @@ int? _scoreLabel(String label, String lowerQuery) {
/// stable original order.
List<MentionCandidate> rankMentionCandidates(
List<MentionCandidate> candidates,
String query,
) {
String query, {
String? currentPubkey,
}) {
final lowerQuery = query.toLowerCase();
final currentLower = currentPubkey?.toLowerCase();
final ranked = <(MentionCandidate, int, int, int)>[];
final ranked = <(MentionCandidate, bool, int, int, int)>[];
for (var order = 0; order < candidates.length; order++) {
final candidate = candidates[order];
@@ -89,15 +91,27 @@ List<MentionCandidate> rankMentionCandidates(
}
if (score == null) continue;
ranked.add((candidate, _groupRank(candidate), score, order));
final isOwnedByCurrentUser =
candidate.isAgent &&
currentLower != null &&
candidate.ownerPubkey?.toLowerCase() == currentLower;
ranked.add((
candidate,
isOwnedByCurrentUser,
_groupRank(candidate),
score,
order,
));
}
ranked.sort((a, b) {
final group = a.$2.compareTo(b.$2);
final owned = (b.$2 ? 1 : 0).compareTo(a.$2 ? 1 : 0);
if (owned != 0) return owned;
final group = a.$3.compareTo(b.$3);
if (group != 0) return group;
final score = a.$3.compareTo(b.$3);
final score = a.$4.compareTo(b.$4);
if (score != 0) return score;
return a.$4.compareTo(b.$4);
return a.$5.compareTo(b.$5);
});
return [for (final item in ranked) item.$1];
@@ -12,6 +12,7 @@ MentionCandidate candidate({
String? secondaryLabel,
bool isAgent = false,
bool isMember = false,
String? ownerPubkey,
String? pubkey,
}) {
return MentionCandidate(
@@ -20,20 +21,50 @@ MentionCandidate candidate({
secondaryLabel: secondaryLabel,
isAgent: isAgent,
isMember: isMember,
ownerPubkey: ownerPubkey,
);
}
List<String> rankedPubkeys(
List<MentionCandidate> candidates, [
String query = 'brain',
String? currentPubkey,
]) {
return [
for (final ranked in rankMentionCandidates(candidates, query))
for (final ranked in rankMentionCandidates(
candidates,
query,
currentPubkey: currentPubkey,
))
ranked.pubkey,
];
}
void main() {
test("current user's agents rank before all other candidates", () {
final currentPubkey = 'a' * 64;
final ownedAgent = candidate(
isAgent: true,
ownerPubkey: currentPubkey.toUpperCase(),
pubkey: '7' * 64,
);
final channelMember = candidate(isMember: true, pubkey: channelBrainPubkey);
final remoteAgent = candidate(
isAgent: true,
ownerPubkey: 'b' * 64,
pubkey: otherBrainPubkey,
);
expect(
rankedPubkeys(
[channelMember, remoteAgent, ownedAgent],
'brain',
currentPubkey,
),
['7' * 64, channelBrainPubkey, otherBrainPubkey],
);
});
test('channel members outrank people and other agents', () {
final remoteAgent = candidate(isAgent: true, pubkey: otherBrainPubkey);
final person = candidate(pubkey: '6' * 64);