From 34b4a5bb0e50f7bcbf82117abb886783d6c2d8ca Mon Sep 17 00:00:00 2001 From: Ettore Di Giacinto Date: Tue, 3 Nov 2026 10:15:03 +0000 Subject: [PATCH] feat(ui): add a Memory tab to the account settings The tab lists the context files that the host puts in the prompt (the user's index and topics, each agent's and conversation's notes, and the agent profiles) with their sizes against the caps. The owner can read, edit, delete and clear them; an index over its cap cannot be saved. Assisted-by: Claude Code:claude-sonnet-5-5 Signed-off-by: Ettore Di Giacinto --- .../http/react-ui/public/locales/de/auth.json | 3 +- .../http/react-ui/public/locales/en/auth.json | 3 +- .../http/react-ui/public/locales/es/auth.json | 3 +- .../http/react-ui/public/locales/fr/auth.json | 3 +- .../http/react-ui/public/locales/id/auth.json | 3 +- .../http/react-ui/public/locales/it/auth.json | 3 +- .../http/react-ui/public/locales/ko/auth.json | 3 +- .../react-ui/public/locales/pt-BR/auth.json | 3 +- .../react-ui/public/locales/zh-CN/auth.json | 3 +- .../react-ui/src/components/MemoryTab.jsx | 212 ++++++++++++++++++ .../react-ui/src/components/memorytab.css | 27 +++ core/http/react-ui/src/pages/Account.jsx | 3 + 12 files changed, 260 insertions(+), 9 deletions(-) create mode 100644 core/http/react-ui/src/components/MemoryTab.jsx create mode 100644 core/http/react-ui/src/components/memorytab.css diff --git a/core/http/react-ui/public/locales/de/auth.json b/core/http/react-ui/public/locales/de/auth.json index f0232dda09..d1b1121932 100644 --- a/core/http/react-ui/public/locales/de/auth.json +++ b/core/http/react-ui/public/locales/de/auth.json @@ -55,7 +55,8 @@ "security": "Sicherheit", "apiKeys": "API-Schlüssel", "about": "Über dich und den Assistenten", - "appearance": "Darstellung" + "appearance": "Darstellung", + "memory": "Gedächtnis" }, "profile": { "displayName": "Anzeigename", diff --git a/core/http/react-ui/public/locales/en/auth.json b/core/http/react-ui/public/locales/en/auth.json index 68594b46f2..705cbe7235 100644 --- a/core/http/react-ui/public/locales/en/auth.json +++ b/core/http/react-ui/public/locales/en/auth.json @@ -80,7 +80,8 @@ "apiKeys": "API keys", "usage": "Usage", "about": "About you and the assistant", - "appearance": "Appearance" + "appearance": "Appearance", + "memory": "Memory" }, "profile": { "displayName": "Display name", diff --git a/core/http/react-ui/public/locales/es/auth.json b/core/http/react-ui/public/locales/es/auth.json index 0c799e9388..6449180f06 100644 --- a/core/http/react-ui/public/locales/es/auth.json +++ b/core/http/react-ui/public/locales/es/auth.json @@ -55,7 +55,8 @@ "security": "Seguridad", "apiKeys": "Claves API", "about": "Sobre ti y el asistente", - "appearance": "Apariencia" + "appearance": "Apariencia", + "memory": "Memoria" }, "profile": { "displayName": "Nombre para mostrar", diff --git a/core/http/react-ui/public/locales/fr/auth.json b/core/http/react-ui/public/locales/fr/auth.json index 596540ebf1..0ffffdf8df 100644 --- a/core/http/react-ui/public/locales/fr/auth.json +++ b/core/http/react-ui/public/locales/fr/auth.json @@ -55,7 +55,8 @@ "security": "Sécurité", "apiKeys": "Clés d'API", "about": "À propos de vous et de l'assistant", - "appearance": "Apparence" + "appearance": "Apparence", + "memory": "Mémoire" }, "profile": { "displayName": "Nom affiché", diff --git a/core/http/react-ui/public/locales/id/auth.json b/core/http/react-ui/public/locales/id/auth.json index 9d1c5752e1..e936a88c91 100644 --- a/core/http/react-ui/public/locales/id/auth.json +++ b/core/http/react-ui/public/locales/id/auth.json @@ -55,7 +55,8 @@ "security": "Keamanan", "apiKeys": "API Key", "about": "Tentang Anda dan asisten", - "appearance": "Tampilan" + "appearance": "Tampilan", + "memory": "Memori" }, "profile": { "displayName": "Nama tampilan", diff --git a/core/http/react-ui/public/locales/it/auth.json b/core/http/react-ui/public/locales/it/auth.json index fa24696154..2ad1a4f074 100644 --- a/core/http/react-ui/public/locales/it/auth.json +++ b/core/http/react-ui/public/locales/it/auth.json @@ -55,7 +55,8 @@ "security": "Sicurezza", "apiKeys": "Chiavi API", "about": "Su di te e sull'assistente", - "appearance": "Aspetto" + "appearance": "Aspetto", + "memory": "Memoria" }, "profile": { "displayName": "Nome visualizzato", diff --git a/core/http/react-ui/public/locales/ko/auth.json b/core/http/react-ui/public/locales/ko/auth.json index 37bac414f4..8d7d7f97fd 100644 --- a/core/http/react-ui/public/locales/ko/auth.json +++ b/core/http/react-ui/public/locales/ko/auth.json @@ -55,7 +55,8 @@ "security": "보안", "apiKeys": "API 키", "about": "나와 어시스턴트에 대하여", - "appearance": "모양" + "appearance": "모양", + "memory": "메모리" }, "profile": { "displayName": "표시 이름", diff --git a/core/http/react-ui/public/locales/pt-BR/auth.json b/core/http/react-ui/public/locales/pt-BR/auth.json index 6988bf7536..be78eb1af3 100644 --- a/core/http/react-ui/public/locales/pt-BR/auth.json +++ b/core/http/react-ui/public/locales/pt-BR/auth.json @@ -55,7 +55,8 @@ "security": "Segurança", "apiKeys": "Chaves de API", "about": "Sobre você e o assistente", - "appearance": "Aparência" + "appearance": "Aparência", + "memory": "Memória" }, "profile": { "displayName": "Nome de exibição", diff --git a/core/http/react-ui/public/locales/zh-CN/auth.json b/core/http/react-ui/public/locales/zh-CN/auth.json index 21a60bf8dc..6c6a4e10a7 100644 --- a/core/http/react-ui/public/locales/zh-CN/auth.json +++ b/core/http/react-ui/public/locales/zh-CN/auth.json @@ -55,7 +55,8 @@ "security": "安全", "apiKeys": "API 密钥", "about": "关于您和助手", - "appearance": "外观" + "appearance": "外观", + "memory": "记忆" }, "profile": { "displayName": "显示名称", diff --git a/core/http/react-ui/src/components/MemoryTab.jsx b/core/http/react-ui/src/components/MemoryTab.jsx new file mode 100644 index 0000000000..c4f1b9e54e --- /dev/null +++ b/core/http/react-ui/src/components/MemoryTab.jsx @@ -0,0 +1,212 @@ +/* eslint-disable no-unused-vars -- components used only inside JSX look unused to this config, which has no eslint-plugin-react */ +import { useCallback, useEffect, useId, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { Link } from 'react-router-dom' +import { chatChangesApi, filesApi } from '../utils/api' +import { byteText } from '../utils/changeProposal' +import { allFiles, groupId, groupKind, indexFits, indexUse } from '../utils/memoryFiles' +import Icon from './Icon' +import { Button, Callout, EmptyState, LoadError, SkeletonRows } from './ui' +import './memorytab.css' + +const READ_LIMIT = 64 * 1024 +const path = (p) => (p.startsWith('/') ? p : `/${p}`) + +// The size of an index against its caps, as a bar and words. The bar is a +// progress element, so it is read as "x of y" without a style. +function Use({ group, t }) { + const u = indexUse(group) + if (!u.index || !u.maxBytes) return null + const words = t(u.maxLines ? 'memoryPage.use' : 'memoryPage.useBytes', { size: byteText(u.bytes), max: byteText(u.maxBytes), lines: u.lines, maxLines: u.maxLines }) + return ( +

+ + {words} + {u.over && {' '}{t('memoryPage.over')}} +

+ ) +} + +// One file of a group: its name and size, and a panel that shows the text and +// switches to an editor. The owner writes straight into their own files here; +// the host keeps every write as a change set, and a delete goes to the Trash. +function FileRow({ file, group, t, busy, onDelete, onSaved, setError }) { + const uid = useId() + const [open, setOpen] = useState(false) + const [text, setText] = useState(null) + const [edit, setEdit] = useState(null) + const [saving, setSaving] = useState(false) + const [ask, setAsk] = useState(false) + + const toggle = async () => { + if (open) { setOpen(false); setEdit(null); return } + setOpen(true) + if (text === null) { + try { setText(await filesApi.readText(path(file.path), READ_LIMIT)) } catch (e) { setText(''); setError(e.message) } + } + } + const fit = file.index && edit !== null ? indexFits(group, edit) : null + const save = async () => { + setSaving(true) + setError('') + try { + await filesApi.uploadWithProgress(path(file.path), new Blob([edit], { type: 'text/markdown' }), { replace: true }) + setText(edit) + setEdit(null) + onSaved(file) + } catch (e) { + setError(t('memoryPage.saveFailed', { message: e.message })) + } finally { + setSaving(false) + } + } + + return ( +
  • +
    + + {byteText(file.size)} + {ask ? ( + + + + + ) : ( + +
    + {open && ( +
    + {text === null ? : edit === null ? ( + <> +
    {text || t('memoryPage.emptyFile')}
    + + + ) : ( +
    { e.preventDefault(); if (!fit || fit.fits) save() }} onKeyDown={(e) => { if (e.key === 'Escape') { e.stopPropagation(); setEdit(null) } }}> + +