From 5b6eab7bb8d58349bd9bf0c5fab4bb40d755bb07 Mon Sep 17 00:00:00 2001 From: Sulthan Nauval Abdillah Date: Thu, 6 Aug 2026 04:20:15 +0000 Subject: [PATCH] fix(memory): make each row scannable and each button say what it deletes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three problems visible the moment the panel is opened with real entries. Every row led with a 43-character generated key — `memory_de3f612e-dd95-4b1a-8ca4-5268007fa4d9` — set at the same weight as the timestamp, directly under the content. For someone scanning what the agent remembers it is the least useful thing on the row and takes the most room; the key only matters when addressing the entry from the API or the CLI. That got worse once the gateway started generating keys for entries stored without one. Every delete button was labelled "Forget this memory", so three rows produced three identical accessible names. Nothing distinguished them for a screen reader or for anyone navigating by keyboard. And the count read "Memory entries· 3" — a missing space. - generated keys collapse to a "copy key" control; a key the agent chose, like `user_lang`, is a name and still reads as one - the timestamp leads, since that is what people actually scan by - delete buttons name the memory they remove - clipboard failure outside a secure context shows the key instead of failing silently, so it can still be selected by hand Verified against the running console with a browser, not by reading the JSX. --- src/components/ops/memory-panel.tsx | 47 +++++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/src/components/ops/memory-panel.tsx b/src/components/ops/memory-panel.tsx index 6b74b95..70a259c 100644 --- a/src/components/ops/memory-panel.tsx +++ b/src/components/ops/memory-panel.tsx @@ -15,6 +15,18 @@ import { toast } from "sonner"; import { MEMORY_CATEGORIES } from "@/lib/types"; import { IconButton, PanelFrame, RefreshButton, SectionTitle } from "./shared"; + +/** Keys the server generates when the caller supplied none. */ +function isGeneratedKey(key: string): boolean { + return /^memory_[0-9a-f-]{36}$/i.test(key); +} + +/** Enough of a memory to tell one row from another in a label. */ +function previewOf(content: string): string { + const flat = content.replace(/\s+/g, " ").trim(); + return flat.length > 48 ? `${flat.slice(0, 48)}…` : flat; +} + export function MemoryPanel() { const { data, loading, error, refresh } = useAsync(() => api.memory(100), []); const [content, setContent] = React.useState(""); @@ -46,6 +58,17 @@ export function MemoryPanel() { } }; + const copyKey = async (key: string) => { + try { + await navigator.clipboard.writeText(key); + toast.success("Key copied"); + } catch { + // Clipboard is blocked outside a secure context; show the key so it can + // still be selected by hand rather than failing silently. + toast.message(key); + } + }; + const del = async () => { const key = pendingForget?.key; if (!key) return; @@ -65,10 +88,11 @@ export function MemoryPanel() { return (
}> - Memory entries{" "} + Memory entries {data && ( - · {data.count} + {" · "} + {data.count} {data.total > data.count ? ` of ${data.total}` : ""} )} @@ -119,8 +143,8 @@ export function MemoryPanel() { setPendingForget({ key: e.key, content: e.content })} disabled={w} - title="Forget" - aria-label="Forget this memory" + title={`Forget "${previewOf(e.content)}"`} + aria-label={`Forget "${previewOf(e.content)}"`} className="hover:bg-destructive/10 hover:text-destructive disabled:opacity-50" > {w ? : } @@ -128,9 +152,20 @@ export function MemoryPanel() {
- {e.key} - · {relativeTime(e.timestamp)} + · + {/* A generated key is an address, not a name: it is 43 + characters of UUID that only matters when reaching this + entry from the API or CLI. Keep it available — clicking + copies it — without letting it outweigh the content. */} +
);