import { matchesRankQuery } from '@/lib/search/fuzzySearch'; import React from 'react'; import { toast } from '@/components/ui'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Textarea } from '@/components/ui/textarea'; import { KnowledgeCard } from './KnowledgeCard'; import { Icon } from '@/components/icon/Icon'; import { useI18n } from '@/lib/i18n'; import { AGENT_MEMORY_BODY_MAX_LENGTH, AGENT_MEMORY_TITLE_MAX_LENGTH, type AgentMemoryEntry, type AgentMemoryScope } from '@/lib/agentMemoryApi'; import { classifyMemory, memoryViewKey, type MemoryBadge } from '@/lib/agentMemoryBadges'; import { cn } from '@/lib/utils'; import { selectProjectMemoryForPath, useAgentMemoryStore } from '@/stores/useAgentMemoryStore'; import { useUIStore } from '@/stores/useUIStore'; /** * One stored memory. * * Read-only text on purpose: this is what the agent wrote, and the useful * action on someone else's claim is to remove it, not to quietly rewrite it * into something the agent will contradict next session. * * There is no confirm button. A badge that the user has to dismiss by hand asks * them to do work that tells the agent nothing — the agent already has the * memory either way — so the badge clears itself once they have looked. */ const MemoryRow: React.FC<{ entry: AgentMemoryEntry; badge: MemoryBadge; expanded: boolean; onToggleExpanded: () => void; onSave: (patch: { title?: string; body?: string }) => void; onDelete: () => void; }> = ({ entry, badge, expanded, onToggleExpanded, onSave, onDelete }) => { const { t } = useI18n(); const [titleDraft, setTitleDraft] = React.useState(entry.title); const [bodyDraft, setBodyDraft] = React.useState(entry.body); // Adopt an external rewrite only while this row is not being edited, so the // agent saving mid-edit cannot swallow what the user is typing. React.useEffect(() => { if (expanded) return; setTitleDraft(entry.title); setBodyDraft(entry.body); }, [entry.body, entry.title, expanded]); const commit = React.useCallback(() => { const title = titleDraft.trim(); const body = bodyDraft.trim(); // An emptied field is a rejected write, not a delete: restore it rather // than sending something the server will refuse. if (!title || !body) { setTitleDraft(entry.title); setBodyDraft(entry.body); return; } if (title === entry.title && body === entry.body) { return; } onSave({ title, body }); }, [bodyDraft, entry.body, entry.title, onSave, titleDraft]); const typeLabel = t(`rightSidebar.contextNotesTodo.memory.type.${entry.type}` as Parameters[0]); return ( { if (expanded) commit(); onToggleExpanded(); }} expandLabel={entry.title} footer={( {typeLabel} {entry.flagged ? ( // Shown rather than hidden: an entry withheld from the agent is // exactly the one the user needs to look at. {t('rightSidebar.contextNotesTodo.memory.flagged')} ) : null} )} header={badge ? ( {t(badge === 'new' ? 'rightSidebar.contextNotesTodo.memory.badge.new' : 'rightSidebar.contextNotesTodo.memory.badge.changed')} ) : null} actions={( )} > {expanded ? ( // Editable on purpose. A memory worded badly enough to mislead should // be fixable where it is read; deleting it and hoping the agent learns // it again, better, is not a repair.
setTitleDraft(event.target.value.slice(0, AGENT_MEMORY_TITLE_MAX_LENGTH))} onBlur={commit} aria-label={t('rightSidebar.contextNotesTodo.memory.actions.editTitle')} className="h-7 typography-ui-label" />