diff --git a/frontend/src/v2/__tests__/V2AgentMemory.test.tsx b/frontend/src/v2/__tests__/V2AgentMemory.test.tsx index 39676cdc7..45e8bbd04 100644 --- a/frontend/src/v2/__tests__/V2AgentMemory.test.tsx +++ b/frontend/src/v2/__tests__/V2AgentMemory.test.tsx @@ -112,7 +112,7 @@ describe('V2AgentMemory', () => { expect(document.querySelectorAll('.v2-agent-memory__mnote .v2-agent-memory__gmeta')).toHaveLength(0); }); - it('shows a denied notice instead of the index when the viewer is not owner or admin', async () => { + it('keeps the agent header, tabs and nav when the viewer cannot read entries, with the empty state inside', async () => { axios.get.mockRejectedValue(new Error('403')); render( @@ -122,6 +122,9 @@ describe('V2AgentMemory', () => { , ); await waitFor(() => expect(screen.getByRole('status')).toBeInTheDocument()); - expect(screen.queryByRole('heading', { name: /Rules/ })).toBeNull(); + expect(screen.getByRole('heading', { level: 1 })).toBeInTheDocument(); + expect(screen.getByRole('link', { name: 'Profile' })).toBeInTheDocument(); + expect(document.querySelector('.v2-agent-memory__nav')).not.toBeNull(); + expect(document.querySelector('.v2-agent-memory__body [role="status"]')).not.toBeNull(); }); }); diff --git a/frontend/src/v2/agents/V2AgentMemory.tsx b/frontend/src/v2/agents/V2AgentMemory.tsx index a7e40330b..a7f82dedc 100644 --- a/frontend/src/v2/agents/V2AgentMemory.tsx +++ b/frontend/src/v2/agents/V2AgentMemory.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react'; import { Link, useParams } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import axios from '../../utils/axiosConfig'; -import V2PageHead from '../components/V2PageHead'; +import V2Avatar from '../components/V2Avatar'; import { localizeRelativeTime } from '../utils/localizeRelativeTime'; import './v2-agent-settings.css'; @@ -80,12 +80,52 @@ const MemIcon: React.FC<{ name: keyof typeof ICON_PATHS; size?: number; classNam ); const GROUP_ICON: Record = { rules: 'pin', how: 'brain', journal: 'cal' }; +// The D215 frame without entries: same nav and body, the state sits inside it. +const MemoryShell: React.FC<{ loading: boolean; message: string; loadingLabel: string; query: string; setQuery: (q: string) => void; searchLabel: string; navLabel: string }> = ({ loading, message, loadingLabel, query, setQuery, searchLabel, navLabel }) => ( +
+ +
+

{loading ? loadingLabel : message}

+
+
+); + const V2AgentMemory: React.FC = () => { const { t } = useTranslation(); const { agentName = '', instanceId = '' } = useParams<{ agentName: string; instanceId: string }>(); const [index, setIndex] = useState(null); const [state, setState] = useState<'loading' | 'ready' | 'denied'>('loading'); const [query, setQuery] = useState(''); + // Public header facts (name, avatar, pod count). Readable by any viewer, so the + // shell renders the same for owners, admins and everyone else. + const [head, setHead] = useState<{ displayName: string; profilePicture?: string; pods: number } | null>(null); + + useEffect(() => { + let cancelled = false; + axios + .get(`/api/agent-profile/${encodeURIComponent(agentName)}/${encodeURIComponent(instanceId || 'default')}`) + .then((res) => { + if (!cancelled) setHead({ displayName: res.data?.agent?.displayName || agentName, profilePicture: res.data?.agent?.profilePicture, pods: res.data?.pods?.count ?? 0 }); + }) + .catch(() => { if (!cancelled) setHead(null); }); + return () => { cancelled = true; }; + }, [agentName, instanceId]); useEffect(() => { let cancelled = false; @@ -108,19 +148,19 @@ const V2AgentMemory: React.FC = () => { return (
- - - - {state === 'loading' &&

{t('common.loading', { defaultValue: 'Loading…' })}

} - {state === 'denied' && ( -

- {t('agentProfile.memory.none', { defaultValue: 'Memory is visible to the agent owner and admins.' })} -

- )} +
+ +
+

{head?.displayName || agentName}

+ {head &&

{t('agentProfile.hero.activeInPods', { count: head.pods })}

} +
+ +
+ {state === 'ready' && index ? null : } {state === 'ready' && index && (() => { const needle = query.trim().toLowerCase(); const groups = groupSections(index.sections) diff --git a/frontend/src/v2/agents/v2-agent-settings.css b/frontend/src/v2/agents/v2-agent-settings.css index 51d0ad260..d97b451c0 100644 --- a/frontend/src/v2/agents/v2-agent-settings.css +++ b/frontend/src/v2/agents/v2-agent-settings.css @@ -174,3 +174,10 @@ .v2-agent-memory__snip { font-size: 12.5px; line-height: 18px; color: var(--v2-text-secondary); } .v2-agent-memory__jday { display: grid; grid-template-columns: 70px minmax(0, 1fr); gap: 12px; padding: 10px 14px; font-size: 13.5px; color: var(--v2-text-secondary); } .v2-agent-memory__jdate { font-size: 11.5px; color: var(--v2-text-tertiary); line-height: 20px; } + +/* Memory page header: the D215 agent bar (avatar, name, pods, segment). */ +.v2-agent-memory__head { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; margin-bottom: 20px; } +.v2-agent-memory__ident h1 { margin: 0; font-size: 20px; } +.v2-agent-memory__ident p { margin: 2px 0 0; font-size: 12.5px; color: var(--v2-text-tertiary); } +.v2-agent-memory__seg { margin-left: auto; margin-bottom: 0; } +.v2-agent-memory__navitem { display: flex; align-items: center; gap: 9px; height: 34px; padding: 0 10px; font-size: 13.5px; color: var(--v2-text-tertiary); }