From 0e096416235e629b1325bb7894c0ce74bc1a3f0c Mon Sep 17 00:00:00 2001 From: npub1qvn3cujt28pg06ehlstrxyz6ayzp06t4uc7r566vxwwgrv24hglq9zju0n <03271c724b51c287eb37fc1633105ae90417e975e63c3a6b4c339c81b155ba3e@sprout-oss.stage.blox.sqprod.co> Date: Sat, 18 Jul 2026 10:55:35 -0700 Subject: [PATCH 1/2] feat(desktop): show agent owners in messages Co-authored-by: npub1qvn3cujt28pg06ehlstrxyz6ayzp06t4uc7r566vxwwgrv24hglq9zju0n <03271c724b51c287eb37fc1633105ae90417e975e63c3a6b4c339c81b155ba3e@sprout-oss.stage.blox.sqprod.co> Signed-off-by: npub1qvn3cujt28pg06ehlstrxyz6ayzp06t4uc7r566vxwwgrv24hglq9zju0n <03271c724b51c287eb37fc1633105ae90417e975e63c3a6b4c339c81b155ba3e@sprout-oss.stage.blox.sqprod.co> --- .../src/features/channels/ui/ChannelPane.tsx | 2 + .../features/channels/ui/ChannelPane.types.ts | 1 + .../features/channels/ui/ChannelScreen.tsx | 6 +++ .../channels/useMessageOwnerProfiles.ts | 22 ++++++++ desktop/src/features/home/lib/inbox.ts | 3 ++ .../src/features/home/lib/inboxViewHelpers.ts | 6 +++ desktop/src/features/home/ui/HomeView.tsx | 16 ++++++ .../src/features/home/ui/InboxMessageRow.tsx | 7 +++ .../messages/lib/formatTimelineMessages.ts | 11 ++++ desktop/src/features/messages/types.ts | 6 +++ .../messages/ui/MessageAgentOwner.tsx | 51 +++++++++++++++++++ .../src/features/messages/ui/MessageRow.tsx | 11 ++++ .../features/messages/ui/MessageTimeline.tsx | 3 ++ .../features/messages/ui/SystemMessageRow.tsx | 41 ++++++++++++++- .../messages/ui/TimelineMessageList.tsx | 8 +++ .../messages/useIndependentThreadPanel.ts | 3 ++ .../features/profile/lib/identity.test.mjs | 22 +++++++- desktop/tests/e2e/messaging.spec.ts | 47 ++++++++++++++++- 18 files changed, 262 insertions(+), 4 deletions(-) create mode 100644 desktop/src/features/channels/useMessageOwnerProfiles.ts create mode 100644 desktop/src/features/messages/ui/MessageAgentOwner.tsx diff --git a/desktop/src/features/channels/ui/ChannelPane.tsx b/desktop/src/features/channels/ui/ChannelPane.tsx index fea77e0ffe..10aa2d2a20 100644 --- a/desktop/src/features/channels/ui/ChannelPane.tsx +++ b/desktop/src/features/channels/ui/ChannelPane.tsx @@ -132,6 +132,7 @@ export const ChannelPane = React.memo(function ChannelPane({ unfollowThreadById, personaLookup, profiles, + ownerProfiles, openThreadHeadId, shouldShowThreadSkeleton, openAgentSessionChannelId, @@ -605,6 +606,7 @@ export const ChannelPane = React.memo(function ChannelPane({ isMessageUnreadById={isMessageUnreadById} personaLookup={personaLookup} profiles={profiles} + ownerProfiles={ownerProfiles} unfollowThreadById={unfollowThreadById} emptyDescription={ activeChannel?.channelType === "forum" diff --git a/desktop/src/features/channels/ui/ChannelPane.types.ts b/desktop/src/features/channels/ui/ChannelPane.types.ts index ccfd3124d6..5a27d85c4d 100644 --- a/desktop/src/features/channels/ui/ChannelPane.types.ts +++ b/desktop/src/features/channels/ui/ChannelPane.types.ts @@ -134,6 +134,7 @@ export type ChannelPaneProps = { ) => void; personaLookup?: Map; profiles?: UserProfileLookup; + ownerProfiles?: UserProfileLookup; openThreadHeadId: string | null; shouldShowThreadSkeleton: boolean; openAgentSessionChannelId: string | null; diff --git a/desktop/src/features/channels/ui/ChannelScreen.tsx b/desktop/src/features/channels/ui/ChannelScreen.tsx index 34692267ec..840d0b5d2b 100644 --- a/desktop/src/features/channels/ui/ChannelScreen.tsx +++ b/desktop/src/features/channels/ui/ChannelScreen.tsx @@ -5,6 +5,7 @@ import { useAppNavigation } from "@/app/navigation/useAppNavigation"; import { useActiveChannelHeader } from "@/features/channels/useActiveChannelHeader"; import { useChannelPaneHandlers } from "@/features/channels/useChannelPaneHandlers"; import { useMessageEventProfilePubkeys } from "@/features/channels/useMessageEventProfilePubkeys"; +import { useMessageOwnerProfiles } from "@/features/channels/useMessageOwnerProfiles"; import { useThreadTargetSync } from "@/features/channels/useThreadTargetSync"; import { useChannelMembersQuery, @@ -350,6 +351,7 @@ export function ChannelScreen({ profiles: messageProfilesQuery.data?.profiles, relayAgents, }); + const messageOwnerProfiles = useMessageOwnerProfiles(messageProfiles); // Agent set for ChannelPane's own consumers (DM huddle member resolution, // the agents list): the community-scoped baseline shared by every surface, // widened with channel-member roles and this screen's profile lookup. @@ -393,6 +395,7 @@ export function ChannelScreen({ personaLookup, respondToLookup, relaySelfPubkey, + messageOwnerProfiles, ), [ activeChannel, @@ -400,6 +403,7 @@ export function ChannelScreen({ currentProfile?.avatarUrl, currentPubkey, messageProfiles, + messageOwnerProfiles, personaLookup, relaySelfPubkey, respondToLookup, @@ -437,6 +441,7 @@ export function ChannelScreen({ currentPubkey, currentAvatarUrl: currentProfile?.avatarUrl ?? null, profiles: messageProfiles, + ownerProfiles: messageOwnerProfiles, members: channelMembers, personaLookup, respondToLookup, @@ -934,6 +939,7 @@ export function ChannelScreen({ profilePanelView={profilePanelView} personaLookup={personaLookup} profiles={messageProfiles} + ownerProfiles={messageOwnerProfiles} firstUnreadMessageId={firstUnreadMessageId} unreadCount={unreadCount} targetMessageId={mainTimelineTargetMessageId} diff --git a/desktop/src/features/channels/useMessageOwnerProfiles.ts b/desktop/src/features/channels/useMessageOwnerProfiles.ts new file mode 100644 index 0000000000..8e1fac6319 --- /dev/null +++ b/desktop/src/features/channels/useMessageOwnerProfiles.ts @@ -0,0 +1,22 @@ +import * as React from "react"; + +import { useUsersBatchQuery } from "@/features/profile/hooks"; +import type { UserProfileLookup } from "@/features/profile/lib/identity"; + +/** Fetches verified agent-owner profiles in one batch for message surfaces. */ +export function useMessageOwnerProfiles(profiles: UserProfileLookup) { + const ownerPubkeys = React.useMemo( + () => [ + ...new Set( + Object.values(profiles) + .map((profile) => profile.ownerPubkey) + .filter((pubkey): pubkey is string => Boolean(pubkey)), + ), + ], + [profiles], + ); + const ownerProfilesQuery = useUsersBatchQuery(ownerPubkeys, { + enabled: ownerPubkeys.length > 0, + }); + return ownerProfilesQuery.data?.profiles; +} diff --git a/desktop/src/features/home/lib/inbox.ts b/desktop/src/features/home/lib/inbox.ts index 5a486e8681..b4c05ef809 100644 --- a/desktop/src/features/home/lib/inbox.ts +++ b/desktop/src/features/home/lib/inbox.ts @@ -60,6 +60,9 @@ export type InboxTypeLabel = { export type InboxReply = { authorLabel: string; authorPubkey: string; + isAgent?: boolean; + ownerLabel?: string | null; + ownerPubkey?: string | null; avatarUrl: string | null; content: string; createdAt: number; diff --git a/desktop/src/features/home/lib/inboxViewHelpers.ts b/desktop/src/features/home/lib/inboxViewHelpers.ts index d46ad04cab..e87671b9fa 100644 --- a/desktop/src/features/home/lib/inboxViewHelpers.ts +++ b/desktop/src/features/home/lib/inboxViewHelpers.ts @@ -139,6 +139,9 @@ export function toInboxContextMessage( id: message.id, authorLabel: message.author, authorPubkey, + isAgent: message.isAgent, + ownerLabel: message.ownerLabel, + ownerPubkey: message.ownerPubkey, avatarUrl: message.avatarUrl ?? null, content: message.body, createdAt: message.createdAt, @@ -169,6 +172,9 @@ export function toTimelineMessage( return { id: message.id, author: message.authorLabel, + isAgent: message.isAgent, + ownerLabel: message.ownerLabel, + ownerPubkey: message.ownerPubkey, avatarUrl: message.avatarUrl, body: message.content, createdAt: message.createdAt, diff --git a/desktop/src/features/home/ui/HomeView.tsx b/desktop/src/features/home/ui/HomeView.tsx index df0b9094e1..c580434e10 100644 --- a/desktop/src/features/home/ui/HomeView.tsx +++ b/desktop/src/features/home/ui/HomeView.tsx @@ -333,6 +333,20 @@ export function HomeView({ enabled: feedProfilePubkeys.length > 0, }); const feedProfiles = feedProfilesQuery.data?.profiles; + const feedOwnerPubkeys = React.useMemo( + () => [ + ...new Set( + Object.values(feedProfiles ?? {}) + .map((profile) => profile.ownerPubkey) + .filter((pubkey): pubkey is string => Boolean(pubkey)), + ), + ], + [feedProfiles], + ); + const feedOwnerProfilesQuery = useUsersBatchQuery(feedOwnerPubkeys, { + enabled: feedOwnerPubkeys.length > 0, + }); + const feedOwnerProfiles = feedOwnerProfilesQuery.data?.profiles; // Agent set for the inbox list/detail bot badges: the community-scoped // baseline widened with this surface's profile lookup. const communityAgentPubkeys = useKnownAgentPubkeys(); @@ -477,6 +491,7 @@ export function HomeView({ undefined, undefined, relaySelfPubkey, + feedOwnerProfiles, ); return timelineMessages.map((message) => @@ -491,6 +506,7 @@ export function HomeView({ channelMessages, currentPubkey, feedProfiles, + feedOwnerProfiles, relaySelfPubkey, selectedChannel, selectedEventId, diff --git a/desktop/src/features/home/ui/InboxMessageRow.tsx b/desktop/src/features/home/ui/InboxMessageRow.tsx index acd3098086..10db413333 100644 --- a/desktop/src/features/home/ui/InboxMessageRow.tsx +++ b/desktop/src/features/home/ui/InboxMessageRow.tsx @@ -7,6 +7,7 @@ import { formatTimeWithoutDayPeriod } from "@/features/messages/lib/dateFormatte import type { TimelineMessage } from "@/features/messages/types"; import { getConfigNudgeAuthorPubkey } from "@/features/messages/ui/configNudgeAuthPubkey"; import { MessageActionBar } from "@/features/messages/ui/MessageActionBar"; +import { MessageAgentOwner } from "@/features/messages/ui/MessageAgentOwner"; import { MessageReactions } from "@/features/messages/ui/MessageReactions"; import { useReactionHandler } from "@/features/messages/ui/useReactionHandler"; import { useMessageEmoji } from "@/features/messages/lib/useMessageEmoji"; @@ -172,6 +173,12 @@ export function InboxMessageRow({ {message.authorLabel} + {message.isAgent ? ( + + ) : null}

{message.fullTimestampLabel}

diff --git a/desktop/src/features/messages/lib/formatTimelineMessages.ts b/desktop/src/features/messages/lib/formatTimelineMessages.ts index 6bbd1e8a12..640c12bb75 100644 --- a/desktop/src/features/messages/lib/formatTimelineMessages.ts +++ b/desktop/src/features/messages/lib/formatTimelineMessages.ts @@ -14,6 +14,7 @@ import { isBroadcastReply, } from "@/features/messages/lib/threading"; import { + formatOwnerLabel, resolveUserLabel, type UserProfileLookup, } from "@/features/profile/lib/identity"; @@ -193,6 +194,8 @@ export function formatTimelineMessages( respondToLookup?: Map, /** Active relay identity from NIP-11 `self`; absent or malformed fails closed to the signer. */ relaySelfPubkey?: string | null, + /** Profiles for verified agent owners, fetched in one batch by the surface. */ + ownerProfiles?: UserProfileLookup, ): TimelineMessage[] { const currentPubkeyLower = currentPubkey?.toLowerCase(); const roleByPubkey = new Map(); @@ -423,6 +426,9 @@ export function formatTimelineMessages( const thread = getThreadReference(event.tags); const edit = editsByTargetId.get(event.id); const role = roleByPubkey.get(authorPubkey.toLowerCase()); + const authorProfile = profiles?.[authorPubkey.toLowerCase()]; + const isAgent = role === "bot" || authorProfile?.isAgent === true; + const ownerPubkey = isAgent ? (authorProfile?.ownerPubkey ?? null) : null; return { id: event.id, renderKey: event.localKey ?? event.id, @@ -430,6 +436,11 @@ export function formatTimelineMessages( pubkey: authorPubkey, signerPubkey: normalizePubkey(event.pubkey), author, + isAgent, + ownerPubkey, + ownerLabel: isAgent + ? formatOwnerLabel(ownerPubkey, currentPubkey, ownerProfiles) + : null, avatarUrl: getAuthorAvatarUrl({ authorPubkey, currentPubkey, diff --git a/desktop/src/features/messages/types.ts b/desktop/src/features/messages/types.ts index 6fec0fbcb0..ec656f2236 100644 --- a/desktop/src/features/messages/types.ts +++ b/desktop/src/features/messages/types.ts @@ -25,6 +25,12 @@ export type TimelineMessage = { */ signerPubkey?: string; author: string; + /** True when the displayed author is known to be an agent. */ + isAgent?: boolean; + /** Verified owner pubkey for an agent author, when available. */ + ownerPubkey?: string | null; + /** Viewer-relative owner label (for example, "you" or "baxen"). */ + ownerLabel?: string | null; avatarUrl?: string | null; role?: string; /** For bot messages, the display name of the persona this bot was created from. */ diff --git a/desktop/src/features/messages/ui/MessageAgentOwner.tsx b/desktop/src/features/messages/ui/MessageAgentOwner.tsx new file mode 100644 index 0000000000..39cfa0ffbb --- /dev/null +++ b/desktop/src/features/messages/ui/MessageAgentOwner.tsx @@ -0,0 +1,51 @@ +import { Bot } from "lucide-react"; + +import { UserProfilePopover } from "@/features/profile/ui/UserProfilePopover"; + +export function MessageAgentOwner({ + ownerLabel, + ownerPubkey, +}: { + ownerLabel?: string | null; + ownerPubkey?: string | null; +}) { + return ( + + + {ownerLabel + ? `Agent owned by ${ownerLabel}` + : "Agent; owner unavailable"} + + {ownerPubkey && ownerLabel ? ( + <> + + + + + + ) : ( + + )} + + ); +} diff --git a/desktop/src/features/messages/ui/MessageRow.tsx b/desktop/src/features/messages/ui/MessageRow.tsx index 0a762bff35..a57bf93e40 100644 --- a/desktop/src/features/messages/ui/MessageRow.tsx +++ b/desktop/src/features/messages/ui/MessageRow.tsx @@ -40,6 +40,7 @@ import { resolveMentionProps } from "@/shared/lib/resolveMentionNames"; import { Markdown } from "@/shared/ui/markdown"; import type { VideoReviewContext } from "@/shared/ui/VideoPlayer"; import { MessageActionBar } from "./MessageActionBar"; +import { MessageAgentOwner } from "./MessageAgentOwner"; import { MessageAuthorText, MessageHeaderRow } from "./MessageHeader"; import { MessageTimestamp } from "./MessageTimestamp"; import { WaveMessageAttachment } from "./WaveMessageAttachment"; @@ -459,6 +460,12 @@ export const MessageRow = React.memo( ) : ( {message.author} ); + const agentOwnerNode = message.isAgent ? ( + + ) : null; const actionBarNode = (
; profiles?: UserProfileLookup; + ownerProfiles?: UserProfileLookup; followThreadById?: (rootId: string) => void; isFollowingThreadById?: (rootId: string) => boolean; isMessageUnreadById?: (messageId: string) => boolean; @@ -170,6 +171,7 @@ const MessageTimelineBase = React.forwardRef< messageFooters, personaLookup, profiles, + ownerProfiles, onDelete, onEdit, onMarkUnread, @@ -636,6 +638,7 @@ const MessageTimelineBase = React.forwardRef< onAtBottomStateChange={handleVirtualizerAtBottomStateChange} personaLookup={personaLookup} profiles={profiles} + ownerProfiles={ownerProfiles} searchActiveMessageId={searchActiveMessageId} searchMatchingMessageIds={searchMatchingMessageIds} searchQuery={searchQuery} diff --git a/desktop/src/features/messages/ui/SystemMessageRow.tsx b/desktop/src/features/messages/ui/SystemMessageRow.tsx index 1ad48ff805..4410964dd9 100644 --- a/desktop/src/features/messages/ui/SystemMessageRow.tsx +++ b/desktop/src/features/messages/ui/SystemMessageRow.tsx @@ -9,7 +9,10 @@ import type { import { MessageReactions } from "@/features/messages/ui/MessageReactions"; import { useReactionHandler } from "@/features/messages/ui/useReactionHandler"; import { recordQuickReactionEmoji } from "@/features/messages/ui/useQuickReactionEmojis"; -import type { UserProfileLookup } from "@/features/profile/lib/identity"; +import { + formatOwnerLabel, + type UserProfileLookup, +} from "@/features/profile/lib/identity"; import { resolveUserLabel } from "@/features/profile/lib/identity"; import { UserProfilePopover } from "@/features/profile/ui/UserProfilePopover"; import { cn } from "@/shared/lib/cn"; @@ -25,6 +28,7 @@ import { import { Popover, PopoverContent, PopoverTrigger } from "@/shared/ui/popover"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/shared/ui/tooltip"; import { UserAvatar } from "@/shared/ui/UserAvatar"; +import { MessageAgentOwner } from "./MessageAgentOwner"; import { MessageAuthorText, MessageHeaderRow } from "./MessageHeader"; import { MessageTimestamp } from "./MessageTimestamp"; @@ -631,6 +635,7 @@ export const SystemMessageRow = React.memo(function SystemMessageRow({ currentPubkey, agentPubkeys, profiles, + ownerProfiles, personaLookup, onToggleReaction, }: { @@ -639,6 +644,7 @@ export const SystemMessageRow = React.memo(function SystemMessageRow({ currentPubkey?: string; agentPubkeys?: ReadonlySet; profiles?: UserProfileLookup; + ownerProfiles?: UserProfileLookup; /** Map from lowercase pubkey → persona display name for bot members. */ personaLookup?: Map; onToggleReaction?: ( @@ -720,6 +726,33 @@ export const SystemMessageRow = React.memo(function SystemMessageRow({ payload.type === "member_joined" || payload.type === "members_added" || payload.type === "members_joined"; + const displayedIdentityPubkey = isMembershipArrival + ? payload.target + : payload.actor; + const displayedIdentityProfile = displayedIdentityPubkey + ? profiles?.[normalizePubkey(displayedIdentityPubkey)] + : undefined; + const displayedTimelineIdentity = displayedIdentityPubkey + ? sourceMessages.find( + (source) => + source.pubkey && + normalizePubkey(source.pubkey) === + normalizePubkey(displayedIdentityPubkey), + ) + : undefined; + const displayedIdentityIsAgent = Boolean( + displayedIdentityProfile?.isAgent || + displayedTimelineIdentity?.isAgent || + (displayedIdentityPubkey && + agentPubkeys?.has(normalizePubkey(displayedIdentityPubkey))), + ); + const displayedOwnerPubkey = + displayedIdentityProfile?.ownerPubkey ?? + displayedTimelineIdentity?.ownerPubkey ?? + null; + const displayedOwnerLabel = + displayedTimelineIdentity?.ownerLabel ?? + formatOwnerLabel(displayedOwnerPubkey, currentPubkey, ownerProfiles); const wouldAddReaction = (emoji: string) => !reactions.some( @@ -750,6 +783,12 @@ export const SystemMessageRow = React.memo(function SystemMessageRow({ {description.title} + {displayedIdentityIsAgent ? ( + + ) : null} ; profiles?: UserProfileLookup; + ownerProfiles?: UserProfileLookup; /** The message ID of the currently active find-in-channel match. */ searchActiveMessageId?: string | null; /** Set of message IDs that match the current find-in-channel query. */ @@ -147,6 +148,7 @@ export const TimelineMessageList = React.memo(function TimelineMessageList({ onSendVideoReviewComment, onToggleReaction, profiles, + ownerProfiles, searchActiveMessageId = null, searchMatchingMessageIds, searchQuery, @@ -237,6 +239,7 @@ export const TimelineMessageList = React.memo(function TimelineMessageList({ footer={messageFooters?.[item.entry.message.id] ?? null} onToggleReaction={onToggleReaction} profiles={profiles} + ownerProfiles={ownerProfiles} /> ); case "system-group": @@ -249,6 +252,7 @@ export const TimelineMessageList = React.memo(function TimelineMessageList({ )} onToggleReaction={onToggleReaction} profiles={profiles} + ownerProfiles={ownerProfiles} /> ); case "message": @@ -306,6 +310,7 @@ export const TimelineMessageList = React.memo(function TimelineMessageList({ onReply, onToggleReaction, profiles, + ownerProfiles, searchActiveMessageId, searchMatchingMessageIds, searchQuery, @@ -782,6 +787,7 @@ function SystemRow({ footer, onToggleReaction, profiles, + ownerProfiles, }: { currentPubkey?: string; entries?: MainTimelineEntry[]; @@ -789,6 +795,7 @@ function SystemRow({ footer: React.ReactNode; onToggleReaction?: TimelineMessageListProps["onToggleReaction"]; profiles?: UserProfileLookup; + ownerProfiles?: UserProfileLookup; }) { const systemEntries = entries ?? (entry ? [entry] : []); const firstEntry = systemEntries[0]; @@ -806,6 +813,7 @@ function SystemRow({ currentPubkey={currentPubkey} onToggleReaction={onToggleReaction} profiles={profiles} + ownerProfiles={ownerProfiles} /> {footer}
diff --git a/desktop/src/features/messages/useIndependentThreadPanel.ts b/desktop/src/features/messages/useIndependentThreadPanel.ts index 086931b59a..41002661d7 100644 --- a/desktop/src/features/messages/useIndependentThreadPanel.ts +++ b/desktop/src/features/messages/useIndependentThreadPanel.ts @@ -19,6 +19,7 @@ export function useIndependentThreadPanel(args: { currentPubkey: string | undefined; currentAvatarUrl: string | null; profiles: UserProfileLookup | undefined; + ownerProfiles: UserProfileLookup | undefined; members: ChannelMember[] | undefined; personaLookup: Map; respondToLookup: Map; @@ -48,6 +49,7 @@ export function useIndependentThreadPanel(args: { args.personaLookup, args.respondToLookup, args.relaySelfPubkey, + args.ownerProfiles, ), [ args.channelEvents, @@ -59,6 +61,7 @@ export function useIndependentThreadPanel(args: { args.currentPubkey, args.currentAvatarUrl, args.profiles, + args.ownerProfiles, args.members, args.personaLookup, args.respondToLookup, diff --git a/desktop/src/features/profile/lib/identity.test.mjs b/desktop/src/features/profile/lib/identity.test.mjs index 282152e03c..da0259a66f 100644 --- a/desktop/src/features/profile/lib/identity.test.mjs +++ b/desktop/src/features/profile/lib/identity.test.mjs @@ -1,7 +1,10 @@ import assert from "node:assert/strict"; import test from "node:test"; -import { profileLookupsEqual } from "./identity.ts"; +import { formatOwnerLabel, profileLookupsEqual } from "./identity.ts"; + +const OWNER_PUBKEY = + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; const summary = (over = {}) => ({ displayName: "Ada", @@ -12,6 +15,23 @@ const summary = (over = {}) => ({ ...over, }); +test("formatOwnerLabel resolves a known owner's display name", () => { + assert.equal( + formatOwnerLabel(OWNER_PUBKEY, null, { + [OWNER_PUBKEY]: summary({ displayName: "baxen" }), + }), + "baxen", + ); +}); + +test("formatOwnerLabel calls the viewer-owned agent's owner you", () => { + assert.equal(formatOwnerLabel(OWNER_PUBKEY, OWNER_PUBKEY, {}), "you"); +}); + +test("formatOwnerLabel returns null when verified ownership is absent", () => { + assert.equal(formatOwnerLabel(null, OWNER_PUBKEY, {}), null); +}); + test("profileLookupsEqual: same reference is equal", () => { const a = { p1: summary() }; assert.equal(profileLookupsEqual(a, a), true); diff --git a/desktop/tests/e2e/messaging.spec.ts b/desktop/tests/e2e/messaging.spec.ts index f32255ae84..4f1736873e 100644 --- a/desktop/tests/e2e/messaging.spec.ts +++ b/desktop/tests/e2e/messaging.spec.ts @@ -92,8 +92,51 @@ async function measureThreadSummaryGeometry(summaryRow: Locator) { }); } -test.beforeEach(async ({ page }) => { - await installMockBridge(page); +test.beforeEach(async ({ page }, testInfo) => { + const mock = testInfo.title.includes("agent owner label") + ? { + searchProfiles: [ + { + pubkey: TEST_IDENTITIES.alice.pubkey, + displayName: "alice", + ownerPubkey: TEST_IDENTITIES.bob.pubkey, + isAgent: true, + }, + { + pubkey: TEST_IDENTITIES.bob.pubkey, + displayName: "bob", + }, + ], + } + : undefined; + await installMockBridge(page, mock); +}); + +test("agent owner label identifies the agent and owner", async ({ page }) => { + await page.goto("/"); + await page.getByTestId("channel-general").click(); + + const aliceMessage = page + .getByTestId("message-row") + .filter({ hasText: "Hey team — checking in." }); + const ownerTreatment = aliceMessage.getByTestId("message-agent-owner"); + + await expect(ownerTreatment.locator("svg")).toBeVisible(); + await expect( + ownerTreatment.getByText("owned by", { exact: true }), + ).toBeVisible(); + await expect(ownerTreatment.locator(".font-semibold")).toHaveText("bob"); + await expect(ownerTreatment.locator(".sr-only")).toHaveText( + "Agent owned by bob", + ); + + const joinedRow = page + .getByTestId("system-message-row") + .filter({ hasText: "alice" }) + .filter({ hasText: "joined the channel" }); + await expect(joinedRow.getByTestId("message-agent-owner")).toContainText( + "owned bybob", + ); }); test("send a message and see it in timeline", async ({ page }) => { From 964b9a88e88cf783bbb53eb28c78a6d1533d4f4b Mon Sep 17 00:00:00 2001 From: npub1qvn3cujt28pg06ehlstrxyz6ayzp06t4uc7r566vxwwgrv24hglq9zju0n <03271c724b51c287eb37fc1633105ae90417e975e63c3a6b4c339c81b155ba3e@sprout-oss.stage.blox.sqprod.co> Date: Sat, 18 Jul 2026 11:18:12 -0700 Subject: [PATCH 2/2] fix(desktop): label owner profile control Co-authored-by: npub1qvn3cujt28pg06ehlstrxyz6ayzp06t4uc7r566vxwwgrv24hglq9zju0n <03271c724b51c287eb37fc1633105ae90417e975e63c3a6b4c339c81b155ba3e@sprout-oss.stage.blox.sqprod.co> Signed-off-by: npub1qvn3cujt28pg06ehlstrxyz6ayzp06t4uc7r566vxwwgrv24hglq9zju0n <03271c724b51c287eb37fc1633105ae90417e975e63c3a6b4c339c81b155ba3e@sprout-oss.stage.blox.sqprod.co> --- .../features/messages/ui/MessageAgentOwner.tsx | 15 +++++++-------- .../features/profile/ui/UserProfilePopover.tsx | 4 ++++ desktop/tests/e2e/messaging.spec.ts | 5 ++--- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/desktop/src/features/messages/ui/MessageAgentOwner.tsx b/desktop/src/features/messages/ui/MessageAgentOwner.tsx index 39cfa0ffbb..1871d767d0 100644 --- a/desktop/src/features/messages/ui/MessageAgentOwner.tsx +++ b/desktop/src/features/messages/ui/MessageAgentOwner.tsx @@ -15,9 +15,7 @@ export function MessageAgentOwner({ data-testid="message-agent-owner" > - {ownerLabel - ? `Agent owned by ${ownerLabel}` - : "Agent; owner unavailable"} + {ownerLabel ? "Agent owned by" : "Agent; owner unavailable"} {ownerPubkey && ownerLabel ? ( <> @@ -28,11 +26,12 @@ export function MessageAgentOwner({ owned by - -