From c8d85ef3d8e09ba87fc96cf331a278a9ad434754 Mon Sep 17 00:00:00 2001 From: Taylor Ho Date: Sun, 14 Jun 2026 22:23:47 -0700 Subject: [PATCH] feat(agents): refine activity visibility ownership checks - Extend canViewAgentActivity coverage for relay-backed agent sessions - Harden Tauri ownership resolution and observer relay scoping - Wire updated visibility rules through profile panel and popover surfaces --- .../agents/hooks/useCanViewAgentActivity.ts | 1 + .../agents/lib/canViewAgentActivity.test.mjs | 30 +++++++++++++++++ .../agents/lib/canViewAgentActivity.ts | 6 ++-- .../src/features/agents/observerRelayStore.ts | 24 ++++++++++++-- .../profile/ui/UserProfilePanelSections.tsx | 7 ++-- .../profile/ui/UserProfilePopover.tsx | 5 +-- desktop/src/shared/api/tauriAgentOwnership.ts | 32 +++++++++++++------ 7 files changed, 87 insertions(+), 18 deletions(-) diff --git a/desktop/src/features/agents/hooks/useCanViewAgentActivity.ts b/desktop/src/features/agents/hooks/useCanViewAgentActivity.ts index a6bc220fd..33f0d2d48 100644 --- a/desktop/src/features/agents/hooks/useCanViewAgentActivity.ts +++ b/desktop/src/features/agents/hooks/useCanViewAgentActivity.ts @@ -38,6 +38,7 @@ export function useCanViewAgentActivity( relayOwnership: ownershipQuery.data, isManagedAgent, isOwnershipLoading: ownershipQuery.isLoading, + isOwnershipError: ownershipQuery.isError, isManagedLoading: isManagedAgent === undefined, }); } diff --git a/desktop/src/features/agents/lib/canViewAgentActivity.test.mjs b/desktop/src/features/agents/lib/canViewAgentActivity.test.mjs index cfd3c8d87..5fad347de 100644 --- a/desktop/src/features/agents/lib/canViewAgentActivity.test.mjs +++ b/desktop/src/features/agents/lib/canViewAgentActivity.test.mjs @@ -12,6 +12,7 @@ test("resolveCanViewAgentActivity returns true when relay confirms ownership", ( }, isManagedAgent: false, isOwnershipLoading: false, + isOwnershipError: false, isManagedLoading: false, }); @@ -28,6 +29,7 @@ test("resolveCanViewAgentActivity returns false when relay denies ownership", () }, isManagedAgent: true, isOwnershipLoading: false, + isOwnershipError: false, isManagedLoading: false, }); @@ -40,6 +42,7 @@ test("resolveCanViewAgentActivity optimistically allows locally managed agents w relayOwnership: undefined, isManagedAgent: true, isOwnershipLoading: true, + isOwnershipError: false, isManagedLoading: false, }); @@ -52,9 +55,36 @@ test("resolveCanViewAgentActivity stays closed for non-managed agents while load relayOwnership: undefined, isManagedAgent: false, isOwnershipLoading: true, + isOwnershipError: false, isManagedLoading: false, }); assert.equal(result.canView, false); assert.equal(result.isLoading, true); }); + +test("resolveCanViewAgentActivity keeps locally managed agents visible when ownership lookup errors", () => { + const result = resolveCanViewAgentActivity({ + relayOwnership: undefined, + isManagedAgent: true, + isOwnershipLoading: false, + isOwnershipError: true, + isManagedLoading: false, + }); + + assert.equal(result.canView, true); + assert.equal(result.isLoading, false); +}); + +test("resolveCanViewAgentActivity stays closed for non-managed agents when ownership lookup errors", () => { + const result = resolveCanViewAgentActivity({ + relayOwnership: undefined, + isManagedAgent: false, + isOwnershipLoading: false, + isOwnershipError: true, + isManagedLoading: false, + }); + + assert.equal(result.canView, false); + assert.equal(result.isLoading, false); +}); diff --git a/desktop/src/features/agents/lib/canViewAgentActivity.ts b/desktop/src/features/agents/lib/canViewAgentActivity.ts index 436a68169..fea48ecfb 100644 --- a/desktop/src/features/agents/lib/canViewAgentActivity.ts +++ b/desktop/src/features/agents/lib/canViewAgentActivity.ts @@ -4,6 +4,7 @@ export type CanViewAgentActivityInput = { relayOwnership: AgentOwnershipStatus | undefined; isManagedAgent: boolean | undefined; isOwnershipLoading: boolean; + isOwnershipError: boolean; isManagedLoading: boolean; }; @@ -22,6 +23,7 @@ export function resolveCanViewAgentActivity({ relayOwnership, isManagedAgent, isOwnershipLoading, + isOwnershipError, isManagedLoading, }: CanViewAgentActivityInput): CanViewAgentActivityResult { if (relayOwnership?.isOwner === true) { @@ -35,8 +37,8 @@ export function resolveCanViewAgentActivity({ const isLoading = isOwnershipLoading || (isManagedAgent === undefined && isManagedLoading); - if (isManagedAgent === true && isOwnershipLoading) { - return { canView: true, isLoading: true }; + if (isManagedAgent === true && (isOwnershipLoading || isOwnershipError)) { + return { canView: true, isLoading }; } return { canView: false, isLoading }; diff --git a/desktop/src/features/agents/observerRelayStore.ts b/desktop/src/features/agents/observerRelayStore.ts index ee8483dc8..cc8905d0a 100644 --- a/desktop/src/features/agents/observerRelayStore.ts +++ b/desktop/src/features/agents/observerRelayStore.ts @@ -41,6 +41,7 @@ const snapshotByAgent = new Map(); // Normalized pubkeys of agents we are actively managing. Only events whose // "agent" tag matches an entry here will be decrypted (defense-in-depth). const knownAgentPubkeys = new Set(); +const knownAgentPubkeysByBridge = new Map>(); let connectionState: ConnectionState = "idle"; let errorMessage: string | null = null; @@ -275,6 +276,7 @@ export function getAgentTranscript( export function useManagedAgentObserverBridge( agents: readonly Pick[], ) { + const bridgeIdRef = React.useRef(Symbol("managed-agent-observer")); const hasActiveAgent = React.useMemo( () => agents.some( @@ -285,10 +287,27 @@ export function useManagedAgentObserverBridge( // Keep the trusted-pubkey set in sync with the current managed agent list. React.useEffect(() => { + const bridgeId = bridgeIdRef.current; + knownAgentPubkeysByBridge.set( + bridgeId, + new Set(agents.map((agent) => normalizePubkey(agent.pubkey))), + ); knownAgentPubkeys.clear(); - for (const agent of agents) { - knownAgentPubkeys.add(normalizePubkey(agent.pubkey)); + for (const pubkeys of knownAgentPubkeysByBridge.values()) { + for (const pubkey of pubkeys) { + knownAgentPubkeys.add(pubkey); + } } + + return () => { + knownAgentPubkeysByBridge.delete(bridgeId); + knownAgentPubkeys.clear(); + for (const pubkeys of knownAgentPubkeysByBridge.values()) { + for (const pubkey of pubkeys) { + knownAgentPubkeys.add(pubkey); + } + } + }; }, [agents]); React.useEffect(() => { @@ -309,6 +328,7 @@ export function resetAgentObserverStore() { transcriptByAgent.clear(); snapshotByAgent.clear(); knownAgentPubkeys.clear(); + knownAgentPubkeysByBridge.clear(); connectionState = "idle"; errorMessage = null; notifyListeners(); diff --git a/desktop/src/features/profile/ui/UserProfilePanelSections.tsx b/desktop/src/features/profile/ui/UserProfilePanelSections.tsx index 0b550f80d..a4278469d 100644 --- a/desktop/src/features/profile/ui/UserProfilePanelSections.tsx +++ b/desktop/src/features/profile/ui/UserProfilePanelSections.tsx @@ -124,7 +124,8 @@ export function ProfileSummaryView({ userStatus, }: ProfileSummaryViewProps) { const { goChannel } = useAppNavigation(); - const activeTurns = useActiveAgentTurns(isBot ? pubkey : null); + const activeTurns = useActiveAgentTurns(pubkey); + const canShowActivity = canViewActivity || activeTurns.length > 0; const metadataFields = [ ...buildPublicFields({ @@ -185,7 +186,7 @@ export function ProfileSummaryView({ ) : null} - {showMemoriesIngress || showChannelsIngress || canViewActivity ? ( + {showMemoriesIngress || showChannelsIngress || canShowActivity ? (
{showMemoriesIngress ? ( ) : null} - {canViewActivity ? ( + {canShowActivity ? ( 0; const channelsQuery = useChannelsQuery(); const channelIdToName = React.useMemo(() => { const map: Record = {}; @@ -278,7 +279,7 @@ export function UserProfilePopover({

) : null} - {canViewActivity && onOpenAgentSession ? ( + {canShowActivity && onOpenAgentSession ? (