feat(desktop): rank next mention snapshot by admitted choices

Co-authored-by: Larry <627498bd4bd1f281a16431e3c6cce3b5c25b6692798c78672298aefbf2f8f8b5@buzz.block.builderlab.xyz>
Signed-off-by: Logan Johnson <loganj@squareup.com>
This commit is contained in:
Logan Johnson
2026-09-08 13:21:48 -04:00
parent 479bbe0f17
commit 1d126d4d65
7 changed files with 234 additions and 2 deletions

View File

@@ -1,3 +1,7 @@
import {
getMentionSelectionHistory,
resetMentionSelectionHistory,
} from "../messages/lib/mentionSelectionHistory.ts";
// Admission against existing root query evidence, without membership freshness production.
// Real mention and picker hooks; Tauri policy/classification are fixture evidence.
import assert from "node:assert/strict";
@@ -246,6 +250,7 @@ async function setup(overrides = {}) {
}
afterEach(async () => {
if (root) await act(async () => root.unmount());
resetMentionSelectionHistory();
client?.clear();
document.body.replaceChildren();
});
@@ -326,6 +331,7 @@ test("retained explicit pin rejects latest policy denial without draft effects",
assert.equal(rows().length, 1, "denial does not move the displayed row");
await act(async () => oldPin(row));
assert.deepEqual(effects, []);
assert.deepEqual(getMentionSelectionHistory(VIEWER, CHANNEL), []);
assert.deepEqual(mention.knownNames, []);
});
@@ -349,6 +355,7 @@ for (const returnToOrigin of [false, true]) {
edit = oldInsert(row, 1);
});
assert.deepEqual(effects, []);
assert.deepEqual(getMentionSelectionHistory(VIEWER, CHANNEL), []);
assert.equal(edit.insertText, "");
assert.deepEqual(mention.knownNames, []);
});
@@ -592,6 +599,9 @@ test("background membership/search updates leave visible same-name rows and Tab
mention.getDraftMentionRefs(edit.insertText)[0].pubkey,
selected.pubkey,
);
assert.deepEqual(getMentionSelectionHistory(VIEWER, CHANNEL), [
selected.pubkey,
]);
await act(async () => mention.updateMentionQuery("@Scou", 5));
await settle();
assert.equal(mention.suggestions.length, 3);
@@ -719,6 +729,7 @@ for (const condition of ["denied", "missing", "failed", "cold-failed"]) {
assert.equal(outcome.suggestion, undefined);
}
assert.deepEqual(effects, []);
assert.deepEqual(getMentionSelectionHistory(VIEWER, CHANNEL), []);
assert.deepEqual(mention.knownNames, []);
});
}

View File

@@ -1,3 +1,4 @@
import { resetMentionSelectionHistory } from "@/features/messages/lib/mentionSelectionHistory";
import { useEffect, useRef, useState } from "react";
import { isTauri } from "@tauri-apps/api/core";
import { isMacPlatform } from "@/shared/lib/platform";
@@ -58,6 +59,7 @@ async function resetCommunityState({
resetAvatarState: boolean;
}): Promise<void> {
relayClient.disconnect();
resetMentionSelectionHistory();
await resetNavigationDeepLinkDrain();
resetRateLimitGate();
clearAllDrafts();

View File

@@ -1,10 +1,17 @@
import assert from "node:assert/strict";
import test from "node:test";
import { buildMentionCandidates } from "./buildMentionCandidates.ts";
import { rankMentionCandidates } from "./mentionRanking.ts";
import { getMentionMemberPubkeys } from "./mentionMemberPubkeys.ts";
import {
getMentionSelectionHistory,
rememberMentionSelection,
resetMentionSelectionHistory,
} from "./mentionSelectionHistory.ts";
const A = "a".repeat(64),
B = "b".repeat(64),
C = "c".repeat(64),
VIEWER = "f".repeat(64);
function input(overrides = {}) {
return {
@@ -161,3 +168,99 @@ test("relay presence and verified ownership are independent of local stopped sta
);
assert.equal(stale.presence, "unknown");
});
const candidate = (pubkey, extra = {}) => ({
kind: "identity",
pubkey,
displayName: "Scout",
isAgent: true,
isMember: true,
action: "mention",
...extra,
});
test("collision order is explicit recent, owned, Online > Away > unknown/Offline, then exact key", () => {
const rows = [
candidate(A, { presence: "online" }),
candidate(B, { isOwned: true, presence: "offline" }),
candidate(C, { presence: "away" }),
];
const keys = (items, history = []) =>
rankMentionCandidates(items, "", new Set(), history).map(
(x) => x.candidate.pubkey,
);
assert.deepEqual(keys(rows), [B, A, C]);
assert.deepEqual(keys(rows, [C]), [C, B, A]);
for (const permutation of [
rows,
[...rows].reverse(),
[rows[1], rows[0], rows[2]],
[rows[2], rows[0], rows[1]],
])
assert.deepEqual(keys(permutation), [B, A, C]);
assert.deepEqual(
keys([
candidate(B, { presence: "unknown" }),
candidate(A, { presence: "offline" }),
]),
[A, B],
);
assert.deepEqual(
keys([
candidate(B, { action: "unavailable" }),
candidate(A, { isMember: false, action: "invite" }),
]),
[A, B],
);
});
test("unrelated text/kind slots do not participate in conditional-comparator cycles", () => {
const rows = [
candidate(B),
candidate(C, { displayName: "Someone", isAgent: false }),
candidate(A),
];
assert.deepEqual(
rankMentionCandidates(rows, "").map((x) => x.candidate.pubkey),
[A, C, B],
);
});
test("explicit history is channel/user scoped, bounded and cleared at community reset", () => {
resetMentionSelectionHistory();
rememberMentionSelection(VIEWER, "room", A);
rememberMentionSelection(VIEWER, "room", B);
assert.deepEqual(getMentionSelectionHistory(VIEWER, "room"), [B, A]);
assert.deepEqual(getMentionSelectionHistory(A, "room"), []);
assert.deepEqual(getMentionSelectionHistory(VIEWER, "other"), []);
resetMentionSelectionHistory();
assert.deepEqual(getMentionSelectionHistory(VIEWER, "room"), []);
});
test("history normalizes keys and evicts excess entries and scopes", () => {
resetMentionSelectionHistory();
for (let i = 0; i < 55; i++)
rememberMentionSelection(VIEWER, "room", i.toString(16).padStart(64, "0"));
assert.equal(getMentionSelectionHistory(VIEWER, "room").length, 50);
rememberMentionSelection(VIEWER.toUpperCase(), "room", A.toUpperCase());
assert.equal(getMentionSelectionHistory(VIEWER, "room")[0], A);
for (let i = 0; i < 100; i++)
rememberMentionSelection(VIEWER, `room-${i}`, A);
assert.deepEqual(getMentionSelectionHistory(VIEWER, "room"), []);
resetMentionSelectionHistory();
});
test("ranking before the visible slice preserves persona and team slots", () => {
const rows = [
candidate(B, { isMember: false, personaId: "active" }),
candidate(undefined, {
kind: "persona",
isMember: false,
personaId: "active",
}),
candidate(undefined, { kind: "team", isMember: false }),
candidate(A, { isMember: false, personaId: "active", isOwned: true }),
];
const rank = (history = []) =>
rankMentionCandidates(rows, "Scout", new Set(["active"]), history)
.slice(0, 3)
.map((item) => item.candidate);
assert.deepEqual(rank(), [rows[3], rows[1], rows[2]]);
assert.deepEqual(rank([B]), [rows[0], rows[1], rows[2]]);
});

View File

@@ -1,9 +1,15 @@
import { isMentionActionable, type MentionAction } from "./mentionPresentation";
import {
isMentionActionable,
type MentionAction,
type MentionPresence,
} from "./mentionPresentation";
import { normalizePubkey, truncatePubkey } from "@/shared/lib/pubkey";
export type MentionCandidateForRanking = {
displayName: string | null;
action?: MentionAction;
isOwned?: boolean;
presence?: MentionPresence;
isAgent: boolean;
isActiveAgent?: boolean;
isMember: boolean;
@@ -116,10 +122,11 @@ export function rankMentionCandidates<T extends MentionCandidateForRanking>(
candidates: readonly T[],
query: string,
activePersonaIds: ReadonlySet<string> = new Set(),
recentExplicitPubkeys: readonly string[] = [],
): RankedMentionCandidate<T>[] {
const lowerQuery = query.toLowerCase();
return candidates
const ranked = candidates
.map((candidate, order) => {
const pubkeyLower = candidate.pubkey
? normalizePubkey(candidate.pubkey)
@@ -160,4 +167,43 @@ export function rankMentionCandidates<T extends MentionCandidateForRanking>(
(a, b) =>
a.groupRank - b.groupRank || a.score - b.score || a.order - b.order,
);
// Reorder only comparable same-name agent slots. A conditional pairwise
// comparator against unrelated rows creates cycles (A<B, B<C, C<A).
const groups = new Map<string, number[]>();
ranked.forEach((item, index) => {
if (
item.candidate.kind !== "identity" ||
!item.candidate.pubkey ||
!item.candidate.isAgent ||
!isMentionActionable(item.candidate)
)
return;
const group = `${item.groupRank}:${item.score}:${item.label.trim().toLowerCase()}`;
groups.set(group, [...(groups.get(group) ?? []), index]);
});
const recent = new Map(
recentExplicitPubkeys.map((key, index) => [normalizePubkey(key), index]),
);
const presenceRank = (value?: MentionPresence) =>
value === "online" ? 0 : value === "away" ? 1 : 2;
for (const indexes of groups.values()) {
if (indexes.length < 2) continue;
const sorted = indexes
.map((index) => ranked[index])
.sort((a, b) => {
const left = a.candidate,
right = b.candidate;
return (
(recent.get(normalizePubkey(left.pubkey ?? "")) ?? Infinity) -
(recent.get(normalizePubkey(right.pubkey ?? "")) ?? Infinity) ||
Number(right.isOwned === true) - Number(left.isOwned === true) ||
presenceRank(left.presence) - presenceRank(right.presence) ||
(left.pubkey ?? "").localeCompare(right.pubkey ?? "")
);
});
indexes.forEach((index, i) => {
ranked[index] = sorted[i];
});
}
return ranked;
}

View File

@@ -0,0 +1,44 @@
import { normalizePubkey } from "@/shared/lib/pubkey";
// Community lifetime is owned by resetCommunityState. Keys additionally isolate
// identities/channels; this is selection intent, never authorization or pins.
const history = new Map<string, string[]>();
const scopeKey = (viewer: string, channel: string) =>
`${normalizePubkey(viewer)}:${channel}`;
/** Clear selection intent when the community changes. */
export function resetMentionSelectionHistory() {
history.clear();
}
/** Read recent successful insertions for this viewer and channel only. */
export function getMentionSelectionHistory(
viewer: string | null,
channel: string | null,
): readonly string[] {
return viewer && channel
? (history.get(scopeKey(viewer, channel)) ?? [])
: [];
}
/** Record a successful exact-key insertion without creating permission or a pin. */
export function rememberMentionSelection(
viewer: string | null,
channel: string | null,
pubkey: string,
) {
if (!viewer || !channel) return;
const scope = scopeKey(viewer, channel);
const key = normalizePubkey(pubkey);
const previous = history.get(scope) ?? [];
history.delete(scope);
history.set(
scope,
[key, ...previous.filter((item) => item !== key)].slice(0, 50),
);
while (history.size > 100) {
const oldest = history.keys().next().value;
if (oldest === undefined) break;
history.delete(oldest);
}
}

View File

@@ -3,6 +3,10 @@ import {
markMentionCollisions,
} from "./mentionPresentation";
import { useCanAddChannelMembers } from "@/features/channels/useCanAddChannelMembers";
import {
getMentionSelectionHistory,
rememberMentionSelection,
} from "./mentionSelectionHistory";
import { useMentionEvidence } from "./useMentionEvidence";
import * as React from "react";
import {
@@ -420,6 +424,7 @@ export function useMentions(
mentionCandidatesWithTeams,
mentionQuery,
activePersonaIds,
getMentionSelectionHistory(currentPubkey, channelId),
)
.slice(0, MENTION_SUGGESTION_LIMIT)
.map(({ candidate, label }) => ({
@@ -438,6 +443,7 @@ export function useMentions(
activePersonaIds,
agentDirectoriesReady,
retryMention,
channelId,
currentPubkey,
mentionCandidatesWithTeams,
mentionQuery,
@@ -592,6 +598,8 @@ export function useMentions(
replaceToOffset: selectionEnd,
insertText: "",
};
if (suggestion.pubkey)
rememberMentionSelection(currentPubkey, channelId, suggestion.pubkey);
const [boundSuggestion] = selectedMentionLabels(
[suggestion],
mentionMapRef.current,
@@ -666,6 +674,8 @@ export function useMentions(
knownAgentPubkeys,
query,
suggestions,
currentPubkey,
channelId,
],
);
const registerMentionPubkey = React.useCallback(

View File

@@ -76,7 +76,12 @@ test("collision distinction, deliberate key choice, and exact publication", asyn
const rowIds = await page
.locator("[data-mention-suggestion-index]")
.evaluateAll((rows) => rows.map((row) => row.getAttribute("data-testid")));
expect(rowIds).toEqual([
`mention-suggestion-${A}`,
`mention-suggestion-${B}`,
]);
const first = rowIds[1]?.endsWith(A) ? A : B;
const second = first === A ? B : A;
await input.press("ArrowDown");
// Membership/presence changes affect the next request, not the visible order.
await page.evaluate(() => {
@@ -122,6 +127,17 @@ test("collision distinction, deliberate key choice, and exact publication", asyn
),
)
.toEqual([[first]]);
await input.fill("@Scout");
await expect
.poll(() =>
page
.locator("[data-mention-suggestion-index]")
.evaluateAll((rows) =>
rows.map((row) => row.getAttribute("data-testid")),
),
)
.toEqual([`mention-suggestion-${first}`, `mention-suggestion-${second}`]);
await capture(page, "next-open-ranking");
});
test("Escape discards delayed picker results across navigation", async ({