Limit agent profile quick actions (#1425)

This commit is contained in:
klopez4212
2026-07-01 07:54:30 -07:00
committed by GitHub
parent 3e4e9dda94
commit 177fe5a3bf
3 changed files with 216 additions and 292 deletions
@@ -10,7 +10,11 @@ import {
useChannelsQuery,
useOpenDmMutation,
} from "@/features/channels/hooks";
import { useProfileQuery, useUserProfileQuery } from "@/features/profile/hooks";
import {
useProfileQuery,
useUserProfileQuery,
useUsersBatchQuery,
} from "@/features/profile/hooks";
import { channelMessagesKey } from "@/features/messages/lib/messageQueryKeys";
import {
useRelayAgentsQuery,
@@ -178,6 +182,9 @@ export function UserProfilePopover({
const openDmMutation = useOpenDmMutation();
const { isStarting: isStartingHuddle, startHuddle } = useHuddle();
const profileQuery = useUserProfileQuery(open ? pubkey : undefined);
const usersBatchQuery = useUsersBatchQuery(open ? [pubkey] : [], {
enabled: open,
});
const relayAgentsQuery = useRelayAgentsQuery({
enabled: open,
});
@@ -196,12 +203,24 @@ export function UserProfilePopover({
const managedAgent = managedAgentsQuery.data?.find(
(a) => a.pubkey === pubkey,
);
const isBotProfile = role === "bot" || Boolean(relayAgent || managedAgent);
const profile = profileQuery.data;
const normalizedPubkey = normalizePubkey(pubkey);
const isAgentByOaOwner = Boolean(
usersBatchQuery.data?.profiles[normalizedPubkey]?.isAgent,
);
const isAgentByProfileOwner = profile?.ownerPubkey != null;
const isBotProfile =
role === "bot" ||
Boolean(relayAgent || managedAgent) ||
isAgentByProfileOwner ||
isAgentByOaOwner;
const isAgentClassificationPending =
open &&
role !== "bot" &&
(relayAgentsQuery.isPending || managedAgentsQuery.isPending);
const profile = profileQuery.data;
(profileQuery.isPending ||
relayAgentsQuery.isPending ||
managedAgentsQuery.isPending ||
usersBatchQuery.isPending);
const displayName = profile?.displayName ?? truncatePubkey(pubkey);
// Owner signal mirrors UserProfilePanel: a declared NIP-OA owner whose agent
// runs elsewhere holds no local seckey, so key custody (`isOwner`) alone
@@ -217,6 +236,8 @@ export function UserProfilePopover({
currentPubkey !== undefined &&
currentPubkey.toLowerCase() === pubkey.toLowerCase();
const showProfileActions = currentPubkey !== undefined && !isSelf;
const showHumanProfileActions =
showProfileActions && !isBotProfile && !isAgentClassificationPending;
const selfProfileQuery = useProfileQuery(open && showProfileActions);
const isCurrentUserOwner =
currentPubkey !== undefined &&
@@ -314,9 +335,9 @@ export function UserProfilePopover({
const handleHuddle = React.useCallback(async () => {
if (
!showProfileActions ||
!showHumanProfileActions ||
pendingAction !== null ||
isStartingHuddle ||
isAgentClassificationPending
isStartingHuddle
) {
return;
}
@@ -327,7 +348,7 @@ export function UserProfilePopover({
try {
const dm = await openDmMutation.mutateAsync({ pubkeys: [pubkey] });
await goChannel(dm.id);
await startHuddle(dm.id, isBotProfile ? [pubkey] : []);
await startHuddle(dm.id, []);
await queryClient.invalidateQueries({ queryKey: channelsQueryKey });
if (isMountedRef.current) {
setOpen(false);
@@ -344,19 +365,24 @@ export function UserProfilePopover({
}, [
clearHoverTimer,
goChannel,
isAgentClassificationPending,
isBotProfile,
isStartingHuddle,
openDmMutation,
pendingAction,
pubkey,
queryClient,
showHumanProfileActions,
showProfileActions,
startHuddle,
]);
const handleWave = React.useCallback(async () => {
if (!showProfileActions || pendingAction !== null) return;
if (
!showProfileActions ||
!showHumanProfileActions ||
pendingAction !== null
) {
return;
}
clearHoverTimer();
setPendingAction("wave");
@@ -444,6 +470,7 @@ export function UserProfilePopover({
pubkey,
queryClient,
selfProfileQuery.data?.displayName,
showHumanProfileActions,
showProfileActions,
]);
@@ -594,34 +621,36 @@ export function UserProfilePopover({
) : null}
{showProfileActions ? (
<div className="flex gap-2">
<Button
aria-label="Wave"
className="buzz-wave-hover-trigger shrink-0 px-3 transition-transform duration-100 ease-out motion-reduce:transition-none motion-safe:active:scale-[0.97]"
data-testid={`user-profile-popover-wave-${pubkey}`}
disabled={
pendingAction !== null || openDmMutation.isPending
}
onClick={() => {
void handleWave();
}}
size="sm"
type="button"
variant="outline"
>
{pendingAction === "wave" ? (
<Spinner
aria-hidden="true"
className="h-3.5 w-3.5 border-2"
/>
) : (
<span
aria-hidden="true"
className="buzz-wave-hand text-sm leading-none"
>
👋
</span>
)}
</Button>
{showHumanProfileActions ? (
<Button
aria-label="Wave"
className="buzz-wave-hover-trigger shrink-0 px-3 transition-transform duration-100 ease-out motion-reduce:transition-none motion-safe:active:scale-[0.97]"
data-testid={`user-profile-popover-wave-${pubkey}`}
disabled={
pendingAction !== null || openDmMutation.isPending
}
onClick={() => {
void handleWave();
}}
size="sm"
type="button"
variant="outline"
>
{pendingAction === "wave" ? (
<Spinner
aria-hidden="true"
className="h-3.5 w-3.5 border-2"
/>
) : (
<span
aria-hidden="true"
className="buzz-wave-hand text-sm leading-none"
>
👋
</span>
)}
</Button>
) : null}
<Button
className="min-w-0 flex-1"
data-testid={`user-profile-popover-message-${pubkey}`}
@@ -645,32 +674,33 @@ export function UserProfilePopover({
)}
Message
</Button>
<Button
className="min-w-0 flex-1"
data-testid={`user-profile-popover-huddle-${pubkey}`}
disabled={
pendingAction !== null ||
openDmMutation.isPending ||
isStartingHuddle ||
isAgentClassificationPending
}
onClick={() => {
void handleHuddle();
}}
size="sm"
type="button"
variant="outline"
>
{pendingAction === "huddle" ? (
<Spinner
aria-hidden="true"
className="h-3.5 w-3.5 border-2"
/>
) : (
<Headphones />
)}
Huddle
</Button>
{showHumanProfileActions ? (
<Button
className="min-w-0 flex-1"
data-testid={`user-profile-popover-huddle-${pubkey}`}
disabled={
pendingAction !== null ||
openDmMutation.isPending ||
isStartingHuddle
}
onClick={() => {
void handleHuddle();
}}
size="sm"
type="button"
variant="outline"
>
{pendingAction === "huddle" ? (
<Spinner
aria-hidden="true"
className="h-3.5 w-3.5 border-2"
/>
) : (
<Headphones />
)}
Huddle
</Button>
) : null}
</div>
) : null}
</>
+106 -184
View File
@@ -26,8 +26,6 @@ const CASEY_PROFILE_PUBKEY =
"1111111111111111111111111111111111111111111111111111111111111111";
const PROFILE_ONLY_AGENT_PUBKEY =
"8f83d6b7f3d74f7d933ae3a54dd8c6cc85c7f98e531c16e5a827b953441a8d67";
const UNSEEDED_PROFILE_ONLY_AGENT_PUBKEY =
"7777777777777777777777777777777777777777777777777777777777777777";
const SYSTEM_MESSAGE_KIND = 40099;
/** Locator scoped to the mention autocomplete dropdown inside the composer. */
@@ -65,27 +63,6 @@ function commandCount(commands: string[], command: string) {
return commands.filter((entry) => entry === command).length;
}
async function readStartHuddleMemberPubkeys(
page: import("@playwright/test").Page,
) {
const commandLog = await readCommandPayloadLog(page);
return commandLog.flatMap((entry) => {
if (entry.command !== "start_huddle") {
return [];
}
const payload =
entry.payload && typeof entry.payload === "object"
? (entry.payload as {
memberPubkeys?: unknown;
member_pubkeys?: unknown;
})
: null;
const memberPubkeys = payload?.memberPubkeys ?? payload?.member_pubkeys;
return Array.isArray(memberPubkeys) ? memberPubkeys.map(String) : [];
});
}
async function emitMockMessage(
page: import("@playwright/test").Page,
channelName: string,
@@ -168,6 +145,21 @@ async function waitForTimelineSettled(page: import("@playwright/test").Page) {
await expect(page.locator("[data-render-pending]")).toHaveCount(0);
}
async function expectAgentProfileMessageOnly(
profilePopover: import("@playwright/test").Locator,
pubkey: string,
) {
await expect(
profilePopover.getByTestId(`user-profile-popover-message-${pubkey}`),
).toBeVisible();
await expect(
profilePopover.getByTestId(`user-profile-popover-wave-${pubkey}`),
).toHaveCount(0);
await expect(
profilePopover.getByTestId(`user-profile-popover-huddle-${pubkey}`),
).toHaveCount(0);
}
test("@ trigger shows unified autocomplete with agents first", async ({
page,
}) => {
@@ -916,9 +908,7 @@ test("system add and remove rows use agent mention styling for managed agents",
).toHaveText("portal");
});
test("system agent profile huddle passes profile-only bot pubkey", async ({
page,
}) => {
test("system agent profile only exposes message action", async ({ page }) => {
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
@@ -960,18 +950,13 @@ test("system agent profile huddle passes profile-only bot pubkey", async ({
'[data-testid="user-profile-popover"][data-state="open"]',
);
await expect(profilePopover).toBeVisible();
await profilePopover
.getByTestId(`user-profile-popover-huddle-${PROFILE_ONLY_AGENT_PUBKEY}`)
.click();
await expect
.poll(() => readStartHuddleMemberPubkeys(page))
.toEqual(expect.arrayContaining([PROFILE_ONLY_AGENT_PUBKEY]));
await expectAgentProfileMessageOnly(
profilePopover,
PROFILE_ONLY_AGENT_PUBKEY,
);
});
test("system agent avatar huddle passes profile-only bot pubkey", async ({
page,
}) => {
test("system agent avatar only exposes message action", async ({ page }) => {
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
@@ -1006,13 +991,48 @@ test("system agent avatar huddle passes profile-only bot pubkey", async ({
'[data-testid="user-profile-popover"][data-state="open"]',
);
await expect(profilePopover).toBeVisible();
await profilePopover
.getByTestId(`user-profile-popover-huddle-${PROFILE_ONLY_AGENT_PUBKEY}`)
.click();
await expectAgentProfileMessageOnly(
profilePopover,
PROFILE_ONLY_AGENT_PUBKEY,
);
});
await expect
.poll(() => readStartHuddleMemberPubkeys(page))
.toEqual(expect.arrayContaining([PROFILE_ONLY_AGENT_PUBKEY]));
test("profile-only agent author popover only exposes message action", async ({
page,
}) => {
await installMockBridge(page, {
searchProfiles: [
{
pubkey: PROFILE_ONLY_AGENT_PUBKEY,
displayName: "mira",
isAgent: true,
},
],
});
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
await waitForMockLiveSubscription(page, "general");
await emitMockMessage(page, "general", "Mira status update.", {
pubkey: PROFILE_ONLY_AGENT_PUBKEY,
});
await waitForTimelineSettled(page);
const messageRow = page
.getByTestId("message-row")
.filter({ hasText: "Mira status update." })
.first();
await messageRow.locator("button").first().hover();
const profilePopover = page.locator(
'[data-testid="user-profile-popover"][data-state="open"]',
);
await expect(profilePopover).toBeVisible();
await expectAgentProfileMessageOnly(
profilePopover,
PROFILE_ONLY_AGENT_PUBKEY,
);
});
test("system member-joined rows render the joined person as a mention chip", async ({
@@ -1315,7 +1335,7 @@ test("hovering avatar opens popover, clicking opens profile panel", async ({
await expect(page.getByTestId("user-profile-panel")).toBeVisible();
});
test("bot profile huddle passes the bot pubkey", async ({ page }) => {
test("bot profile only exposes message action", async ({ page }) => {
await page.goto("/");
await page.getByTestId("channel-agents").click();
await expect(page.getByTestId("chat-title")).toHaveText("agents");
@@ -1331,18 +1351,13 @@ test("bot profile huddle passes the bot pubkey", async ({ page }) => {
);
await expect(profilePopover).toBeVisible();
await expect(profilePopover.getByText("Codex")).toBeVisible();
await profilePopover
.getByTestId(
`user-profile-popover-huddle-${TEST_IDENTITIES.charlie.pubkey}`,
)
.click();
await expect
.poll(() => readStartHuddleMemberPubkeys(page))
.toEqual(expect.arrayContaining([TEST_IDENTITIES.charlie.pubkey]));
await expectAgentProfileMessageOnly(
profilePopover,
TEST_IDENTITIES.charlie.pubkey,
);
});
test("agent mention profile huddle passes the bot pubkey", async ({ page }) => {
test("agent mention profile only exposes message action", async ({ page }) => {
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
@@ -1364,39 +1379,57 @@ test("agent mention profile huddle passes the bot pubkey", async ({ page }) => {
'[data-testid="user-profile-popover"][data-state="open"]',
);
await expect(profilePopover).toBeVisible();
await profilePopover
.getByTestId(
`user-profile-popover-huddle-${TEST_IDENTITIES.charlie.pubkey}`,
)
.click();
await expect
.poll(() => readStartHuddleMemberPubkeys(page))
.toEqual(expect.arrayContaining([TEST_IDENTITIES.charlie.pubkey]));
await expectAgentProfileMessageOnly(
profilePopover,
TEST_IDENTITIES.charlie.pubkey,
);
});
test("profile popover wave sends a direct message", async ({ page }) => {
test("profile popover wave sends a direct message for a human profile", async ({
page,
}) => {
await installMockBridge(page, { sendMessageDelayMs: 2_500 });
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
await waitForMockLiveSubscription(page, "general");
const aliceMessage = page
await emitMockMessage(page, "general", "Bob says hello.", {
pubkey: TEST_IDENTITIES.bob.pubkey,
});
await waitForTimelineSettled(page);
const bobMessage = page
.getByTestId("message-row")
.filter({ hasText: "Hey team — checking in." })
.filter({ hasText: "Bob says hello." })
.first();
await aliceMessage.locator("button").first().hover();
await bobMessage.locator("button").first().hover();
const profilePopover = page.locator(
'[data-testid="user-profile-popover"][data-state="open"]',
);
await expect(profilePopover).toBeVisible();
await expect(
profilePopover.getByTestId(
`user-profile-popover-message-${TEST_IDENTITIES.bob.pubkey}`,
),
).toBeVisible();
await expect(
profilePopover.getByTestId(
`user-profile-popover-huddle-${TEST_IDENTITIES.bob.pubkey}`,
),
).toBeVisible();
await expect(
profilePopover.getByTestId(
`user-profile-popover-wave-${TEST_IDENTITIES.bob.pubkey}`,
),
).toBeVisible();
await profilePopover
.getByTestId(`user-profile-popover-wave-${TEST_IDENTITIES.alice.pubkey}`)
.getByTestId(`user-profile-popover-wave-${TEST_IDENTITIES.bob.pubkey}`)
.click();
await expect(page.getByTestId("chat-title")).toHaveText("alice-tyler");
await expect(page.getByTestId("chat-title")).toHaveText("bob-tyler");
const waveAttachment = page.getByTestId("message-wave-attachment");
await expect(waveAttachment).toBeVisible({ timeout: 1_500 });
await expect(page.getByText("Sending")).toHaveCount(0, { timeout: 4_000 });
@@ -1431,108 +1464,7 @@ test("profile popover wave sends a direct message", async ({ page }) => {
);
});
test("wave attachment huddle passes the bot DM pubkey", async ({ page }) => {
await page.goto("/");
await page.getByTestId("channel-agents").click();
await expect(page.getByTestId("chat-title")).toHaveText("agents");
const charlieMessage = page
.getByTestId("message-row")
.filter({ hasText: "Indexing the channel catalog now." })
.first();
await charlieMessage.locator("button").first().hover();
const profilePopover = page.locator(
'[data-testid="user-profile-popover"][data-state="open"]',
);
await expect(profilePopover).toBeVisible();
await expect(profilePopover.getByText("Codex")).toBeVisible();
await profilePopover
.getByTestId(`user-profile-popover-wave-${TEST_IDENTITIES.charlie.pubkey}`)
.click();
await expect(page.getByTestId("message-wave-attachment")).toBeVisible();
await page
.getByTestId("message-wave-attachment")
.getByRole("button", { name: "Start huddle" })
.click();
await expect
.poll(() => readStartHuddleMemberPubkeys(page))
.toEqual(expect.arrayContaining([TEST_IDENTITIES.charlie.pubkey]));
});
test("wave attachment huddle waits for placeholder profile-only bot data", async ({
page,
}) => {
await installMockBridge(page, {
searchProfiles: [
{
pubkey: UNSEEDED_PROFILE_ONLY_AGENT_PUBKEY,
displayName: "nova",
ownerPubkey: TEST_IDENTITIES.tyler.pubkey,
isAgent: true,
},
],
usersBatchDelayMs: 2_000,
});
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
await waitForMockLiveSubscription(page, "general", SYSTEM_MESSAGE_KIND);
await page.evaluate(
({ kind, targetPubkey }) => {
window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({
channelName: "general",
content: JSON.stringify({
type: "member_joined",
actor: targetPubkey,
target: targetPubkey,
}),
kind,
});
},
{
kind: SYSTEM_MESSAGE_KIND,
targetPubkey: UNSEEDED_PROFILE_ONLY_AGENT_PUBKEY,
},
);
await waitForTimelineSettled(page);
const joinedRow = page
.getByTestId("system-message-row")
.filter({ hasText: "joined the channel" });
const agentChip = joinedRow.locator("[data-mention]", {
hasText: "77777777…7777",
});
await expect(agentChip).toBeVisible({ timeout: 5_000 });
await agentChip.hover();
const profilePopover = page.locator(
'[data-testid="user-profile-popover"][data-state="open"]',
);
await expect(profilePopover).toBeVisible();
await profilePopover
.getByTestId(
`user-profile-popover-wave-${UNSEEDED_PROFILE_ONLY_AGENT_PUBKEY}`,
)
.click();
const startHuddleButton = page
.getByTestId("message-wave-attachment")
.getByRole("button", { name: "Start huddle" });
await expect(startHuddleButton).toBeDisabled();
await expect(startHuddleButton).toBeEnabled({ timeout: 5_000 });
await startHuddleButton.click();
await expect
.poll(() => readStartHuddleMemberPubkeys(page))
.toEqual(expect.arrayContaining([UNSEEDED_PROFILE_ONLY_AGENT_PUBKEY]));
});
test("wave attachment huddle waits for delayed bot DM pubkey", async ({
test("delayed agent profile keeps wave and huddle hidden while classifying", async ({
page,
}) => {
await installMockBridge(page, {
@@ -1572,18 +1504,8 @@ test("wave attachment huddle waits for delayed bot DM pubkey", async ({
'[data-testid="user-profile-popover"][data-state="open"]',
);
await expect(profilePopover).toBeVisible();
await profilePopover
.getByTestId(`user-profile-popover-wave-${DELAYED_RELAY_AGENT_PUBKEY}`)
.click();
const startHuddleButton = page
.getByTestId("message-wave-attachment")
.getByRole("button", { name: "Start huddle" });
await expect(startHuddleButton).toBeDisabled();
await expect(startHuddleButton).toBeEnabled({ timeout: 7_000 });
await startHuddleButton.click();
await expect
.poll(() => readStartHuddleMemberPubkeys(page))
.toEqual(expect.arrayContaining([DELAYED_RELAY_AGENT_PUBKEY]));
await expectAgentProfileMessageOnly(
profilePopover,
DELAYED_RELAY_AGENT_PUBKEY,
);
});
+16 -44
View File
@@ -83,42 +83,6 @@ async function selectHomeInboxFilter(
await page.getByRole("menuitemradio", { name: label }).click();
}
async function readCommandPayloadLog(page: import("@playwright/test").Page) {
return page.evaluate(() => {
return (
(
window as Window & {
__BUZZ_E2E_COMMAND_LOG__?: Array<{
command: string;
payload: unknown;
}>;
}
).__BUZZ_E2E_COMMAND_LOG__ ?? []
);
});
}
async function readStartHuddleMemberPubkeys(
page: import("@playwright/test").Page,
) {
const commandLog = await readCommandPayloadLog(page);
return commandLog.flatMap((entry) => {
if (entry.command !== "start_huddle") {
return [];
}
const payload =
entry.payload && typeof entry.payload === "object"
? (entry.payload as {
memberPubkeys?: unknown;
member_pubkeys?: unknown;
})
: null;
const memberPubkeys = payload?.memberPubkeys ?? payload?.member_pubkeys;
return Array.isArray(memberPubkeys) ? memberPubkeys.map(String) : [];
});
}
test.beforeEach(async ({ page }) => {
await installMockBridge(page);
});
@@ -230,7 +194,7 @@ test("inbox feed shows channel and agent activity sections", async ({
);
});
test("inbox agent hover huddle passes the agent pubkey", async ({ page }) => {
test("inbox agent hover only exposes message action", async ({ page }) => {
await page.goto("/");
await selectHomeInboxFilter(page, "Agents");
@@ -244,13 +208,21 @@ test("inbox agent hover huddle passes the agent pubkey", async ({ page }) => {
'[data-testid="user-profile-popover"][data-state="open"]',
);
await expect(profilePopover).toBeVisible();
await profilePopover
.getByTestId(`user-profile-popover-huddle-${DEFAULT_AGENT_ACTIVITY_PUBKEY}`)
.click();
await expect
.poll(() => readStartHuddleMemberPubkeys(page))
.toEqual(expect.arrayContaining([DEFAULT_AGENT_ACTIVITY_PUBKEY]));
await expect(
profilePopover.getByTestId(
`user-profile-popover-message-${DEFAULT_AGENT_ACTIVITY_PUBKEY}`,
),
).toBeVisible();
await expect(
profilePopover.getByTestId(
`user-profile-popover-wave-${DEFAULT_AGENT_ACTIVITY_PUBKEY}`,
),
).toHaveCount(0);
await expect(
profilePopover.getByTestId(
`user-profile-popover-huddle-${DEFAULT_AGENT_ACTIVITY_PUBKEY}`,
),
).toHaveCount(0);
});
test("opens a mocked forum activity item from the inbox feed", async ({