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 ? (