[eric] memory: one user-curated fact store rides every agent turn, fully visible and editable in Settings

This commit is contained in:
ciregenz
2026-08-06 10:42:54 -07:00
parent 09e3116041
commit a64fcef234
11 changed files with 415 additions and 3 deletions
@@ -15,6 +15,7 @@ import AccountCard from './sections/subscription/AccountCard';
import GeneralAgentDefaults from './sections/general/GeneralAgentDefaults';
import GeneralInterface from './sections/general/GeneralInterface';
import DictationSettings from './sections/general/DictationSettings';
import MemorySettings from './sections/general/MemorySettings';
import CanvasSettings from './sections/general/CanvasSettings';
import AgentBehaviorSettings from './sections/general/AgentBehaviorSettings';
import GeneralAdvanced from './sections/general/GeneralAdvanced';
@@ -32,7 +33,7 @@ import { PROVIDER_COLORS, OPENSWARM_GRADIENT, useModelOptions } from './settings
// Module-scope: remember the last open tab across closes (System Settings style).
let lastOpenTab: string | null = null;
const TAB_VALUES = ['account', 'general', 'appearance', 'dictation', 'canvas', 'agents', 'notifications', 'privacy', 'advanced', 'models', 'commands', 'usage'] as const;
const TAB_VALUES = ['account', 'general', 'appearance', 'dictation', 'memory', 'canvas', 'agents', 'notifications', 'privacy', 'advanced', 'models', 'commands', 'usage'] as const;
type SettingsTab = typeof TAB_VALUES[number];
const isValidTab = (t: string | null | undefined): t is SettingsTab =>
!!t && (TAB_VALUES as readonly string[]).includes(t);
@@ -139,6 +140,10 @@ const SettingsBody: React.FC<SettingsBodyProps> = ({ active, onRequestClose }) =
<Box sx={{ pt: 0.5, pb: 2, animation: 'fadeIn 0.2s ease', '@keyframes fadeIn': { from: { opacity: 0 }, to: { opacity: 1 } } }}>
<DictationSettings form={form} setForm={setForm} styles={styles} />
</Box>
) : activeTab === 'memory' ? (
<Box sx={{ pt: 0.5, pb: 2, animation: 'fadeIn 0.2s ease', '@keyframes fadeIn': { from: { opacity: 0 }, to: { opacity: 1 } } }}>
<MemorySettings form={form} setForm={setForm} styles={styles} />
</Box>
) : activeTab === 'canvas' ? (
<Box sx={{ pt: 0.5, pb: 2, animation: 'fadeIn 0.2s ease', '@keyframes fadeIn': { from: { opacity: 0 }, to: { opacity: 1 } } }}>
<CanvasSettings form={form} setForm={setForm} styles={styles} />
@@ -1,7 +1,7 @@
import React from 'react';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import { User, Settings2, Palette, ShieldCheck, Wrench, Boxes, SquareSlash, BarChart3, Bell, Mic, LayoutGrid, Bot } from 'lucide-react';
import { User, Settings2, Palette, ShieldCheck, Wrench, Boxes, SquareSlash, BarChart3, Bell, Mic, LayoutGrid, Bot, Brain } from 'lucide-react';
import type { LucideIcon } from 'lucide-react';
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
@@ -26,6 +26,7 @@ export const RAIL_GROUPS: RailGroup[] = [
{ value: 'general', label: 'General', Icon: Settings2 },
{ value: 'appearance', label: 'Appearance', Icon: Palette },
{ value: 'dictation', label: 'Dictation', Icon: Mic },
{ value: 'memory', label: 'Memory', Icon: Brain },
{ value: 'canvas', label: 'Canvas', Icon: LayoutGrid },
{ value: 'agents', label: 'Agents', Icon: Bot },
{ value: 'notifications', label: 'Notifications', Icon: Bell },
@@ -0,0 +1,138 @@
import React, { useEffect, useState } from 'react';
import Box from '@mui/material/Box';
import Typography from '@mui/material/Typography';
import Switch from '@mui/material/Switch';
import IconButton from '@mui/material/IconButton';
import DeleteOutlineIcon from '@mui/icons-material/DeleteOutline';
import { AppSettings } from '@/shared/state/settingsSlice';
import { useClaudeTokens } from '@/shared/styles/ThemeContext';
import { API_BASE } from '@/shared/config';
import type { SettingsStyles } from '../settingsStyles';
import { settingSelectAttrs } from '../settingSelect';
interface MemoryFact {
id: string;
text: string;
source: string;
updated_at: string;
}
/** What agents know about you: every fact visible, editable, deletable; nothing hidden. */
const MemorySettings: React.FC<{
form: AppSettings;
setForm: React.Dispatch<React.SetStateAction<AppSettings>>;
styles: SettingsStyles;
}> = ({ form, setForm, styles }) => {
const c = useClaudeTokens();
const { inlineRowSx, labelSx, descSx } = styles;
const [facts, setFacts] = useState<MemoryFact[]>([]);
const [draft, setDraft] = useState('');
const [editingId, setEditingId] = useState<string | null>(null);
const [editText, setEditText] = useState('');
const refresh = async (): Promise<void> => {
try {
const res = await fetch(`${API_BASE}/memory`);
if (res.ok) setFacts(((await res.json()) as { facts: MemoryFact[] }).facts);
} catch { /* backend down reads as an empty list, never a crash */ }
};
useEffect(() => { void refresh(); }, []);
const add = async (): Promise<void> => {
const text = draft.trim();
if (!text) return;
setDraft('');
await fetch(`${API_BASE}/memory`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text }) });
void refresh();
};
const saveEdit = async (): Promise<void> => {
if (!editingId) return;
const text = editText.trim();
setEditingId(null);
if (!text) return;
await fetch(`${API_BASE}/memory/${editingId}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text }) });
void refresh();
};
const remove = async (id: string): Promise<void> => {
await fetch(`${API_BASE}/memory/${id}`, { method: 'DELETE' });
void refresh();
};
const inputSx = {
width: '100%',
px: 1.25,
py: 0.7,
borderRadius: '8px',
border: `1px solid ${c.border.subtle}`,
background: c.bg.surface,
color: c.text.primary,
fontSize: '0.8438rem',
fontFamily: 'inherit',
outline: 'none',
'&:focus': { borderColor: c.accent.primary },
} as const;
return (
<>
<Box sx={inlineRowSx} {...settingSelectAttrs('memory_enabled', 'Memory', 'Interface', 'Whether agents see your saved facts.')}>
<Box sx={{ mr: 3 }}>
<Typography sx={labelSx}>Memory</Typography>
<Typography sx={descSx}>Agents remember these facts in every chat. Off means none of them reach any model.</Typography>
</Box>
<Switch size="small" checked={form.memory_enabled !== false} onChange={(e) => setForm({ ...form, memory_enabled: e.target.checked })} />
</Box>
<Box sx={{ ...inlineRowSx, alignItems: 'flex-start', flexDirection: 'column', gap: 1.25 }}>
<Box>
<Typography sx={labelSx}>What agents know about you</Typography>
<Typography sx={descSx}>Add facts yourself, fix wrong ones, delete anything. This list is the whole memory; there is nothing hidden behind it.</Typography>
</Box>
<Box sx={{ width: '100%', display: 'flex', flexDirection: 'column', gap: 0.25 }}>
{facts.length === 0 && (
<Typography sx={{ color: c.text.ghost, fontSize: '0.8125rem', py: 0.5 }}>Nothing saved yet.</Typography>
)}
{facts.map((fact) => (
<Box key={fact.id} sx={{ display: 'flex', alignItems: 'center', gap: 1, py: 0.4, borderBottom: `1px solid ${c.border.subtle}`, '&:last-of-type': { borderBottom: 'none' }, '&:hover .osw-mem-del': { opacity: 1 } }}>
{editingId === fact.id ? (
<Box
component="input"
autoFocus
value={editText}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setEditText(e.target.value)}
onBlur={() => void saveEdit()}
onKeyDown={(e: React.KeyboardEvent) => { if (e.key === 'Enter') void saveEdit(); if (e.key === 'Escape') setEditingId(null); }}
sx={inputSx}
/>
) : (
<Typography
onClick={() => { setEditingId(fact.id); setEditText(fact.text); }}
sx={{ flex: 1, color: c.text.primary, fontSize: '0.8438rem', cursor: 'text', py: 0.3 }}
>
{fact.text}
</Typography>
)}
{fact.source === 'distilled' && (
<Typography sx={{ color: c.text.ghost, fontSize: '0.6875rem', flexShrink: 0 }}>learned</Typography>
)}
<IconButton className="osw-mem-del" size="small" onClick={() => void remove(fact.id)} sx={{ opacity: 0, transition: 'opacity 0.12s', color: c.text.ghost, '&:hover': { color: c.status.error } }}>
<DeleteOutlineIcon sx={{ fontSize: 16 }} />
</IconButton>
</Box>
))}
<Box
component="input"
value={draft}
placeholder="Add a fact agents should always know (Enter to save)"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setDraft(e.target.value)}
onKeyDown={(e: React.KeyboardEvent) => { if (e.key === 'Enter') void add(); }}
sx={{ ...inputSx, mt: 1 }}
/>
</Box>
</Box>
</>
);
};
export default MemorySettings;
@@ -34,6 +34,7 @@ export interface AppSettings {
dictation_model?: string | null;
dictation_dictionary?: string;
dictation_sounds?: boolean;
memory_enabled?: boolean;
dictation_haptics?: boolean;
dictation_sound_volume?: number;
dictation_disabled_surfaces?: string;
@@ -170,6 +171,7 @@ export const DEFAULT_SETTINGS: AppSettings = {
dictation_model: null,
dictation_dictionary: '',
dictation_sounds: true,
memory_enabled: true,
dictation_haptics: true,
dictation_sound_volume: 0.35,
dictation_disabled_surfaces: '',