fix(desktop): unify owned-agent cloud provenance markers (#7129)

Unify presentation-only cloud provenance across agent identity surfaces. Keep successful local-inventory and verified-ownership gates; preserve main availability and mention spacing behavior.

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-02 22:35:37 -04:00
committed by GitHub
parent d5a73b9f35
commit 5073e075d7
32 changed files with 717 additions and 117 deletions

View File

@@ -78,6 +78,7 @@ export default defineConfig({
"**/composer-tooltip-dismiss.spec.ts",
"**/mentions.spec.ts",
"**/mention-spacing.spec.ts",
"**/cloud-provenance.spec.ts",
"**/team-mentions.spec.ts",
"**/persistent-agent-audience.spec.ts",
"**/relay-reconnect.spec.ts",

View File

@@ -216,6 +216,11 @@ with a TypeScript lookup table or an id comparison in a component.
Unqueried persona siblings are unknown. No presence state grants deletion or
Stop authority; native local stop-before-remove remains independent. See
[the availability contract](../../../../docs/agent-availability.md).
The shared cloud marker means “Not managed on this device” only
after ownership and successful local inventory are known. It does not imply
hosting location, availability, or permission. Keep all identity surfaces on
the shared provenance context, without per-row directory subscriptions. See
[the provenance contract](../../../../docs/agent-management-provenance.md).
14. **Thinking effort has two surfaces: a local-only WRITE control and a
read-only two-facts DISPLAY.** The write control is `EffortPickerField`
(`ui/EffortPickerField.tsx`), a self-contained section component mounted in

View File

@@ -1,7 +1,10 @@
import assert from "node:assert/strict";
import test from "node:test";
import { isOtherSetupAgent } from "./otherSetupAgent.ts";
import {
isOtherSetupAgent,
isOwnedAgentNotManagedOnDevice,
} from "./otherSetupAgent.ts";
const OWNER = "a".repeat(64);
const AGENT = "b".repeat(64);
@@ -20,7 +23,7 @@ test("fails closed while the local managed directory is unresolved", () => {
);
});
test("labels a viewer-owned non-local identity as another setup", () => {
test("labels a viewer-owned identity as not managed on this device", () => {
assert.equal(
isOtherSetupAgent({
agentDirectoriesReady: true,
@@ -33,3 +36,38 @@ test("labels a viewer-owned non-local identity as another setup", () => {
true,
);
});
test("a locally managed provider is not labeled as another device", () => {
assert.equal(
isOtherSetupAgent({
agentDirectoriesReady: true,
currentPubkey: OWNER,
managedAgents: [{ pubkey: AGENT, backend: { type: "provider" } }],
profileOwnerPubkey: OWNER,
pubkey: AGENT,
relayAgents: [],
}),
false,
);
});
for (const [name, overrides, expected] of [
["owned absent key", {}, true],
["loading local inventory", { localInventoryReady: false }, false],
["exact local provider record", { isLocallyManaged: true }, false],
["different owner", { ownerPubkey: "b".repeat(64) }, false],
["unknown ownership", { ownerPubkey: null }, false],
]) {
test(`shared provenance: ${name}`, () => {
assert.equal(
isOwnedAgentNotManagedOnDevice({
currentPubkey: "a".repeat(64),
ownerPubkey: "A".repeat(64),
localInventoryReady: true,
isLocallyManaged: false,
...overrides,
}),
expected,
);
});
}

View File

@@ -1,6 +1,7 @@
import type { ManagedAgent, RelayAgent } from "@/shared/api/types";
import { normalizePubkey } from "@/shared/lib/pubkey";
/** Owned identity absent from the loaded local inventory; not evidence of hosting location. */
export function isOtherSetupAgent({
agentDirectoriesReady,
currentPubkey,
@@ -32,8 +33,31 @@ export function isOtherSetupAgent({
)?.ownerPubkey;
const ownerPubkey = profileOwnerPubkey ?? relayOwnerPubkey;
return isOwnedAgentNotManagedOnDevice({
currentPubkey,
ownerPubkey,
localInventoryReady: agentDirectoriesReady,
isLocallyManaged: false,
});
}
/** Presentation provenance only; neither hosting location nor availability. */
export function isOwnedAgentNotManagedOnDevice({
currentPubkey,
ownerPubkey,
localInventoryReady,
isLocallyManaged,
}: {
currentPubkey?: string;
ownerPubkey?: string | null;
localInventoryReady: boolean;
isLocallyManaged: boolean;
}): boolean {
return Boolean(
ownerPubkey &&
localInventoryReady &&
!isLocallyManaged &&
currentPubkey &&
ownerPubkey &&
normalizePubkey(ownerPubkey) === normalizePubkey(currentPubkey),
);
}

View File

@@ -1,8 +1,10 @@
import { Cloud } from "lucide-react";
import { useIsOtherSetupAgent } from "../useKnownAgentPubkeys";
import { cn } from "@/shared/lib/cn";
const OTHER_SETUP_LABEL = "From another Buzz setup";
const OTHER_SETUP_LABEL = "Not managed on this device";
export function OtherSetupAgentMarker({
className,
@@ -23,3 +25,21 @@ export function OtherSetupAgentMarker({
</span>
);
}
/** Connected marker for identity details; shares the app's directory subscriptions. */
export function AgentManagementMarker({
pubkey,
ownerPubkey,
className,
testId,
}: {
pubkey?: string | null;
ownerPubkey?: string | null;
className?: string;
testId?: string;
}) {
const show = useIsOtherSetupAgent(pubkey, ownerPubkey);
return show ? (
<OtherSetupAgentMarker className={className} testId={testId} />
) : null;
}

View File

@@ -0,0 +1,73 @@
import assert from "node:assert/strict";
import { after, test } from "node:test";
import { createElement } from "react";
import { JSDOM } from "jsdom";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import {
KnownAgentPubkeysProvider,
useIsOtherSetupAgent,
} from "./useKnownAgentPubkeys.tsx";
const dom = new JSDOM("<!doctype html><html><body></body></html>", {
url: "http://localhost",
});
Object.assign(globalThis, {
window: dom.window,
document: dom.window.document,
HTMLElement: dom.window.HTMLElement,
IS_REACT_ACT_ENVIRONMENT: true,
});
after(() => dom.window.close());
test("provenance context follows exact local inventory and rejects failed cached reads", async () => {
const { act, renderHook, cleanup } = await import("@testing-library/react");
const owner = "a".repeat(64),
remote = "b".repeat(64),
local = "c".repeat(64);
const client = new QueryClient({
defaultOptions: {
queries: { enabled: false, retry: false, staleTime: Infinity },
},
});
client.setQueryData(["identity"], { pubkey: owner });
client.setQueryData(
["managed-agents"],
[{ pubkey: local, status: "stopped" }],
);
client.setQueryData(
["relay-agents"],
[{ pubkey: remote, ownerPubkey: owner }],
);
const wrapper = ({ children }) =>
createElement(
QueryClientProvider,
{ client },
createElement(KnownAgentPubkeysProvider, null, children),
);
const { result } = renderHook(
() => [
useIsOtherSetupAgent(remote),
useIsOtherSetupAgent(local, owner),
useIsOtherSetupAgent("d".repeat(64), owner),
],
{ wrapper },
);
assert.deepEqual(result.current, [true, false, true]);
await act(async () =>
client.setQueryData(
["managed-agents"],
[{ pubkey: remote, status: "deployed" }],
),
);
assert.deepEqual(result.current, [false, true, true]);
await act(async () => {
client
.getQueryCache()
.find({ queryKey: ["managed-agents"] })
.setState({ error: new Error("inventory unavailable"), status: "error" });
await new Promise((resolve) => setTimeout(resolve, 5));
});
assert.deepEqual(result.current, [false, false, false]);
cleanup();
client.clear();
});

View File

@@ -5,10 +5,25 @@ import {
useRelayAgentsQuery,
} from "@/features/agents/hooks";
import { mergeKnownAgentPubkeys } from "@/features/agents/knownAgentPubkeys";
import { useStableSet } from "@/shared/hooks/useStableReference";
import { useStableMap, useStableSet } from "@/shared/hooks/useStableReference";
import { useIdentityQuery } from "@/shared/api/hooks";
import { normalizePubkey } from "@/shared/lib/pubkey";
import { isAgentDirectoryReady } from "./lib/agentAutocompleteEligibility";
import { isOwnedAgentNotManagedOnDevice } from "./lib/otherSetupAgent";
const EMPTY_KNOWN_AGENT_PUBKEYS: ReadonlySet<string> = new Set();
const AgentManagementContext = React.createContext<{
currentPubkey?: string;
localInventoryReady: boolean;
localPubkeys: ReadonlySet<string>;
relayOwners: ReadonlyMap<string, string | null>;
}>({
localInventoryReady: false,
localPubkeys: new Set(),
relayOwners: new Map(),
});
const KnownAgentPubkeysContext = React.createContext<ReadonlySet<string>>(
EMPTY_KNOWN_AGENT_PUBKEYS,
);
@@ -39,8 +54,36 @@ export function KnownAgentPubkeysProvider({
}: {
children: React.ReactNode;
}) {
const managedAgents = useManagedAgentsQuery().data;
const relayAgents = useRelayAgentsQuery().data;
const managedQuery = useManagedAgentsQuery();
const relayQuery = useRelayAgentsQuery();
const currentPubkey = useIdentityQuery().data?.pubkey;
const managedAgents = managedQuery.data;
const relayAgents = relayQuery.data;
const localPubkeys = useStableSet(
new Set(
(managedAgents ?? []).map((agent) => normalizePubkey(agent.pubkey)),
),
);
const relayOwners = useStableMap(
new Map(
isAgentDirectoryReady(relayQuery)
? (relayAgents ?? []).map((agent) => [
normalizePubkey(agent.pubkey),
agent.ownerPubkey,
])
: [],
),
);
const localInventoryReady = isAgentDirectoryReady(managedQuery);
const management = React.useMemo(
() => ({
currentPubkey,
localInventoryReady,
localPubkeys,
relayOwners,
}),
[currentPubkey, localInventoryReady, localPubkeys, relayOwners],
);
const merged = React.useMemo(
() => mergeKnownAgentPubkeys(managedAgents, relayAgents),
@@ -50,7 +93,9 @@ export function KnownAgentPubkeysProvider({
return (
<KnownAgentPubkeysContext.Provider value={stable}>
{children}
<AgentManagementContext.Provider value={management}>
{children}
</AgentManagementContext.Provider>
</KnownAgentPubkeysContext.Provider>
);
}
@@ -82,3 +127,21 @@ export function KnownAgentPubkeysProvider({
export function useKnownAgentPubkeys(): ReadonlySet<string> {
return React.useContext(KnownAgentPubkeysContext);
}
/** Shared provenance without per-row query observers; exact keys, never personas. */
export function useIsOtherSetupAgent(
pubkey?: string | null,
profileOwnerPubkey?: string | null,
): boolean {
const state = React.useContext(AgentManagementContext);
const key = normalizePubkey(pubkey ?? "");
return (
Boolean(key) &&
isOwnedAgentNotManagedOnDevice({
currentPubkey: state.currentPubkey,
ownerPubkey: profileOwnerPubkey ?? state.relayOwners.get(key),
localInventoryReady: state.localInventoryReady,
isLocallyManaged: state.localPubkeys.has(key),
})
);
}

View File

@@ -1,3 +1,4 @@
import { AgentManagementMarker } from "@/features/agents/ui/OtherSetupAgentMarker";
import { Bot } from "lucide-react";
import type { UserSearchResult } from "@/shared/api/types";
import { Button } from "@/shared/ui/button";
@@ -60,6 +61,10 @@ export function AddMemberSearchResultRow({
<Bot aria-hidden="true" className="h-4 w-4" />
agent
</span>
<AgentManagementMarker
pubkey={user.pubkey}
ownerPubkey={user.ownerPubkey}
/>
</div>
<span className="block truncate font-mono text-2xs text-muted-foreground">
{truncatePubkey(user.pubkey)}

View File

@@ -14,6 +14,7 @@ import {
ProfileAvatarWithStatus,
scaleProfileAvatarStatusGeometry,
} from "@/features/profile/ui/ProfileAvatarWithStatus";
import { AgentManagementMarker } from "@/features/agents/ui/OtherSetupAgentMarker";
import { UserProfilePopover } from "@/features/profile/ui/UserProfilePopover";
import { Button } from "@/shared/ui/button";
import type { Channel, PresenceStatus } from "@/shared/api/types";
@@ -194,9 +195,17 @@ export function ChannelScreenHeader({
) : undefined
}
statusBadge={
<ChannelHeaderStatusBadge
ephemeralDisplay={activeChannelEphemeralDisplay}
/>
<>
<ChannelHeaderStatusBadge
ephemeralDisplay={activeChannelEphemeralDisplay}
/>
{!isGroupDm && activeDmParticipant ? (
<AgentManagementMarker
pubkey={activeDmParticipant.pubkey}
testId="chat-header-agent-provenance"
/>
) : null}
</>
}
title={activeChannelTitle}
transparentChrome={transparentChrome}

View File

@@ -12,10 +12,8 @@ import {
coalesceAgentAutocompleteCandidates,
getMentionableAgentPubkeys,
getSharedChannelIds,
isAgentDirectoryReady,
isAgentIdentityInAllowedList,
} from "@/features/agents/lib/agentAutocompleteEligibility";
import { isOtherSetupAgent } from "@/features/agents/lib/otherSetupAgent";
import { useIsArchivedPredicate } from "@/features/identity-archive/hooks";
import { useClassifiedMembers } from "@/features/channels/lib/useClassifiedMembers";
import { formatMemberName } from "@/features/channels/lib/memberUtils";
@@ -184,9 +182,6 @@ export function MembersSidebar({
managedAgentsQuery,
relayAgentsQuery,
} = useClassifiedMembers(rawMembers, currentPubkey);
const agentDirectoriesReady =
isAgentDirectoryReady(managedAgentsQuery) &&
isAgentDirectoryReady(relayAgentsQuery);
const activeMembers = React.useMemo(
() =>
[...people, ...bots].sort((left, right) =>
@@ -618,16 +613,6 @@ export function MembersSidebar({
const managedAgent = memberIsBot
? managedAgentByPubkey.get(normalizePubkey(member.pubkey))
: undefined;
const showOtherSetupMarker =
memberIsBot &&
isOtherSetupAgent({
agentDirectoriesReady,
currentPubkey,
managedAgents: managedAgentsQuery.data ?? [],
profileOwnerPubkey: memberProfile?.ownerPubkey,
pubkey: member.pubkey,
relayAgents: relayAgentsQuery.data ?? [],
});
const managedAgentRuntime =
memberIsBot && relayUrl
? findManagedAgentRuntime(
@@ -685,7 +670,7 @@ export function MembersSidebar({
pairAction={pairAction}
presenceStatus={getAvailability(member.pubkey)}
profileAvatarUrl={memberProfile?.avatarUrl ?? null}
showOtherSetupMarker={showOtherSetupMarker}
profileOwnerPubkey={memberProfile?.ownerPubkey}
viewerIsOwner={viewerIsOwner}
/>
);

View File

@@ -19,7 +19,7 @@ import {
getManagedAgentPrimaryActionLabel,
isManagedAgentActive,
} from "@/features/agents/lib/managedAgentControlActions";
import { OtherSetupAgentMarker } from "@/features/agents/ui/OtherSetupAgentMarker";
import { AgentManagementMarker } from "@/features/agents/ui/OtherSetupAgentMarker";
import { ProfileAvatar } from "@/features/profile/ui/ProfileAvatar";
import { PresenceDot } from "@/features/presence/ui/PresenceBadge";
import {
@@ -75,7 +75,7 @@ type MembersSidebarMemberCardProps = {
onViewActivity?: (pubkey: string) => void;
presenceStatus?: PresenceStatus | null;
profileAvatarUrl?: string | null;
showOtherSetupMarker?: boolean;
profileOwnerPubkey?: string | null;
viewerIsOwner: boolean;
};
@@ -144,7 +144,7 @@ export function MembersSidebarMemberCard({
onViewActivity,
presenceStatus,
profileAvatarUrl,
showOtherSetupMarker = false,
profileOwnerPubkey,
viewerIsOwner,
}: MembersSidebarMemberCardProps) {
const roleLabel = formatRoleLabel(member, memberIsBot);
@@ -199,11 +199,11 @@ export function MembersSidebarMemberCard({
</span>
</span>
</div>
{showOtherSetupMarker ? (
<OtherSetupAgentMarker
testId={`sidebar-member-agent-provenance-${member.pubkey}`}
/>
) : null}
<AgentManagementMarker
pubkey={member.pubkey}
ownerPubkey={profileOwnerPubkey}
testId={`sidebar-member-agent-provenance-${member.pubkey}`}
/>
</div>
) : (
<div className="flex min-w-0 items-center gap-2">

View File

@@ -149,3 +149,22 @@ test("policy-only discovery stays selectable without claiming active presence",
assert.equal(candidate.isActiveAgent, false);
assert.equal(candidate.ownerPubkey, MEMBER_PUBKEY);
});
for (const locallyManaged of [true, false]) {
test(`roster candidate preserves exact local management: ${locallyManaged}`, () => {
const [candidate] = buildMentionCandidates(
input({
members: [{ pubkey: AGENT_PUBKEY, displayName: "Scout", role: "bot" }],
managedAgentNamesByPubkey: new Map(
locallyManaged ? [[AGENT_PUBKEY, "Scout"]] : [],
),
managedAgents: locallyManaged
? [{ pubkey: AGENT_PUBKEY, name: "Scout", status: "deployed" }]
: [],
mentionableAgentPubkeys: new Set([AGENT_PUBKEY]),
}),
);
assert.equal(candidate.isMember, true);
assert.equal(Boolean(candidate.isManagedAgent), locallyManaged);
});
}

View File

@@ -154,6 +154,7 @@ export function buildMentionCandidates({
managedAgentNamesByPubkey.has(pubkey) ||
relayAgentNamesByPubkey.has(pubkey),
isActiveAgent: activeAgentPubkeys.has(pubkey),
isManagedAgent: managedAgentNamesByPubkey.has(pubkey),
ownerPubkey: profile?.ownerPubkey ?? null,
personaName: personaNameByPubkey.get(pubkey) ?? null,
role: member.role,

View File

@@ -1,3 +1,4 @@
import { isOwnedAgentNotManagedOnDevice } from "@/features/agents/lib/otherSetupAgent";
import type { MentionSuggestion } from "@/features/messages/ui/MentionAutocomplete";
import type { UserProfileLookup } from "@/features/profile/lib/identity";
import { formatOwnerLabel } from "@/features/profile/lib/identity";
@@ -61,10 +62,12 @@ export function mapMentionCandidateToSuggestion(opts: {
agentProvenanceReady && candidate.kind === "identity" && candidate.isAgent
? candidate.isManagedAgent
? "managed-here"
: candidate.ownerPubkey &&
currentPubkey &&
normalizePubkey(candidate.ownerPubkey) ===
normalizePubkey(currentPubkey)
: isOwnedAgentNotManagedOnDevice({
currentPubkey: currentPubkey ?? undefined,
ownerPubkey: candidate.ownerPubkey,
localInventoryReady: agentProvenanceReady,
isLocallyManaged: Boolean(candidate.isManagedAgent),
})
? "managed-elsewhere"
: undefined
: undefined,

View File

@@ -1,3 +1,4 @@
import { AgentManagementMarker } from "@/features/agents/ui/OtherSetupAgentMarker";
import { ArrowUp, AtSign, Square, X } from "lucide-react";
import {
AnimatePresence,
@@ -270,7 +271,7 @@ export function ComposerMentionButton({
</TooltipTrigger>
<TooltipContent>
Don't automatically mention {agent.displayName} in this
thread
thread <AgentManagementMarker pubkey={agent.pubkey} />
</TooltipContent>
</Tooltip>
))}

View File

@@ -334,25 +334,56 @@ function suggestion(agentProvenance) {
test("duplicate owned agents mark only the other setup", () => {
assert.equal(
showMentionAgentProvenanceMarker(suggestion("managed-here"), true),
showMentionAgentProvenanceMarker(suggestion("managed-here")),
false,
);
assert.equal(
showMentionAgentProvenanceMarker(suggestion("managed-elsewhere"), true),
showMentionAgentProvenanceMarker(suggestion("managed-elsewhere")),
true,
);
});
test("unique agents omit management provenance", () => {
test("unique agents mark the same not-locally-managed provenance", () => {
assert.equal(
showMentionAgentProvenanceMarker(suggestion("managed-here"), false),
showMentionAgentProvenanceMarker(suggestion("managed-elsewhere")),
true,
);
assert.equal(
showMentionAgentProvenanceMarker(suggestion("managed-here")),
false,
);
});
test("agents without trustworthy provenance omit management provenance", () => {
assert.equal(
showMentionAgentProvenanceMarker(suggestion(undefined), true),
false,
);
assert.equal(showMentionAgentProvenanceMarker(suggestion(undefined)), false);
});
for (const duplicate of [false, true]) {
test(`rendered provenance is truthful (${duplicate ? "duplicate" : "unique"} names)`, async () => {
const React = await import("react");
const { render } = await import("@testing-library/react");
const { MentionAutocomplete } = await import("./MentionAutocomplete.tsx");
const { TooltipProvider } = await import("@/shared/ui/tooltip");
const remote = suggestion("managed-elsewhere");
const suggestions = duplicate
? [remote, { ...suggestion("managed-here"), pubkey: "2".repeat(64) }]
: [remote];
const view = render(
React.createElement(
TooltipProvider,
null,
React.createElement(MentionAutocomplete, {
suggestions,
selectedIndex: 0,
onSelect() {},
}),
),
);
const markers = view.getAllByRole("img", {
name: "Not managed on this device",
});
assert.equal(markers.length, 1);
assert.equal(markers[0].title, "Not managed on this device");
assert.equal(view.queryByTitle("From another Buzz setup"), null);
});
}

View File

@@ -52,9 +52,8 @@ type MentionAutocompleteProps = {
export function showMentionAgentProvenanceMarker(
suggestion: MentionSuggestion,
hasNameCollision: boolean,
): boolean {
return hasNameCollision && suggestion.agentProvenance === "managed-elsewhere";
return suggestion.agentProvenance === "managed-elsewhere";
}
/** Focus the now-always-visible automatic-mentions switch. */
@@ -254,10 +253,8 @@ export const MentionAutocomplete = React.memo(function MentionAutocomplete({
suggestion.displayName;
const hasNameCollision =
(nameCounts.get(suggestion.displayName.toLowerCase()) ?? 0) > 1;
const showAgentProvenanceMarker = showMentionAgentProvenanceMarker(
suggestion,
hasNameCollision,
);
const showAgentProvenanceMarker =
showMentionAgentProvenanceMarker(suggestion);
const ownerLabel =
hasNameCollision && suggestion.agentProvenance
? null

View File

@@ -1,4 +1,5 @@
import * as React from "react";
import { AgentManagementMarker } from "@/features/agents/ui/OtherSetupAgentMarker";
import type { UserProfileLookup } from "@/features/profile/lib/identity";
import { UserProfilePopover } from "@/features/profile/ui/UserProfilePopover";
@@ -37,6 +38,10 @@ export function MessageAgentAddressPrefix({
interactive
>
{label}
<AgentManagementMarker
pubkey={pubkey}
ownerPubkey={profile?.ownerPubkey}
/>
</InlineChip>
</UserProfilePopover>{" "}
</React.Fragment>

View File

@@ -1,5 +1,8 @@
import * as React from "react";
import { AgentManagementMarker } from "@/features/agents/ui/OtherSetupAgentMarker";
import { UserProfilePopover } from "@/features/profile/ui/UserProfilePopover";
import { cn } from "@/shared/lib/cn";
type MessageHeaderRowProps = {
@@ -104,3 +107,40 @@ export function MessageAuthorText({
</Component>
);
}
/** Author navigation and provenance always refer to the same exact identity. */
export function MessageAuthorIdentity({
pubkey,
ownerPubkey,
role,
displayName,
children,
}: {
pubkey?: string | null;
ownerPubkey?: string | null;
role?: string;
displayName: string;
children: React.ReactNode;
}) {
return (
<>
{pubkey ? (
<UserProfilePopover
pubkey={pubkey}
role={role}
botIdenticonValue={displayName}
>
<button
className="truncate rounded leading-message-author focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring"
type="button"
>
{children}
</button>
</UserProfilePopover>
) : (
children
)}
<AgentManagementMarker pubkey={pubkey} ownerPubkey={ownerPubkey} />
</>
);
}

View File

@@ -50,6 +50,7 @@ import { toast } from "sonner";
import { MessageAgentOwner } from "./MessageAgentOwner";
import {
MessageAuthorText,
MessageAuthorIdentity,
MessageHeaderRow,
MessageMetaSegments,
} from "./MessageHeader";
@@ -646,22 +647,14 @@ export const MessageRow = React.memo(
const headerNode = isDisplayedAsContinuation ? null : (
<MessageHeaderRow>
{message.pubkey ? (
<UserProfilePopover
pubkey={message.pubkey}
role={profilePopoverRole}
botIdenticonValue={message.author}
>
<button
className="truncate rounded leading-message-author focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring"
type="button"
>
{authorNode}
</button>
</UserProfilePopover>
) : (
authorNode
)}
<MessageAuthorIdentity
pubkey={message.pubkey}
ownerPubkey={message.ownerPubkey}
role={profilePopoverRole}
displayName={message.author}
>
{authorNode}
</MessageAuthorIdentity>
{/* Author is not a segment: "Alice 9:53 AM" needs no divider. */}
<MessageMetaSegments
segments={[

View File

@@ -1,3 +1,4 @@
import { AgentManagementMarker } from "@/features/agents/ui/OtherSetupAgentMarker";
import { Bot } from "lucide-react";
import { formatOwnerLabel } from "@/features/profile/lib/identity";
@@ -133,6 +134,10 @@ export function NewMessageResultRow({
/>
agent
</span>
<AgentManagementMarker
pubkey={user.pubkey}
ownerPubkey={user.ownerPubkey}
/>
</div>
{ownerLabel ? (
<span className="block truncate text-xs text-muted-foreground">

View File

@@ -1,3 +1,4 @@
import { AgentManagementMarker } from "@/features/agents/ui/OtherSetupAgentMarker";
import { SmilePlus } from "lucide-react";
import * as React from "react";
@@ -285,6 +286,7 @@ function ProfileName({
interactive={Boolean(pubkey)}
>
{children}
<AgentManagementMarker pubkey={pubkey} />
</InlineChip>
) : (
<span
@@ -295,6 +297,7 @@ function ProfileName({
)}
>
{children}
<AgentManagementMarker pubkey={pubkey} />
</span>
);
@@ -914,6 +917,10 @@ export const SystemMessageRow = React.memo(function SystemMessageRow({
<MessageAuthorText as="div" className="text-foreground">
{description.title}
</MessageAuthorText>
<AgentManagementMarker
pubkey={displayedIdentityPubkey}
ownerPubkey={displayedOwnerPubkey}
/>
{displayedIdentityIsAgent ? (
<>
<MessageAgentOwner

View File

@@ -759,6 +759,7 @@ export function UserProfilePanel({
{
agentSettingsMenu,
effectivePubkey,
ownerPubkey: profile?.ownerPubkey,
logCopyValue: isDiagnosticsLikeView ? managedAgentLogContent : null,
logSubtitle: logHeaderSubtitle,
onBack: () => setView("summary"),

View File

@@ -1,5 +1,6 @@
import type { ReactNode } from "react";
import { AgentManagementMarker } from "@/features/agents/ui/OtherSetupAgentMarker";
import { CopyButton } from "@/features/agents/ui/CopyButton";
import { MemoryRefreshButton } from "@/features/agent-memory/ui/MemorySection";
import {
@@ -16,6 +17,7 @@ import { Button } from "@/shared/ui/button";
export function getUserProfilePanelHeaderContent({
agentSettingsMenu,
effectivePubkey,
ownerPubkey,
logCopyValue,
logSubtitle,
onBack,
@@ -25,6 +27,7 @@ export function getUserProfilePanelHeaderContent({
}: {
agentSettingsMenu: ReactNode;
effectivePubkey: string | null;
ownerPubkey?: string | null;
logCopyValue?: string | null;
logSubtitle?: string | null;
onBack: () => void;
@@ -47,6 +50,13 @@ export function getUserProfilePanelHeaderContent({
subtitleTitle={logSubtitle ?? undefined}
title={title}
/>
{view !== "summary" ? (
<AgentManagementMarker
pubkey={effectivePubkey}
ownerPubkey={ownerPubkey}
testId="user-profile-header-agent-provenance"
/>
) : null}
</AuxiliaryPanelHeaderGroup>
);
const headerActions = (

View File

@@ -1,6 +1,8 @@
import * as React from "react";
import { ChevronDown, ChevronUp, Pencil } from "lucide-react";
import { OtherSetupAgentMarker } from "@/features/agents/ui/OtherSetupAgentMarker";
import { useIsOtherSetupAgent } from "@/features/agents/useKnownAgentPubkeys";
import { useAgentWorking } from "@/features/agents/agentWorkingSignal";
import { agentPresenceStartBlockReason } from "@/features/agents/lib/useAgentAvailability";
import {
@@ -189,6 +191,7 @@ export function ProfileSummaryView({
unfollowMutation,
userStatus,
}: ProfileSummaryViewProps) {
const notManagedOnDevice = useIsOtherSetupAgent(pubkey, profile?.ownerPubkey);
const activeTurns = useAgentWorking(isBot ? pubkey : null).channels;
const stickyLayoutRef = React.useRef<HTMLDivElement>(null);
const [primaryActionsConcealed, setPrimaryActionsConcealed] =
@@ -398,6 +401,7 @@ export function ProfileSummaryView({
>
<ProfileHero
displayName={displayName}
notManagedOnDevice={notManagedOnDevice}
isBot={isBot}
onEditAgent={canEditAgent ? handleEditAgent : undefined}
presenceStatus={presenceStatus}
@@ -600,6 +604,7 @@ export function ProfileSummaryView({
// ── Hero & metadata ──────────────────────────────────────────────────────────
function ProfileHero({
notManagedOnDevice,
displayName,
isBot,
onEditAgent,
@@ -608,6 +613,7 @@ function ProfileHero({
userStatus,
}: {
displayName: string;
notManagedOnDevice?: boolean;
isBot: boolean;
onEditAgent?: () => void;
presenceStatus: "online" | "away" | "offline" | undefined;
@@ -674,6 +680,7 @@ function ProfileHero({
{displayName}
</span>
{botIndicator}
{notManagedOnDevice ? <OtherSetupAgentMarker /> : null}
<span
aria-hidden="true"
className="pointer-events-none absolute top-1/2 left-full ml-1 -translate-y-1/2 text-muted-foreground opacity-0 transition-[color,opacity] duration-150 ease-out group-hover:text-foreground group-hover:opacity-100 group-focus-visible:opacity-100"
@@ -690,6 +697,7 @@ function ProfileHero({
>
<span className="truncate">{displayName}</span>
{botIndicator}
{notManagedOnDevice ? <OtherSetupAgentMarker /> : null}
</h3>
)}

View File

@@ -1,6 +1,8 @@
import * as React from "react";
import { Activity, Headphones, MessageSquare } from "lucide-react";
import { AgentManagementMarker } from "@/features/agents/ui/OtherSetupAgentMarker";
import { useChannelsQuery } from "@/features/channels/hooks";
import {
useUserProfileQuery,
@@ -390,6 +392,11 @@ function UserProfilePopoverBody({
<div className="min-w-0 flex-1">
<div className="flex items-center gap-1.5">
<HoverPubkeyName displayName={displayName} pubkey={pubkey} />
<AgentManagementMarker
pubkey={pubkey}
ownerPubkey={ownerPubkey}
testId="user-profile-popover-agent-provenance"
/>
{isBotProfile && botIdenticonValue ? (
<BotIdenticon
value={botIdenticonValue}

View File

@@ -7,6 +7,7 @@ import {
ContextMenuTrigger,
} from "@/shared/ui/context-menu";
import { AgentManagementMarker } from "@/features/agents/ui/OtherSetupAgentMarker";
import { ChannelContextMenuItems } from "@/features/sidebar/ui/ChannelContextMenu";
import type { ActiveChannelTurnSummary } from "@/features/agents/activeAgentTurnsStore";
import { formatElapsed } from "@/features/agents/ui/agentSessionUtils";
@@ -336,6 +337,12 @@ export function ChannelMenuButton({
variant="sidebar"
/>
) : null}
{channel.channelType === "dm" && dmParticipants?.length === 1 ? (
<AgentManagementMarker
pubkey={dmParticipants[0].pubkey}
testId={`channel-agent-provenance-${channel.id}`}
/>
) : null}
{activeWorking ? (
<ChannelWorkingBadge
channelName={channel.name}

View File

@@ -13,7 +13,6 @@ import {
resolveMessageLinkRenderTarget,
type ParsedMessageLink,
} from "@/features/messages/lib/messageLink";
import { UserProfilePopover } from "@/features/profile/ui/UserProfilePopover";
import { renderAudioMessageAttachment } from "@/features/messages/ui/AudioMessageAttachment";
import { useChannelNavigation } from "@/shared/context/ChannelNavigationContext";
import { cn } from "@/shared/lib/cn";
@@ -23,7 +22,7 @@ import { rewriteRelayUrl } from "@/shared/lib/mediaUrl";
import { useRelayOrigin } from "@/shared/lib/useRelayOrigin";
import { AttachmentGroup } from "@/shared/ui/attachment";
import { ConfigNudgeCard } from "@/shared/ui/config-nudge-attachment";
import { InlineChip } from "@/shared/ui/InlineChip";
import { MarkdownMention } from "./markdown/MarkdownMention";
import { LinkPreviewList } from "@/shared/ui/link-preview-list";
import { useSmoothCorners } from "@/shared/ui/smoothCorners";
import {
@@ -1576,48 +1575,9 @@ export function createMarkdownComponents(
ul: ({ children }) => (
<ul className={cn("list-disc", listClassName)}>{children}</ul>
),
mention: function MarkdownMention({
children,
}: {
children?: React.ReactNode;
}) {
const { agentMentionPubkeysByName, mentionPubkeysByName } =
useMarkdownRuntime();
const mentionText = String(children ?? "");
const mentionName = mentionText.replace(/^@/, "").trim().toLowerCase();
const pubkey = mentionPubkeysByName?.[mentionName];
const isAgentMention =
pubkey !== undefined &&
agentMentionPubkeysByName?.[mentionName] === pubkey;
const mentionLabel = mentionText.replace(/^@/, "");
// Only chips that actually open a profile get the clickable affordance.
// A mention whose pubkey didn't resolve stays a plain chip — a pointer
// cursor there promises a click that does nothing.
const opensProfile = interactive && pubkey !== undefined;
const mentionNode = (
<InlineChip
data-mention=""
className={cn(isAgentMention && "agent-mention-highlight")}
icon={isAgentMention ? "agent" : "human"}
interactive={opensProfile}
>
{mentionLabel}
</InlineChip>
);
return opensProfile ? (
<UserProfilePopover
botIdenticonValue={mentionLabel}
pubkey={pubkey}
role={isAgentMention ? "bot" : undefined}
triggerElement="span"
>
{mentionNode}
</UserProfilePopover>
) : (
mentionNode
);
},
mention: ({ children }: { children?: React.ReactNode }) => (
<MarkdownMention interactive={interactive}>{children}</MarkdownMention>
),
emoji: ({ src, alt }: { src?: string; alt?: string }) => {
const resolvedSrc = src ? rewriteRelayUrl(src) : src;
if (!resolvedSrc) {

View File

@@ -0,0 +1,52 @@
import type * as React from "react";
import { AgentManagementMarker } from "@/features/agents/ui/OtherSetupAgentMarker";
import { UserProfilePopover } from "@/features/profile/ui/UserProfilePopover";
import { InlineChip } from "@/shared/ui/InlineChip";
import { cn } from "@/shared/lib/cn";
import { useMarkdownRuntime } from "./runtimeContext";
/** Exact-identity mention chip and its shared management provenance. */
export function MarkdownMention({
children,
interactive,
}: {
children?: React.ReactNode;
interactive: boolean;
}) {
const { agentMentionPubkeysByName, mentionPubkeysByName } =
useMarkdownRuntime();
const mentionText = String(children ?? "");
const mentionName = mentionText.replace(/^@/, "").trim().toLowerCase();
const pubkey = mentionPubkeysByName?.[mentionName];
const isAgentMention =
pubkey !== undefined && agentMentionPubkeysByName?.[mentionName] === pubkey;
const mentionLabel = mentionText.replace(/^@/, "");
// Only chips that actually open a profile get the clickable affordance.
// A mention whose pubkey didn't resolve stays a plain chip — a pointer
// cursor there promises a click that does nothing.
const opensProfile = interactive && pubkey !== undefined;
const mentionNode = (
<InlineChip
data-mention=""
className={cn(isAgentMention && "agent-mention-highlight")}
icon={isAgentMention ? "agent" : "human"}
interactive={opensProfile}
>
{mentionLabel}
{isAgentMention ? <AgentManagementMarker pubkey={pubkey} /> : null}
</InlineChip>
);
return opensProfile ? (
<UserProfilePopover
botIdenticonValue={mentionLabel}
pubkey={pubkey}
role={isAgentMention ? "bot" : undefined}
triggerElement="span"
>
{mentionNode}
</UserProfilePopover>
) : (
mentionNode
);
}

View File

@@ -0,0 +1,200 @@
import { expect, test } from "@playwright/test";
import { waitForAnimations } from "../helpers/animations";
import { installMockBridge, openNewMessagePage } from "../helpers/bridge";
const OWNER = "deadbeef".repeat(8);
const REMOTE = "ed".repeat(32);
const GENERAL = "9a1657ac-f7aa-5db0-b632-d8bbeb6dfb50";
const LABEL = "Not managed on this device";
// The delayed fixture exceeds the marker assertion's 5s deadline, proving that
// source readiness is awaited independently of whether a cloud ever renders.
for (const agentListDelayMs of [0, 6_000]) {
test(`existing owned member uses one cloud across picker, members, author, chip, hover, profile and DM (directory delay ${agentListDelayMs}ms)`, async ({
page,
}, testInfo) => {
await installMockBridge(page, {
agentListDelayMs,
managedAgents: [],
searchProfiles: [
{
pubkey: REMOTE,
displayName: "Remote Scout",
ownerPubkey: OWNER,
isAgent: true,
},
],
relayAgents: [
{
pubkey: REMOTE,
name: "Remote Scout",
ownerPubkey: OWNER,
respondTo: "owner-only",
channelNames: ["general"],
status: "offline",
},
],
});
await page.goto("/");
await page.getByTestId("channel-general").click();
await expect(page.getByTestId("chat-title")).toHaveText("general");
// The channel shell can mount before the provider's directory queries settle.
// Wait for successful source reads, not a delay or a larger marker timeout:
// the assertions below must still fail if ready data renders no cloud.
await expect
.poll(
() =>
page.evaluate(() => {
const client = window.__BUZZ_E2E_QUERY_CLIENT__;
return Object.fromEntries(
["identity", "managed-agents", "relay-agents"].map((key) => {
const state = client?.getQueryState([key]);
return [
key,
{ status: state?.status, fetchStatus: state?.fetchStatus },
];
}),
);
}),
{
message:
"Cloud provenance requires successful identity and agent directory reads",
timeout: 10_000,
},
)
.toMatchObject({
identity: { status: "success" },
"managed-agents": { status: "success" },
"relay-agents": { status: "success" },
});
// Fixture setup only: the UI workflow starts with membership and a DM already present.
const dm = await page.evaluate(
async ({ pubkey, channelId }) => {
await window.__BUZZ_E2E_INVOKE_MOCK_COMMAND__?.("add_channel_members", {
channelId,
pubkeys: [pubkey],
role: "bot",
});
const dm = (await window.__BUZZ_E2E_INVOKE_MOCK_COMMAND__?.("open_dm", {
pubkeys: [pubkey],
})) as { id: string };
await window.__BUZZ_E2E_QUERY_CLIENT__?.invalidateQueries({
queryKey: ["channels"],
});
return dm;
},
{ pubkey: REMOTE, channelId: GENERAL },
);
await page.getByTestId("channel-general").click();
const input = page.getByTestId("message-input");
await input.fill("@Remote");
const suggestion = page.getByTestId(`mention-suggestion-${REMOTE}`);
await expect(
suggestion.getByRole("img", { name: LABEL }).locator("svg.lucide-cloud"),
).toBeVisible();
await waitForAnimations(page);
await page
.getByTestId("mention-autocomplete")
.screenshot({ path: testInfo.outputPath("cloud-picker.png") });
await input.press("Escape");
await input.fill("");
await page.getByTestId("channel-members-trigger").click();
const members = page.getByTestId("members-sidebar");
const memberMarker = page.getByTestId(
`sidebar-member-agent-provenance-${REMOTE}`,
);
await expect(memberMarker).toHaveAttribute("aria-label", LABEL);
await expect(memberMarker.locator("svg.lucide-cloud")).toBeVisible();
await waitForAnimations(page);
await members.screenshot({
path: testInfo.outputPath("cloud-members.png"),
});
await members.getByRole("button", { name: "Close", exact: true }).click();
await expect
.poll(() =>
page.evaluate(() =>
window.__BUZZ_E2E_HAS_MOCK_LIVE_SUBSCRIPTION__?.({
channelName: "general",
}),
),
)
.toBe(true);
await page.evaluate((pubkey) => {
window.__BUZZ_E2E_EMIT_MOCK_MESSAGE__?.({
channelName: "general",
content: "Cloud provenance example: @Remote Scout",
pubkey,
// A self-reference is non-notifying, but still resolves a mention chip.
extraTags: [["mention", pubkey]],
kind: 40002,
});
}, REMOTE);
const article = page
.getByTestId("message-row")
.filter({ hasText: "Cloud provenance example:" });
await expect(
article.getByTestId("message-header").getByRole("img", { name: LABEL }),
).toBeVisible();
const chip = article.locator("[data-mention]");
await expect(chip.locator("svg.lucide-cloud")).toBeVisible();
await waitForAnimations(page);
await article.screenshot({
path: testInfo.outputPath("cloud-author-chip.png"),
});
await chip.hover();
const hover = page.getByTestId("user-profile-popover");
await expect(
hover.getByTestId("user-profile-popover-agent-provenance"),
).toHaveAttribute("title", LABEL);
await waitForAnimations(page);
await hover.screenshot({ path: testInfo.outputPath("cloud-hover.png") });
await chip.click();
const panel = page.getByTestId("user-profile-panel");
await expect(
panel.getByRole("img", { name: LABEL }).locator("svg.lucide-cloud"),
).toBeVisible();
await waitForAnimations(page);
await panel.screenshot({ path: testInfo.outputPath("cloud-profile.png") });
await page.getByTestId("auxiliary-panel-close").click();
await expect(
page
.getByTestId(`channel-agent-provenance-${dm.id}`)
.locator("svg.lucide-cloud"),
).toBeVisible();
await page.locator(`[data-channel-id="${dm.id}"]`).click();
await expect(
page
.getByTestId("chat-header-agent-provenance")
.locator("svg.lucide-cloud"),
).toBeVisible();
await waitForAnimations(page);
await page.screenshot({ path: testInfo.outputPath("cloud-dm.png") });
await openNewMessagePage(page);
await page.getByTestId("new-dm-search").fill("Remote Scout");
const recipient = page.getByTestId(`new-dm-result-${REMOTE}`);
await expect(
recipient.getByRole("img", { name: LABEL }).locator("svg.lucide-cloud"),
).toBeVisible();
await waitForAnimations(page);
await recipient.screenshot({
path: testInfo.outputPath("cloud-recipient.png"),
});
await page.getByTestId("channel-random").click();
await page.getByTestId("channel-members-trigger").click();
await page.getByPlaceholder("Add people and agents").fill("Remote Scout");
const addResult = page.getByTestId(`channel-user-search-result-${REMOTE}`);
await expect(
addResult.getByRole("img", { name: LABEL }).locator("svg.lucide-cloud"),
).toBeVisible();
await waitForAnimations(page);
await addResult.screenshot({
path: testInfo.outputPath("cloud-add-member-picker.png"),
});
// No invitation or local lifecycle action is exercised by this presentation test.
await expect(
page.getByRole("button", { name: "Invite", exact: true }),
).toHaveCount(0);
});
}

View File

@@ -386,11 +386,11 @@ test("duplicate owned agents preserve provenance and exact pubkey selection", as
);
await expect(relayProvenanceMarker).toHaveAttribute(
"aria-label",
"From another Buzz setup",
"Not managed on this device",
);
await expect(relayProvenanceMarker).toHaveAttribute(
"title",
"From another Buzz setup",
"Not managed on this device",
);
await expect(relayProvenanceMarker).toBeVisible();
await expect(relayProvenanceMarker).toHaveText("");
@@ -465,7 +465,7 @@ test("duplicate owned agents preserve provenance and exact pubkey selection", as
).toHaveCount(0);
await expect(remoteSidebarMarker).toHaveAttribute(
"aria-label",
"From another Buzz setup",
"Not managed on this device",
);
await expect(remoteSidebarMarker).toHaveText("");
await expect(remoteSidebarMarker.locator("svg")).toBeVisible();

View File

@@ -0,0 +1,30 @@
# Agent management provenance
The shared **Not managed on this device** cloud marker means a viewer-owned
identity has no exact-key record in successfully loaded local managed inventory.
The glyph is the approved visual convention, not proof of physical cloud hosting.
Locally managed provider-backed agents, unknown/failed local inventory, unknown
owners, and other people's identities do not get the marker. Neither lifecycle
state nor relay presence decides management provenance.
`KnownAgentPubkeysProvider` supplies stable inventory and ownership context to
identity surfaces; rows must not create their own directory query observers.
Picker candidates preserve exact local-management flags and use the same pure
predicate. Profile ownership can supplement the shared directory evidence.
Pickers, member cards, authors, markdown mention chips, address controls, hover
cards, profile headers/hero/subviews, new-message recipients and DM headers/sidebar
rows use the same glyph and accessible label. The marker grants no membership,
mention eligibility, availability or local management capability. This change does
not change profile navigation, presence, native discovery or invitation/routing.
Regression gates: `otherSetupAgent.test.mjs`, `useKnownAgentPubkeys.test.mjs`,
`buildMentionCandidates.test.mjs`, `MentionAutocomplete.test.mjs`, and
`tests/e2e/cloud-provenance.spec.ts`. Browser evidence is mock-Tauri rendered UI,
using an already eligible channel member and an existing DM, not a discovery or
invitation acceptance test. `mentions.spec.ts` retains its marker-label assertions.
The cloud smoke waits (at most 10 seconds, with per-query status diagnostics) for
successful identity, local inventory and relay directory reads before opening the
picker. Both immediate and delayed-directory fixtures exercise the same rendered
workflow. Each cloud assertion retains its own normal deadline: successful data
loading must not hide a missing glyph or accessible marker.