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 { localizeRelativeTime } from '../utils/localizeRelativeTime'; import './v2-agent-settings.css'; // Owner/admin memory index, as served by GET /api/agent-memory/:agentName/:instanceId. interface MemoryNote { header: string; snippet: string } interface MemorySection { key: string; label: string; kind: string; updatedAt?: string | null; notes: MemoryNote[] } interface MemoryIndex { viewerRole: 'owner' | 'admin'; totalEntries: number; sections: MemorySection[] } // Which backend sections land in which group. Journal is the dated log. Soul // is a rule-like section and sits at the top of Rules. Long-term memory is split // by its own headings (see splitLongTerm). Any other section lands in How I work // so nothing is hidden. Change the mapping here only. export const MEMORY_GROUPS = [ { id: 'rules', label: 'Rules' }, { id: 'how', label: 'How I work' }, { id: 'journal', label: 'Journal' }, ] as const; type GroupId = typeof MEMORY_GROUPS[number]['id']; interface GroupedSection { key: string; label: string; updatedAt: string | null; notes: MemoryNote[] } const JOURNAL_KEYS = new Set(['daily', 'cycles']); // A long_term heading whose first word names a rule goes under Rules. const RULE_HEADING = /^(rules?|never|always|don'?t)\b/i; const sectionGroup = (key: string): GroupId => (JOURNAL_KEYS.has(key) ? 'journal' : 'how'); // Splits a long_term section's headings into Rules and How I work. The // "Overview" note (text before the first heading) is How I work. export const splitLongTerm = (section: MemorySection): Record<'rules' | 'how', GroupedSection | null> => { const rules = section.notes.filter((n) => RULE_HEADING.test(n.header)); const how = section.notes.filter((n) => !RULE_HEADING.test(n.header)); const make = (notes: MemoryNote[]): GroupedSection | null => ( notes.length ? { key: section.key, label: section.label, updatedAt: section.updatedAt ?? null, notes } : null ); return { rules: make(rules), how: make(how) }; }; export const groupSections = (sections: MemorySection[]) => { const byGroup: Record = { rules: [], how: [], journal: [] }; // Soul first, so it leads Rules. const soul = sections.find((s) => s.key === 'soul'); if (soul) byGroup.rules.push({ key: soul.key, label: soul.label, updatedAt: soul.updatedAt ?? null, notes: soul.notes }); for (const s of sections) { if (s.key === 'soul') continue; if (s.key === 'long_term') { const split = splitLongTerm(s); if (split.rules) byGroup.rules.push(split.rules); if (split.how) byGroup.how.push(split.how); continue; } byGroup[sectionGroup(s.key)].push({ key: s.key, label: s.label, updatedAt: s.updatedAt ?? null, notes: s.notes }); } return MEMORY_GROUPS.map((g) => ({ id: g.id, label: g.label, sections: byGroup[g.id], })); }; // Frame icons (docs/design/artboards/d215, #s-memory): stroke-only, 1.8 weight. const ICON_PATHS: Record<'pin' | 'brain' | 'cal' | 'search' | 'info' | 'chev', React.ReactNode> = { chev: , pin: , brain: , cal: <>, search: <>, info: <>, }; const MemIcon: React.FC<{ name: keyof typeof ICON_PATHS; size?: number; className?: string }> = ({ name, size = 16, className }) => ( ); const GROUP_ICON: Record = { rules: 'pin', how: 'brain', journal: 'cal' }; 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(''); useEffect(() => { let cancelled = false; setState('loading'); axios .get(`/api/agent-memory/${encodeURIComponent(agentName)}/${encodeURIComponent(instanceId || 'default')}`) .then((res) => { if (cancelled) return; setIndex(res.data); setState('ready'); }) .catch(() => { if (!cancelled) setState('denied'); }); return () => { cancelled = true; }; }, [agentName, instanceId]); const profileHref = `/v2/agent/${encodeURIComponent(agentName)}/${encodeURIComponent(instanceId)}`; const settingsHref = `/v2/agents/${encodeURIComponent(agentName)}/${encodeURIComponent(instanceId)}`; return (
{state === 'loading' &&

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

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

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

)} {state === 'ready' && index && (() => { const needle = query.trim().toLowerCase(); const groups = groupSections(index.sections) .map((group) => ({ ...group, sections: group.sections .map((sec) => ({ ...sec, notes: needle ? sec.notes.filter((n) => `${n.header} ${n.snippet}`.toLowerCase().includes(needle)) : sec.notes, })) .filter((sec) => sec.notes.length > 0), })); return (
{groups.filter((g) => g.sections.length > 0).map((group) => { // One time per group: the newest section update in it. Never per note. const updated = group.sections.map((s) => s.updatedAt).filter(Boolean).sort().pop() ?? null; return (
{group.label} {updated && ( {t('agentProfile.memory.sectionUpdated', { defaultValue: 'Section updated {{when}}', when: localizeRelativeTime(updated, t) })} )}
    {group.sections.flatMap((sec) => sec.notes).map((n, i) => ( group.id === 'journal' ? (
  • {n.header} {n.snippet}
  • ) : (
  • {n.header}
    {n.snippet &&
    {n.snippet}
    }
  • ) ))}
); })}
); })()}
); }; export default V2AgentMemory;