Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
getMentionableAgentPubkeys,
getSharedChannelIds,
isAgentIdentityInManagedList,
isAgentMentionReachable,
relayAgentIsSharedWithUser,
shouldHideAgentFromMentions,
} from "./agentAutocompleteEligibility.ts";
Expand Down Expand Up @@ -162,6 +163,39 @@ test("isAgentIdentityInManagedList: keeps people and only current managed agent
);
});

test("isAgentMentionReachable: keeps remote channel agents without restoring stale non-members", () => {
const managedAgentPubkeys = new Set([PUB_A]);

assert.equal(
isAgentMentionReachable(
{ isAgent: false, isMember: false, pubkey: PUB_D },
managedAgentPubkeys,
),
true,
);
assert.equal(
isAgentMentionReachable(
{ isAgent: true, isMember: false, pubkey: PUB_A },
managedAgentPubkeys,
),
true,
);
assert.equal(
isAgentMentionReachable(
{ isAgent: true, isMember: true, pubkey: PUB_B },
managedAgentPubkeys,
),
true,
);
assert.equal(
isAgentMentionReachable(
{ isAgent: true, isMember: false, pubkey: PUB_C },
managedAgentPubkeys,
),
false,
);
});

test("shouldHideAgentFromMentions: never hides non-agents", () => {
assert.equal(
shouldHideAgentFromMentions({
Expand Down
17 changes: 17 additions & 0 deletions desktop/src/features/agents/lib/agentAutocompleteEligibility.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,23 @@ export function isAgentIdentityInManagedList(
);
}

/**
* Keep agent identities that this client can actually address.
*
* Channel members must reach the policy-aware gate below even when their
* process is managed on another machine. Non-members stay limited to agents
* managed by this client.
*/
export function isAgentMentionReachable(
candidate: { isAgent?: boolean; isMember?: boolean; pubkey: string },
managedAgentPubkeys: ReadonlySet<string>,
) {
if (candidate.isAgent !== true) return true;

const normalized = normalizePubkey(candidate.pubkey);
return candidate.isMember === true || managedAgentPubkeys.has(normalized);
}

export function shouldHideAgentFromMentions({
isAgent,
isMember,
Expand Down
4 changes: 2 additions & 2 deletions desktop/src/features/messages/lib/useMentions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
coalesceAutocompleteCandidatesByKey,
getMentionableAgentPubkeys,
getSharedChannelIds,
isAgentIdentityInManagedList,
isAgentMentionReachable,
shouldHideAgentFromMentions,
} from "@/features/agents/lib/agentAutocompleteEligibility";
import {
Expand Down Expand Up @@ -246,7 +246,7 @@ export function useMentions(
if (isArchivedDiscovery(pubkey)) {
return;
}
if (!isAgentIdentityInManagedList(candidate, managedAgentPubkeys)) {
if (!isAgentMentionReachable(candidate, managedAgentPubkeys)) {
return;
}
if (
Expand Down
51 changes: 49 additions & 2 deletions desktop/tests/e2e/mentions.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ async function expectAgentProfileActionsHidden(
).toHaveCount(0);
}

test("@ trigger prioritizes channel members before runnable personas and other managed agents", async ({
test("@ trigger prioritizes remote channel agents before runnable personas and other managed agents", async ({
page,
}) => {
await installMockBridge(page, {
Expand All @@ -209,7 +209,7 @@ test("@ trigger prioritizes channel members before runnable personas and other m

const dropdown = autocomplete(page);
await expect(dropdown).toBeVisible();
await expect(dropdown.getByText("alice")).toHaveCount(0);
await expect(dropdown.getByText("alice")).toBeVisible();
await expect(dropdown.getByText("bob")).toBeVisible();
await expect(dropdown.getByText("Fizz")).toBeVisible();
await expect(dropdown.getByText("charlie")).toBeVisible();
Expand All @@ -226,6 +226,7 @@ test("@ trigger prioritizes channel members before runnable personas and other m
const suggestions = dropdown.locator("button");
const suggestionText = await suggestions.allInnerTexts();
const fizzIndex = suggestionText.findIndex((text) => text.includes("Fizz"));
const aliceIndex = suggestionText.findIndex((text) => text.includes("alice"));
const bobIndex = suggestionText.findIndex((text) => text.includes("bob"));
const charlieIndex = suggestionText.findIndex((text) =>
text.includes("charlie"),
Expand All @@ -234,13 +235,59 @@ test("@ trigger prioritizes channel members before runnable personas and other m
text.includes("outsider"),
);
expect(fizzIndex).toBeGreaterThanOrEqual(0);
expect(aliceIndex).toBeGreaterThanOrEqual(0);
expect(bobIndex).toBeGreaterThanOrEqual(0);
expect(charlieIndex).toBeGreaterThanOrEqual(0);
expect(outsiderIndex).toEqual(-1);
expect(aliceIndex).toBeLessThan(fizzIndex);
expect(bobIndex).toBeLessThan(fizzIndex);
expect(fizzIndex).toBeLessThan(charlieIndex);
});

test("mentioning a remote channel agent routes its pubkey without starting it locally", async ({
page,
}) => {
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");

const input = page.getByTestId("message-input");
await input.fill("@alice");

const dropdown = autocomplete(page);
await expect(dropdown.getByText("alice")).toBeVisible();
await input.press("Enter");
await page.keyboard.type(" can you help?");

const baselineStartCount = commandCount(
await readCommandLog(page),
"start_managed_agent",
);
await page.getByTestId("send-message").click();

await expect
.poll(() =>
page.evaluate(() => {
const events = (
window as Window & {
__BUZZ_E2E_SIGNED_EVENTS__?: Array<{
content: string;
tags: string[][];
}>;
}
).__BUZZ_E2E_SIGNED_EVENTS__;
return (
events?.find((event) => event.content.includes("can you help?"))
?.tags ?? []
);
}),
)
.toContainEqual(["p", TEST_IDENTITIES.alice.pubkey]);
expect(commandCount(await readCommandLog(page), "start_managed_agent")).toBe(
baselineStartCount,
);
});

test("thread autocomplete keeps multiple long names readable in a narrow panel", async ({
page,
}) => {
Expand Down