diff --git a/canvas/src/components/tabs/MemoryTab.tsx b/canvas/src/components/tabs/MemoryTab.tsx index 2c5055f7..4243786f 100644 --- a/canvas/src/components/tabs/MemoryTab.tsx +++ b/canvas/src/components/tabs/MemoryTab.tsx @@ -10,6 +10,7 @@ interface Props { interface MemoryEntry { key: string; value: unknown; + version?: number; expires_at: string | null; updated_at: string; } @@ -28,6 +29,10 @@ export function MemoryTab({ workspaceId }: Props) { const [newValue, setNewValue] = useState(""); const [newTTL, setNewTTL] = useState(""); const [error, setError] = useState(null); + const [editingKey, setEditingKey] = useState(null); + const [editValue, setEditValue] = useState(""); + const [editTTL, setEditTTL] = useState(""); + const [editError, setEditError] = useState(null); const awarenessUrl = useMemo(() => { try { @@ -109,6 +114,69 @@ export function MemoryTab({ workspaceId }: Props) { } }; + const beginEdit = (entry: MemoryEntry) => { + setEditError(null); + setEditingKey(entry.key); + // Stringify objects/arrays as pretty JSON; render plain strings raw so the + // editor doesn't surprise users with surrounding quotes. + setEditValue( + typeof entry.value === "string" + ? entry.value + : JSON.stringify(entry.value, null, 2), + ); + if (entry.expires_at) { + const remainingMs = new Date(entry.expires_at).getTime() - Date.now(); + const ttl = Math.max(0, Math.floor(remainingMs / 1000)); + setEditTTL(ttl > 0 ? String(ttl) : ""); + } else { + setEditTTL(""); + } + }; + + const cancelEdit = () => { + setEditingKey(null); + setEditValue(""); + setEditTTL(""); + setEditError(null); + }; + + const handleEditSave = async (entry: MemoryEntry) => { + setEditError(null); + + let parsedValue: unknown; + try { + parsedValue = JSON.parse(editValue); + } catch { + parsedValue = editValue; + } + + // if_match_version closes the silent-overwrite hole when two writers + // race. The handler returns 409 with the current version on mismatch + // — surface that as a retry hint and reload to pick up the new state. + const body: Record = { key: entry.key, value: parsedValue }; + if (typeof entry.version === "number") { + body.if_match_version = entry.version; + } + if (editTTL) { + const ttl = parseInt(editTTL); + if (!Number.isNaN(ttl) && ttl > 0) body.ttl_seconds = ttl; + } + + try { + await api.post(`/workspaces/${workspaceId}/memory`, body); + cancelEdit(); + loadMemory(); + } catch (e) { + const message = e instanceof Error ? e.message : "Failed to save"; + if (message.includes("409") || /if_match_version mismatch/i.test(message)) { + setEditError("This entry changed since you opened it. Reloading."); + loadMemory(); + } else { + setEditError(message); + } + } + }; + const openAwareness = () => { window.open(awarenessUrl, "_blank", "noopener,noreferrer"); }; @@ -308,24 +376,71 @@ export function MemoryTab({ workspaceId }: Props) { {expanded === entry.key && (
-
-                        {JSON.stringify(entry.value, null, 2)}
-                      
+ {editingKey === entry.key ? ( +
+