diff --git a/desktop/src/app/routes/channels.$channelId.tsx b/desktop/src/app/routes/channels.$channelId.tsx index 3c64f3ec9..1607684fd 100644 --- a/desktop/src/app/routes/channels.$channelId.tsx +++ b/desktop/src/app/routes/channels.$channelId.tsx @@ -1,13 +1,17 @@ import * as React from "react"; import { createFileRoute } from "@tanstack/react-router"; +import { + parseProfilePanelView, + type ProfilePanelView, +} from "@/features/profile/ui/UserProfilePanelUtils"; import { ViewLoadingFallback } from "@/shared/ui/ViewLoadingFallback"; type ChannelRouteSearch = { agentSession?: string; messageId?: string; profile?: string; - profileView?: "memories" | "channels"; + profileView?: ProfilePanelView; thread?: string; threadRootId?: string; }; @@ -16,8 +20,8 @@ function nonEmptyString(value: unknown): string | undefined { return typeof value === "string" && value.length > 0 ? value : undefined; } -function profileViewValue(value: unknown): "memories" | "channels" | undefined { - return value === "memories" || value === "channels" ? value : undefined; +function profileViewValue(value: unknown): ProfilePanelView | undefined { + return parseProfilePanelView(value) ?? undefined; } function validateChannelSearch( diff --git a/desktop/src/app/routes/pulse.tsx b/desktop/src/app/routes/pulse.tsx index e1a5a001e..949b56d0b 100644 --- a/desktop/src/app/routes/pulse.tsx +++ b/desktop/src/app/routes/pulse.tsx @@ -1,6 +1,10 @@ import * as React from "react"; import { createFileRoute } from "@tanstack/react-router"; +import { + parseProfilePanelView, + type ProfilePanelView, +} from "@/features/profile/ui/UserProfilePanelUtils"; import { usePreviewFeatureWarning } from "@/shared/features"; import { ViewLoadingFallback } from "@/shared/ui/ViewLoadingFallback"; @@ -11,7 +15,7 @@ const PulseScreen = React.lazy(async () => { type PulseRouteSearch = { profile?: string; - profileView?: "memories" | "channels"; + profileView?: ProfilePanelView; }; function validatePulseSearch( @@ -22,10 +26,7 @@ function validatePulseSearch( typeof search.profile === "string" && search.profile.length > 0 ? search.profile : undefined, - profileView: - search.profileView === "memories" || search.profileView === "channels" - ? search.profileView - : undefined, + profileView: parseProfilePanelView(search.profileView) ?? undefined, }; } diff --git a/desktop/src/features/agents/observerRelayStore.test.mjs b/desktop/src/features/agents/observerRelayStore.test.mjs new file mode 100644 index 000000000..93c04733d --- /dev/null +++ b/desktop/src/features/agents/observerRelayStore.test.mjs @@ -0,0 +1,45 @@ +import assert from "node:assert/strict"; +import { beforeEach, describe, it } from "node:test"; + +import { + isKnownAgentPubkey, + registerKnownAgentPubkeys, + resetAgentObserverStore, + unregisterKnownAgentPubkeys, +} from "./observerRelayStore.ts"; + +const AGENT_A = + "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; +const AGENT_B = + "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"; +const AGENT_C = + "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"; + +describe("observerRelayStore known agent registrations", () => { + beforeEach(() => { + resetAgentObserverStore(); + }); + + it("unions known agents from multiple bridge registrations", () => { + const agentsPage = Symbol("agents-page"); + const profilePanel = Symbol("profile-panel"); + + registerKnownAgentPubkeys(agentsPage, [AGENT_A, AGENT_B]); + registerKnownAgentPubkeys(profilePanel, [AGENT_C]); + + assert.equal(isKnownAgentPubkey(AGENT_A), true); + assert.equal(isKnownAgentPubkey(AGENT_B), true); + assert.equal(isKnownAgentPubkey(AGENT_C), true); + + registerKnownAgentPubkeys(profilePanel, []); + + assert.equal(isKnownAgentPubkey(AGENT_A), true); + assert.equal(isKnownAgentPubkey(AGENT_B), true); + assert.equal(isKnownAgentPubkey(AGENT_C), false); + + unregisterKnownAgentPubkeys(agentsPage); + + assert.equal(isKnownAgentPubkey(AGENT_A), false); + assert.equal(isKnownAgentPubkey(AGENT_B), false); + }); +}); diff --git a/desktop/src/features/agents/observerRelayStore.ts b/desktop/src/features/agents/observerRelayStore.ts index 6f3370de0..6e9251a46 100644 --- a/desktop/src/features/agents/observerRelayStore.ts +++ b/desktop/src/features/agents/observerRelayStore.ts @@ -47,7 +47,7 @@ const snapshotByAgent = new Map(); // We key each subscriber's contribution in `knownAgentsBySubscription` and // recompute the union, so co-mounted callers no longer clobber each other. const knownAgentPubkeys = new Set(); -const knownAgentsBySubscription = new Map>(); +const knownAgentsBySubscription = new Map>(); function recomputeKnownAgentPubkeys() { knownAgentPubkeys.clear(); @@ -58,8 +58,8 @@ function recomputeKnownAgentPubkeys() { } } -function registerKnownAgents( - subscriptionId: string, +export function registerKnownAgentPubkeys( + subscriptionId: string | symbol, pubkeys: readonly string[], ) { knownAgentsBySubscription.set( @@ -69,12 +69,16 @@ function registerKnownAgents( recomputeKnownAgentPubkeys(); } -function unregisterKnownAgents(subscriptionId: string) { +export function unregisterKnownAgentPubkeys(subscriptionId: string | symbol) { if (knownAgentsBySubscription.delete(subscriptionId)) { recomputeKnownAgentPubkeys(); } } +export function isKnownAgentPubkey(pubkey: string) { + return knownAgentPubkeys.has(normalizePubkey(pubkey)); +} + let connectionState: ConnectionState = "idle"; let errorMessage: string | null = null; let unsubscribeRelay: (() => Promise) | null = null; @@ -176,7 +180,7 @@ async function handleRelayObserverEvent( // Verify agent is known/trusted before decrypting. // Silently drop events from agents we are not managing. - if (!knownAgentPubkeys.has(normalizePubkey(agentPubkey))) { + if (!isKnownAgentPubkey(agentPubkey)) { return; } @@ -326,9 +330,9 @@ export function useManagedAgentObserverBridge( // own agent list. The store recomputes the union across all subscribers, so // a co-mounted caller no longer wipes out this caller's agents. React.useEffect(() => { - registerKnownAgents(subscriptionId, agentPubkeys); + registerKnownAgentPubkeys(subscriptionId, agentPubkeys); return () => { - unregisterKnownAgents(subscriptionId); + unregisterKnownAgentPubkeys(subscriptionId); }; }, [subscriptionId, agentPubkeys]); diff --git a/desktop/src/features/channels/ui/useChannelPanelHistoryState.ts b/desktop/src/features/channels/ui/useChannelPanelHistoryState.ts index 588a1e6df..e51041f3c 100644 --- a/desktop/src/features/channels/ui/useChannelPanelHistoryState.ts +++ b/desktop/src/features/channels/ui/useChannelPanelHistoryState.ts @@ -1,6 +1,9 @@ import * as React from "react"; -import type { ProfilePanelView } from "@/features/profile/ui/UserProfilePanel"; +import { + profilePanelViewFromSearch, + type ProfilePanelView, +} from "@/features/profile/ui/UserProfilePanelUtils"; import { type HistorySearchSetterOptions, useHistorySearchState, @@ -36,10 +39,6 @@ const CHANNEL_SEARCH_KEYS = [ const CHANNEL_MANAGEMENT_OPEN_VALUE = "1"; -function asProfilePanelView(value: string | null): ProfilePanelView { - return value === "memories" || value === "channels" ? value : "summary"; -} - export function useChannelPanelHistoryState() { const { applyPatch, values } = useHistorySearchState(CHANNEL_SEARCH_KEYS); @@ -88,7 +87,7 @@ export function useChannelPanelHistoryState() { openAgentSessionPubkey: values.agentSession, openThreadHeadId: values.thread, profilePanelPubkey: values.profile, - profilePanelView: asProfilePanelView(values.profileView), + profilePanelView: profilePanelViewFromSearch(values.profileView), setChannelManagementOpen, setOpenAgentSessionPubkey, setOpenThreadHeadId, diff --git a/desktop/src/features/profile/ui/UserProfilePanel.tsx b/desktop/src/features/profile/ui/UserProfilePanel.tsx index 013f1914e..813c32886 100644 --- a/desktop/src/features/profile/ui/UserProfilePanel.tsx +++ b/desktop/src/features/profile/ui/UserProfilePanel.tsx @@ -735,7 +735,11 @@ export function UserProfilePanel({ const headerActions = (
{view === "memories" && viewerIsOwner && effectivePubkey ? ( - + ) : null}
); } diff --git a/desktop/src/features/profile/ui/UserProfilePanelUtils.test.mjs b/desktop/src/features/profile/ui/UserProfilePanelUtils.test.mjs index d7839790d..287e944d5 100644 --- a/desktop/src/features/profile/ui/UserProfilePanelUtils.test.mjs +++ b/desktop/src/features/profile/ui/UserProfilePanelUtils.test.mjs @@ -85,7 +85,6 @@ test("personaManagedAgentUpdate syncs edited persona identity to linked agent", assert.deepEqual(personaManagedAgentUpdate(agent(), persona()), { pubkey: "deadbeef".repeat(8), name: "Fizz Prime", - avatarUrl: null, systemPrompt: "New prompt", model: "new-model", envVars: { NEW_KEY: "2" }, @@ -121,7 +120,6 @@ test("personaManagedAgentUpdate maps changed persona runtime to linked agent com { pubkey: "deadbeef".repeat(8), name: "Fizz Prime", - avatarUrl: null, systemPrompt: "New prompt", model: "new-model", envVars: { NEW_KEY: "2" }, diff --git a/desktop/src/features/pulse/ui/PulseScreen.tsx b/desktop/src/features/pulse/ui/PulseScreen.tsx index 1baef3b14..1ed60c0c9 100644 --- a/desktop/src/features/pulse/ui/PulseScreen.tsx +++ b/desktop/src/features/pulse/ui/PulseScreen.tsx @@ -3,9 +3,10 @@ import * as React from "react"; import { useAppNavigation } from "@/app/navigation/useAppNavigation"; import { useOpenDmMutation } from "@/features/channels/hooks"; import { + profilePanelViewFromSearch, type ProfilePanelView, - UserProfilePanel, -} from "@/features/profile/ui/UserProfilePanel"; +} from "@/features/profile/ui/UserProfilePanelUtils"; +import { UserProfilePanel } from "@/features/profile/ui/UserProfilePanel"; import { PulseView } from "@/features/pulse/ui/PulseView"; import { useIdentityQuery } from "@/shared/api/hooks"; import { ProfilePanelProvider } from "@/shared/context/ProfilePanelContext"; @@ -18,10 +19,7 @@ export function PulseScreen() { const identityQuery = useIdentityQuery(); const { applyPatch, values } = useHistorySearchState(PULSE_PANEL_SEARCH_KEYS); const profilePanelPubkey = values.profile; - const profilePanelView: ProfilePanelView = - values.profileView === "memories" || values.profileView === "channels" - ? values.profileView - : "summary"; + const profilePanelView = profilePanelViewFromSearch(values.profileView); const handleOpenProfilePanel = React.useCallback( (pubkey: string) => applyPatch({ profile: pubkey, profileView: null }), [applyPatch],