fix(desktop): distinguish duplicate agent devices (#6337)

## Summary

- distinguish same-name owned agents by management provenance: `managed
here` for Desktop-managed identities and `managed elsewhere` for
same-owner relay identities
- show provenance only when same-name suggestions collide, alongside
each identity's short npub
- preserve exact-pubkey selection and keep unique-agent autocomplete
unchanged
- add a composed mock-bridge regression covering relay owner
propagation, rendered labels, keyboard/pointer selection, and outbound
mention pubkeys

## Testing

- focused mention suggestion mapping and label tests
- composed Desktop E2E passes for both same-name identities and
exact-pubkey routing
- causal mutation verified: replacing the relay candidate's
`ownerPubkey` with `null` makes the composed E2E fail on the `managed
elsewhere` assertion
- pre-push Desktop checks: Biome, TypeScript, file-size ratchet, and
5,103 Desktop tests

## Manual test

With two same-name owned agents visible in a channel, type `@<name>`.
Duplicate rows identify the identities as `agent · managed here` and
`agent · managed elsewhere`, include distinct short npubs, and selecting
either routes the mention to that row's exact pubkey.

---------

Signed-off-by: Wes <wesbillman@users.noreply.github.com>
Co-authored-by: Carl <c7ebe626f000404285d3686e1dc74cc07cc60a9754a150041ba132e14bd3e2ec@buzz.block.builderlab.xyz>
This commit is contained in:
Wes
2026-08-20 10:16:01 -06:00
committed by GitHub
parent e5d1dfef7b
commit 2e7583bf5a
6 changed files with 219 additions and 6 deletions

View File

@@ -0,0 +1,51 @@
import assert from "node:assert/strict";
import test from "node:test";
import { mapMentionCandidateToSuggestion } from "./mentionSuggestionMapping.ts";
const OWNER = "a".repeat(64);
function candidate(overrides = {}) {
return {
kind: "identity",
pubkey: "b".repeat(64),
isAgent: true,
isMember: true,
ownerPubkey: OWNER,
...overrides,
};
}
function suggestion(overrides = {}) {
return mapMentionCandidateToSuggestion({
candidate: candidate(overrides),
currentPubkey: OWNER,
label: "Carl",
});
}
test("labels Desktop-managed agent identities as managed here", () => {
assert.equal(
suggestion({ isManagedAgent: true }).agentProvenance,
"managed-here",
);
});
test("labels same-owner relay agent identities as managed elsewhere", () => {
assert.equal(suggestion().agentProvenance, "managed-elsewhere");
});
test("does not attribute another owner's agent to a device", () => {
assert.equal(
suggestion({ ownerPubkey: "c".repeat(64) }).agentProvenance,
undefined,
);
});
test("does not attribute people or personas to a device", () => {
assert.equal(suggestion({ isAgent: false }).agentProvenance, undefined);
assert.equal(
suggestion({ kind: "persona", pubkey: undefined }).agentProvenance,
undefined,
);
});

View File

@@ -13,6 +13,7 @@ export type MentionSuggestionCandidate = {
teamMembers?: TeamMentionMember[];
avatarUrl?: string | null;
isAgent: boolean;
isManagedAgent?: boolean;
isMember: boolean;
role?: ChannelRole | null;
ownerPubkey?: string | null;
@@ -52,6 +53,17 @@ export function mapMentionCandidateToSuggestion(opts: {
: null) ??
null,
isAgent: candidate.isAgent,
agentProvenance:
candidate.kind === "identity" && candidate.isAgent
? candidate.isManagedAgent
? "managed-here"
: candidate.ownerPubkey &&
currentPubkey &&
normalizePubkey(candidate.ownerPubkey) ===
normalizePubkey(currentPubkey)
? "managed-elsewhere"
: undefined
: undefined,
notInChannel:
candidate.kind !== "team" &&
channelType !== "dm" &&

View File

@@ -349,7 +349,7 @@ export function useMentions(
personaId:
managedAgentPersonaIdsByPubkey.get(pubkey) ??
(activePersonaById.has(pubkey) ? pubkey : undefined),
ownerPubkey: null,
ownerPubkey: agent.ownerPubkey,
isAgent: true,
});
}
@@ -515,13 +515,14 @@ export function useMentions(
searchableNamesLowerRef.current = searchableNamesLower;
}, [searchableNamesLower]);
React.useEffect(() => {
return () => {
React.useEffect(
() => () => {
if (debounceTimerRef.current !== null) {
clearTimeout(debounceTimerRef.current);
}
};
}, []);
},
[],
);
const matchingSuggestions = React.useMemo<MentionSuggestion[]>(() => {
if (mentionQuery === null) {

View File

@@ -0,0 +1,32 @@
import assert from "node:assert/strict";
import test from "node:test";
import { mentionAgentLabel } from "./MentionAutocomplete.tsx";
function suggestion(agentProvenance) {
return {
pubkey: "1".repeat(64),
displayName: "Carl",
isAgent: true,
agentProvenance,
};
}
test("duplicate owned agents show their management provenance", () => {
assert.equal(
mentionAgentLabel(suggestion("managed-here"), true),
"agent · managed here",
);
assert.equal(
mentionAgentLabel(suggestion("managed-elsewhere"), true),
"agent · managed elsewhere",
);
});
test("unique agents keep the compact generic label", () => {
assert.equal(mentionAgentLabel(suggestion("managed-here"), false), "agent");
});
test("agents without trustworthy provenance keep the generic label", () => {
assert.equal(mentionAgentLabel(suggestion(undefined), true), "agent");
});

View File

@@ -22,6 +22,7 @@ export type MentionSuggestion = {
displayName: string;
avatarUrl?: string | null;
isAgent?: boolean;
agentProvenance?: "managed-here" | "managed-elsewhere";
notInChannel?: boolean;
ownerLabel?: string | null;
role?: string | null;
@@ -35,6 +36,16 @@ type MentionAutocompleteProps = {
position?: "above" | "below";
};
export function mentionAgentLabel(
suggestion: MentionSuggestion,
hasNameCollision: boolean,
) {
if (!hasNameCollision || !suggestion.agentProvenance) return "agent";
return suggestion.agentProvenance === "managed-here"
? "agent · managed here"
: "agent · managed elsewhere";
}
export const MentionAutocomplete = React.memo(function MentionAutocomplete({
suggestions,
selectedIndex,
@@ -100,9 +111,9 @@ export const MentionAutocomplete = React.memo(function MentionAutocomplete({
(suggestion.personaId ? `persona-${suggestion.personaId}` : null) ??
(suggestion.teamId ? `team-${suggestion.teamId}` : null) ??
suggestion.displayName;
const agentLabel = "agent";
const hasNameCollision =
(nameCounts.get(suggestion.displayName.toLowerCase()) ?? 0) > 1;
const agentLabel = mentionAgentLabel(suggestion, hasNameCollision);
const collisionNpub =
hasNameCollision && suggestion.pubkey
? safeNpub(suggestion.pubkey)

View File

@@ -320,6 +320,112 @@ test("@ trigger prioritizes channel members before runnable personas and other m
expect(fizzIndex).toBeLessThan(charlieIndex);
});
test("duplicate owned agents preserve provenance and exact pubkey selection", async ({
page,
}) => {
const managedPubkey = IN_CHANNEL_MANAGED_AGENT_PUBKEY;
const relayPubkey = ALLOWLIST_RELAY_AGENT_PUBKEY;
await installMockBridge(page, {
managedAgents: [
{
pubkey: managedPubkey,
name: "carl",
status: "running",
channelNames: ["general"],
backend: {
type: "provider",
id: "mock",
config: {},
},
},
],
relayAgents: [
{
pubkey: relayPubkey,
ownerPubkey: MOCK_VIEWER_PUBKEY,
name: "carl",
respondTo: "owner-only",
channelNames: ["general"],
},
],
});
await page.goto("/");
await page.getByTestId("channel-general").click();
await page.evaluate(
async ({ channelId, pubkey }) => {
const invoke = window.__BUZZ_E2E_INVOKE_MOCK_COMMAND__;
if (!invoke) throw new Error("Mock bridge is not installed.");
await invoke("add_channel_members", {
channelId,
pubkeys: [pubkey],
role: "bot",
});
await window.__BUZZ_E2E_QUERY_CLIENT__?.invalidateQueries({
queryKey: ["channels"],
});
},
{ channelId: GENERAL_CHANNEL_ID, pubkey: relayPubkey },
);
const input = page.getByTestId("message-input");
await input.fill("@carl");
const dropdown = autocomplete(page);
const managedRow = dropdown.getByTestId(
`mention-suggestion-${managedPubkey}`,
);
const relayRow = dropdown.getByTestId(`mention-suggestion-${relayPubkey}`);
await expect(managedRow).toContainText("agent · managed here");
await expect(relayRow).toContainText("agent · managed elsewhere");
const collisionKeys = dropdown.getByTestId("mention-collision-npub");
await expect(collisionKeys).toHaveCount(2);
const fullNpubs = await collisionKeys.evaluateAll((nodes) =>
nodes.map((node) => node.getAttribute("title")),
);
expect(fullNpubs).toHaveLength(2);
expect(new Set(fullNpubs).size).toBe(2);
const initialRows = dropdown.locator("button");
const managedIndex = await initialRows.evaluateAll(
(buttons, pubkey) =>
buttons.findIndex(
(button) =>
button.getAttribute("data-testid") === `mention-suggestion-${pubkey}`,
),
managedPubkey,
);
expect(managedIndex).toBeGreaterThanOrEqual(0);
for (let index = 0; index < managedIndex; index += 1) {
await input.press("ArrowDown");
}
await input.press("Enter");
await page.keyboard.type("local");
await page.getByTestId("send-message").click();
await expect
.poll(() => readOutgoingMentionPubkeys(page, "@carl local"))
.toEqual([managedPubkey]);
await expect(input).toBeEmpty();
await input.fill("@carl");
const reopenedDropdown = autocomplete(page);
await expect(reopenedDropdown).toBeVisible();
await reopenedDropdown
.getByTestId(`mention-suggestion-${relayPubkey}`)
.click();
await page.keyboard.type("remote");
await page.getByTestId("send-message").click();
const sendWithoutInviting = page.getByRole("button", { name: "Do nothing" });
try {
await sendWithoutInviting.waitFor({ state: "visible", timeout: 2_000 });
await sendWithoutInviting.click();
} catch {
// In-channel selections send immediately without opening the prompt.
}
await expect
.poll(() => readOutgoingMentionPubkeys(page, "@carl remote"))
.toEqual([relayPubkey]);
});
test("relay-only shared agents emit an outbound mention tag when selected", async ({
page,
}) => {