/ src / components / memory / memory-detail.tsx
memory-detail.tsx
  1  'use client'
  2  
  3  import { useEffect, useState, useCallback } from 'react'
  4  import { useAppStore } from '@/stores/use-app-store'
  5  import { useNavigate } from '@/lib/app/navigation'
  6  import { getMemory, updateMemory, deleteMemory } from '@/lib/memory'
  7  import { deriveMemoryScope, getMemoryScopeLabel, getMemoryTier } from '@/lib/memory-presentation'
  8  import { AgentAvatar } from '@/components/agents/agent-avatar'
  9  import { ConfirmDialog } from '@/components/shared/confirm-dialog'
 10  import type { MemoryEntry } from '@/types'
 11  
 12  const CATEGORIES = ['note', 'fact', 'preference', 'finding', 'learning', 'general']
 13  
 14  export function MemoryDetail() {
 15    const selectedId = useAppStore((s) => s.selectedMemoryId)
 16    const setSelectedId = useAppStore((s) => s.setSelectedMemoryId)
 17    const triggerRefresh = useAppStore((s) => s.triggerMemoryRefresh)
 18    const agents = useAppStore((s) => s.agents)
 19    const sessions = useAppStore((s) => s.sessions)
 20    const setCurrentAgent = useAppStore((s) => s.setCurrentAgent)
 21    const navigateTo = useNavigate()
 22  
 23    const [entry, setEntry] = useState<MemoryEntry | null>(null)
 24    const [editing, setEditing] = useState(false)
 25    const [title, setTitle] = useState('')
 26    const [content, setContent] = useState('')
 27    const [category, setCategory] = useState('note')
 28    const [editTier, setEditTier] = useState<'working' | 'durable' | 'archive'>('durable')
 29    const [editAgentId, setEditAgentId] = useState<string | null>(null)
 30    const [editSharedWith, setEditSharedWith] = useState<string[]>([])
 31    const [saving, setSaving] = useState(false)
 32    const [confirmDelete, setConfirmDelete] = useState(false)
 33    const [linkedTitles, setLinkedTitles] = useState<Record<string, string>>({})
 34    const [refsExpanded, setRefsExpanded] = useState(false)
 35    const [metaExpanded, setMetaExpanded] = useState(false)
 36  
 37    // Load memory entry when selection changes
 38    useEffect(() => {
 39      if (!selectedId) {
 40        setEntry(null)
 41        setEditing(false)
 42        return
 43      }
 44  
 45      let cancelled = false
 46      getMemory(selectedId, { depth: 0 })
 47        .then((found) => {
 48          if (cancelled || !found) return
 49  
 50          const resolved = Array.isArray(found)
 51            ? found.find((item) => item.id === selectedId) || found[0] || null
 52            : found
 53  
 54          if (!resolved) return
 55  
 56          setEntry(resolved)
 57          setTitle(resolved.title)
 58          setContent(resolved.content)
 59          setCategory(resolved.category || 'note')
 60          setEditTier(getMemoryTier(resolved))
 61          setEditAgentId(resolved.agentId || null)
 62          setEditSharedWith(resolved.sharedWith || [])
 63          setEditing(false)
 64          setRefsExpanded(false)
 65          setMetaExpanded(false)
 66        })
 67        .catch((err) => console.error('Memory operation failed:', err))
 68  
 69      return () => {
 70        cancelled = true
 71      }
 72    }, [selectedId])
 73  
 74    // Resolve linked memory titles
 75    useEffect(() => {
 76      if (!entry?.linkedMemoryIds?.length) {
 77        setLinkedTitles({})
 78        return
 79      }
 80      let cancelled = false
 81      Promise.all(
 82        entry.linkedMemoryIds.map((id) =>
 83          getMemory(id, { depth: 0 }).then((m) => {
 84            const resolved = Array.isArray(m) ? m[0] : m
 85            return [id, resolved?.title || id] as const
 86          }).catch(() => [id, id] as const),
 87        ),
 88      ).then((pairs) => {
 89        if (cancelled) return
 90        setLinkedTitles(Object.fromEntries(pairs))
 91      })
 92      return () => { cancelled = true }
 93    }, [entry?.linkedMemoryIds])
 94  
 95    const handleSave = useCallback(async () => {
 96      if (!entry) return
 97      setSaving(true)
 98      try {
 99        const updated = await updateMemory(entry.id, {
100          title,
101          content,
102          category,
103          agentId: editAgentId,
104          sharedWith: editSharedWith.length ? editSharedWith : undefined,
105          metadata: {
106            ...(entry.metadata || {}),
107            tier: editTier,
108            scope: editAgentId ? 'agent' : 'global',
109            visibility: editAgentId ? (editSharedWith.length ? 'shared' : 'private') : 'global',
110          },
111        })
112        setEntry(updated)
113        setEditing(false)
114        triggerRefresh()
115      } catch { /* ignore */ }
116      setSaving(false)
117      // eslint-disable-next-line react-hooks/exhaustive-deps
118    }, [entry, title, content, category, editAgentId, editSharedWith])
119  
120    const handleDelete = useCallback(async () => {
121      if (!entry) return
122      await deleteMemory(entry.id)
123      setSelectedId(null)
124      triggerRefresh()
125      // eslint-disable-next-line react-hooks/exhaustive-deps
126    }, [entry])
127  
128    const handleTogglePin = useCallback(async () => {
129      if (!entry) return
130      try {
131        const updated = await updateMemory(entry.id, { pinned: !entry.pinned })
132        setEntry(updated)
133        triggerRefresh()
134      } catch { /* ignore */ }
135      // eslint-disable-next-line react-hooks/exhaustive-deps
136    }, [entry])
137  
138    const handleNavigateToSession = useCallback(() => {
139      if (!entry?.sessionId) return
140      const agentId = sessions[entry.sessionId]?.agentId
141      if (agentId) void setCurrentAgent(agentId)
142      navigateTo('agents')
143      // eslint-disable-next-line react-hooks/exhaustive-deps
144    }, [entry, sessions])
145  
146    if (!entry) {
147      return (
148        <div className="flex-1 flex flex-col items-center justify-center gap-4 text-text-3 p-8 text-center">
149          <div className="w-14 h-14 rounded-[16px] bg-white/[0.03] flex items-center justify-center mb-2">
150            <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" className="text-text-3/60">
151              <ellipse cx="12" cy="5" rx="9" ry="3" />
152              <path d="M21 12c0 1.66-4 3-9 3s-9-1.34-9-3" />
153              <path d="M3 5v14c0 1.66 4 3 9 3s9-1.34 9-3V5" />
154            </svg>
155          </div>
156          <p className="font-display text-[17px] font-600 text-text-2">Select a Memory</p>
157          <p className="text-[13px] text-text-3/70 max-w-[300px]">
158            Choose a memory from the list to view its details
159          </p>
160        </div>
161      )
162    }
163  
164    const agentName = entry.agentId ? (agents[entry.agentId]?.name || entry.agentId) : null
165    const sessionName = entry.sessionId ? (sessions[entry.sessionId]?.name || entry.sessionId) : null
166    const scope = deriveMemoryScope(entry)
167    const tier = getMemoryTier(entry)
168    const imagePath = entry.image?.path || entry.imagePath || null
169    const imageUrl = imagePath
170      ? imagePath.startsWith('data/memory-images/')
171        ? `/api/memory-images/${imagePath.split('/').pop()}`
172        : imagePath
173      : null
174  
175    const inputClass = "w-full px-4 py-3 rounded-[12px] border border-white/[0.06] bg-white/[0.02] text-text outline-none transition-all duration-200 placeholder:text-text-3/70 focus:border-accent-bright/20 focus:bg-white/[0.03]"
176    const refs = entry.references || []
177    const showRefsCollapse = refs.length > 3
178    const entryMeta = entry.metadata && typeof entry.metadata === 'object'
179      ? entry.metadata as Record<string, unknown>
180      : {}
181    const knowledgeSourceId = typeof entryMeta.sourceId === 'string' ? entryMeta.sourceId : null
182    const knowledgeSourceTitle = typeof entryMeta.sourceTitle === 'string' ? entryMeta.sourceTitle : null
183    const knowledgeSourceKind = typeof entryMeta.sourceKind === 'string' ? entryMeta.sourceKind : null
184    const knowledgeSourceLabel = typeof entryMeta.sourceLabel === 'string' ? entryMeta.sourceLabel : null
185    const knowledgeSourceUrl = typeof entryMeta.sourceUrl === 'string' ? entryMeta.sourceUrl : null
186    const knowledgeChunkIndex = typeof entryMeta.chunkIndex === 'number' ? entryMeta.chunkIndex : null
187    const knowledgeChunkCount = typeof entryMeta.chunkCount === 'number' ? entryMeta.chunkCount : null
188    const knowledgeSectionLabel = typeof entryMeta.sectionLabel === 'string' ? entryMeta.sectionLabel : null
189    const knowledgeCharStart = typeof entryMeta.charStart === 'number' ? entryMeta.charStart : null
190    const knowledgeCharEnd = typeof entryMeta.charEnd === 'number' ? entryMeta.charEnd : null
191  
192    return (
193      <div className="flex-1 flex flex-col h-full min-h-0">
194        {/* Header */}
195        <div className="shrink-0 px-6 py-4 border-b border-white/[0.04] flex items-center gap-3">
196          <div className="flex-1 min-w-0">
197            <div className="flex items-center gap-2.5">
198              <span className="shrink-0 text-[10px] font-700 uppercase tracking-wider text-accent-bright/70 bg-accent-soft px-2 py-0.5 rounded-[6px]">
199                {entry.category || 'note'}
200              </span>
201              <span className="shrink-0 text-[10px] font-700 uppercase tracking-wider text-text-3/70 bg-white/[0.04] px-2 py-0.5 rounded-[6px]">
202                {getMemoryScopeLabel(scope)}
203              </span>
204              <span className={`shrink-0 text-[10px] font-700 uppercase tracking-wider px-2 py-0.5 rounded-[6px] ${
205                tier === 'working'
206                  ? 'bg-amber-400/10 text-amber-300'
207                  : tier === 'archive'
208                    ? 'bg-sky-400/10 text-sky-300'
209                    : 'bg-emerald-400/10 text-emerald-300'
210              }`}>
211                {tier}
212              </span>
213              {!editing && (
214                <h2 className="font-display text-[16px] font-700 truncate tracking-[-0.02em]">{entry.title || 'Untitled'}</h2>
215              )}
216            </div>
217            <div className="flex items-center gap-3 mt-1">
218              {agentName && (
219                <span className="text-[11px] text-text-3/50 flex items-center gap-1">
220                  <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" /><circle cx="12" cy="7" r="4" /></svg>
221                  {agentName}
222                </span>
223              )}
224              {sessionName && (
225                <button
226                  onClick={handleNavigateToSession}
227                  className="text-[11px] text-accent-bright/50 hover:text-accent-bright flex items-center gap-1 bg-transparent border-none cursor-pointer p-0 transition-colors"
228                >
229                  <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" /></svg>
230                  {sessionName}
231                </button>
232              )}
233              <span className="text-[10px] text-text-3/50 font-mono tabular-nums">
234                {new Date(entry.createdAt).toLocaleString()}
235              </span>
236            </div>
237          </div>
238  
239          <div className="flex items-center gap-2 shrink-0">
240            {/* Pin/unpin toggle */}
241            <button
242              onClick={handleTogglePin}
243              className={`p-2 rounded-[8px] cursor-pointer transition-all bg-transparent border-none
244                ${entry.pinned ? 'text-amber-400 hover:text-amber-300' : 'text-text-3/40 hover:text-amber-400/70'}`}
245              title={entry.pinned ? 'Unpin memory' : 'Pin memory (always preloaded)'}
246            >
247              <svg width="15" height="15" viewBox="0 0 24 24" fill={entry.pinned ? 'currentColor' : 'none'} stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
248                <path d="M12 17v5" /><path d="M9 2h6l-1.5 6H16l1 4H7l1-4h1.5z" />
249              </svg>
250            </button>
251            {editing ? (
252              <>
253                <button
254                  onClick={() => {
255                    setTitle(entry.title)
256                    setContent(entry.content)
257                    setCategory(entry.category || 'note')
258                    setEditTier(getMemoryTier(entry))
259                    setEditAgentId(entry.agentId || null)
260                    setEditSharedWith(entry.sharedWith || [])
261                    setEditing(false)
262                  }}
263                  className="px-3 py-2 rounded-[10px] border border-white/[0.08] bg-transparent text-text-2 text-[12px] font-600 cursor-pointer hover:bg-surface-2 transition-all"
264                  style={{ fontFamily: 'inherit' }}
265                >
266                  Cancel
267                </button>
268                <button
269                  onClick={handleSave}
270                  disabled={saving}
271                  className="px-4 py-2 rounded-[10px] bg-accent-bright text-white text-[12px] font-600
272                    cursor-pointer border-none transition-all hover:brightness-110 active:scale-[0.97]
273                    disabled:opacity-50 shadow-[0_2px_10px_rgba(99,102,241,0.2)]"
274                  style={{ fontFamily: 'inherit' }}
275                >
276                  {saving ? 'Saving...' : 'Save'}
277                </button>
278              </>
279            ) : (
280              <button
281                onClick={() => setEditing(true)}
282                className="px-3 py-2 rounded-[10px] border border-white/[0.08] bg-transparent text-text-2 text-[12px] font-600 cursor-pointer hover:bg-white/[0.04] transition-all flex items-center gap-1.5"
283                style={{ fontFamily: 'inherit' }}
284              >
285                <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round">
286                  <path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7" />
287                  <path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z" />
288                </svg>
289                Edit
290              </button>
291            )}
292            <button
293              onClick={() => setConfirmDelete(true)}
294              className="p-2 rounded-[8px] text-text-3/70 hover:text-red-400 hover:bg-red-400/[0.06]
295                cursor-pointer transition-all bg-transparent border-none"
296              title="Delete memory"
297            >
298              <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
299                <polyline points="3 6 5 6 21 6" /><path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" />
300                <path d="M10 11v6" /><path d="M14 11v6" /><path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2" />
301              </svg>
302            </button>
303          </div>
304        </div>
305  
306        {/* Content area */}
307        <div className="flex-1 overflow-y-auto px-6 py-5">
308          <div className="max-w-[720px] space-y-5">
309            {editing ? (
310              <>
311                {/* Title input */}
312                <div>
313                  <label className="block text-[11px] font-600 text-text-3/60 uppercase tracking-[0.06em] mb-2">Title</label>
314                  <input
315                    type="text"
316                    value={title}
317                    onChange={(e) => setTitle(e.target.value)}
318                    className={`${inputClass} text-[15px] font-600`}
319                    style={{ fontFamily: 'inherit' }}
320                    placeholder="Memory title"
321                  />
322                </div>
323  
324                {/* Category picker */}
325                <div>
326                  <label className="block text-[11px] font-600 text-text-3/60 uppercase tracking-[0.06em] mb-2">Category</label>
327                  <div className="flex gap-1.5 flex-wrap">
328                    {CATEGORIES.map((c) => (
329                      <button
330                        key={c}
331                        onClick={() => setCategory(c)}
332                        className={`px-3 py-1.5 rounded-[8px] text-[11px] font-600 capitalize cursor-pointer transition-all border-none
333                          ${category === c
334                            ? 'bg-accent-soft text-accent-bright'
335                            : 'bg-white/[0.03] text-text-3 hover:text-text-2 hover:bg-white/[0.05]'}`}
336                        style={{ fontFamily: 'inherit' }}
337                      >
338                        {c}
339                      </button>
340                    ))}
341                  </div>
342                </div>
343  
344                <div>
345                  <label className="block text-[11px] font-600 text-text-3/60 uppercase tracking-[0.06em] mb-2">Tier</label>
346                  <select
347                    value={editTier}
348                    onChange={(e) => setEditTier(e.target.value as typeof editTier)}
349                    className={inputClass}
350                    style={{ fontFamily: 'inherit' }}
351                  >
352                    <option value="working">Working</option>
353                    <option value="durable">Durable</option>
354                    <option value="archive">Archive</option>
355                  </select>
356                </div>
357  
358                {/* Agent assignment */}
359                <div>
360                  <label className="block text-[11px] font-600 text-text-3/60 uppercase tracking-[0.06em] mb-2">Visibility</label>
361                  <div className="flex gap-1.5 flex-wrap">
362                    <button
363                      onClick={() => setEditAgentId(null)}
364                      className={`flex items-center gap-1.5 px-3 py-1.5 rounded-[8px] text-[11px] font-600 cursor-pointer transition-all border
365                        ${!editAgentId
366                          ? 'bg-accent-soft border-accent-bright/20 text-accent-bright'
367                          : 'bg-white/[0.02] border-white/[0.06] text-text-3 hover:text-text-2 hover:bg-white/[0.04]'}`}
368                      style={{ fontFamily: 'inherit' }}
369                    >
370                      <svg width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className={!editAgentId ? 'text-accent-bright' : 'text-text-3/60'}>
371                        <circle cx="12" cy="12" r="10" /><line x1="2" y1="12" x2="22" y2="12" />
372                        <path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z" />
373                      </svg>
374                      Global
375                    </button>
376                    {Object.values(agents).sort((a, b) => a.name.localeCompare(b.name)).map((agent) => (
377                      <button
378                        key={agent.id}
379                        onClick={() => setEditAgentId(agent.id)}
380                        className={`flex items-center gap-1.5 px-3 py-1.5 rounded-[8px] text-[11px] font-600 cursor-pointer transition-all border
381                          ${editAgentId === agent.id
382                            ? 'bg-accent-soft border-accent-bright/20 text-accent-bright'
383                            : 'bg-white/[0.02] border-white/[0.06] text-text-3 hover:text-text-2 hover:bg-white/[0.04]'}`}
384                        style={{ fontFamily: 'inherit' }}
385                      >
386                        <AgentAvatar seed={agent.avatarSeed || null} avatarUrl={agent.avatarUrl} name={agent.name} size={16} />
387                        <span className="truncate max-w-[100px]">{agent.name}</span>
388                      </button>
389                    ))}
390                  </div>
391                </div>
392  
393                {/* Shared with */}
394                {editAgentId && (
395                  <div>
396                    <label className="block text-[11px] font-600 text-text-3/60 uppercase tracking-[0.06em] mb-2">Share with</label>
397                    <div className="flex gap-1.5 flex-wrap">
398                      {Object.values(agents)
399                        .filter((a) => a.id !== editAgentId)
400                        .sort((a, b) => a.name.localeCompare(b.name))
401                        .map((agent) => {
402                          const isShared = editSharedWith.includes(agent.id)
403                          return (
404                            <button
405                              key={agent.id}
406                              onClick={() => {
407                                setEditSharedWith(isShared
408                                  ? editSharedWith.filter((id) => id !== agent.id)
409                                  : [...editSharedWith, agent.id])
410                              }}
411                              className={`flex items-center gap-1.5 px-3 py-1.5 rounded-[8px] text-[11px] font-600 cursor-pointer transition-all border
412                                ${isShared
413                                  ? 'bg-accent-soft border-accent-bright/20 text-accent-bright'
414                                  : 'bg-white/[0.02] border-white/[0.06] text-text-3 hover:text-text-2 hover:bg-white/[0.04]'}`}
415                              style={{ fontFamily: 'inherit' }}
416                            >
417                              <AgentAvatar seed={agent.avatarSeed || null} avatarUrl={agent.avatarUrl} name={agent.name} size={16} />
418                              <span className="truncate max-w-[100px]">{agent.name}</span>
419                            </button>
420                          )
421                        })}
422                    </div>
423                    {editSharedWith.length === 0 && (
424                      <p className="text-[10px] text-text-3/40 mt-1.5">No agents selected — only the assigned agent can access this memory</p>
425                    )}
426                  </div>
427                )}
428  
429                {/* Content textarea */}
430                <div>
431                  <label className="block text-[11px] font-600 text-text-3/60 uppercase tracking-[0.06em] mb-2">Content</label>
432                  <textarea
433                    value={content}
434                    onChange={(e) => setContent(e.target.value)}
435                    placeholder="Memory content..."
436                    rows={12}
437                    className={`${inputClass} text-[14px] resize-y min-h-[200px] leading-relaxed`}
438                    style={{ fontFamily: 'inherit' }}
439                  />
440                </div>
441              </>
442            ) : (
443              <>
444                {/* Read-mode: Title as h1 */}
445                <h1 className="font-display text-[22px] font-700 tracking-[-0.02em] text-text leading-tight">
446                  {entry.title || 'Untitled'}
447                </h1>
448  
449                {/* Read-mode: Content as readable prose */}
450                <div className="text-[15px] leading-[1.7] text-text-2 whitespace-pre-wrap break-words">
451                  {entry.content || '(empty)'}
452                </div>
453  
454                {knowledgeSourceId && (
455                  <div className="rounded-[14px] border border-white/[0.06] bg-white/[0.02] px-4 py-3">
456                    <label className="block text-[11px] font-600 text-text-3/60 uppercase tracking-[0.06em] mb-2">Source</label>
457                    <div className="space-y-1.5">
458                      <p className="text-[13px] text-text-2">
459                        {knowledgeSourceTitle || entry.title}
460                        {knowledgeSourceKind ? ` • ${knowledgeSourceKind}` : ''}
461                      </p>
462                      {knowledgeSourceLabel && (
463                        <p className="text-[12px] text-text-3/65">{knowledgeSourceLabel}</p>
464                      )}
465                      {knowledgeSourceUrl && (
466                        <a href={knowledgeSourceUrl} target="_blank" rel="noreferrer" className="text-[12px] text-accent-bright hover:underline break-all">
467                          {knowledgeSourceUrl}
468                        </a>
469                      )}
470                      <p className="text-[11px] text-text-3/55">
471                        {knowledgeChunkIndex != null && knowledgeChunkCount != null
472                          ? `Chunk ${knowledgeChunkIndex + 1} of ${knowledgeChunkCount}`
473                          : 'Source-backed knowledge'}
474                        {knowledgeSectionLabel ? ` • ${knowledgeSectionLabel}` : ''}
475                        {knowledgeCharStart != null && knowledgeCharEnd != null ? ` • chars ${knowledgeCharStart}-${knowledgeCharEnd}` : ''}
476                      </p>
477                    </div>
478                  </div>
479                )}
480  
481                {/* Shared with (read mode) */}
482                {entry.sharedWith && entry.sharedWith.length > 0 && (
483                  <div>
484                    <label className="block text-[11px] font-600 text-text-3/60 uppercase tracking-[0.06em] mb-2">Shared with</label>
485                    <div className="flex gap-1.5 flex-wrap">
486                      {entry.sharedWith.map((aid) => {
487                        const a = agents[aid]
488                        return (
489                          <span key={aid} className="flex items-center gap-1.5 px-2.5 py-1 rounded-[8px] bg-white/[0.03] text-[11px] text-text-3">
490                            <AgentAvatar seed={a?.avatarSeed || null} avatarUrl={a?.avatarUrl} name={a?.name || aid} size={16} />
491                            {a?.name || aid}
492                          </span>
493                        )
494                      })}
495                    </div>
496                  </div>
497                )}
498              </>
499            )}
500  
501            {/* Image (both modes) */}
502            {imageUrl && (
503              <div>
504                {editing && <label className="block text-[11px] font-600 text-text-3/60 uppercase tracking-[0.06em] mb-2">Image</label>}
505                <a href={imageUrl} target="_blank" rel="noreferrer" className="inline-block rounded-[12px] overflow-hidden border border-white/[0.08]">
506                  <img src={imageUrl} alt={entry.title} className="max-w-[600px] w-full max-h-[400px] object-cover block" />
507                </a>
508              </div>
509            )}
510  
511            {/* Linked Memories */}
512            {entry.linkedMemoryIds?.length ? (
513              <div>
514                <label className="block text-[11px] font-600 text-text-3/60 uppercase tracking-[0.06em] mb-2">Linked Memories</label>
515                <div className="flex flex-col gap-1.5">
516                  {entry.linkedMemoryIds.map((id) => (
517                    <button
518                      key={id}
519                      onClick={() => setSelectedId(id)}
520                      className="flex items-center gap-2.5 px-3 py-2 rounded-[10px] bg-white/[0.02] border border-white/[0.06] hover:bg-white/[0.04] cursor-pointer transition-colors text-left w-full"
521                    >
522                      <svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" className="text-accent-bright/60 shrink-0">
523                        <path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
524                        <path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
525                      </svg>
526                      <span className="text-[13px] text-text-2 truncate">
527                        {linkedTitles[id] || id}
528                      </span>
529                    </button>
530                  ))}
531                </div>
532              </div>
533            ) : null}
534  
535            {/* References (collapsible) */}
536            {refs.length > 0 && (
537              <div>
538                <button
539                  onClick={() => setRefsExpanded(!refsExpanded)}
540                  className="flex items-center gap-1.5 text-[11px] font-600 text-text-3/60 uppercase tracking-[0.06em] mb-2 bg-transparent border-none cursor-pointer p-0 hover:text-text-3 transition-colors"
541                  style={{ fontFamily: 'inherit' }}
542                >
543                  <svg
544                    width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"
545                    className={`transition-transform ${refsExpanded || !showRefsCollapse ? 'rotate-90' : ''}`}
546                  >
547                    <polyline points="9 18 15 12 9 6" />
548                  </svg>
549                  References ({refs.length})
550                </button>
551                {(refsExpanded || !showRefsCollapse) && (
552                  <div className="space-y-2">
553                    {refs.map((ref, idx) => (
554                      <div key={`${ref.type}-${ref.path || ref.title || idx}`} className="text-[12px] rounded-[10px] border border-white/[0.06] bg-white/[0.02] px-3 py-2">
555                        <div className="text-text-2/70">
556                          <span className="uppercase text-[10px] tracking-[0.06em] mr-1">{ref.type}</span>
557                          {ref.path || ref.title || '(no path)'}
558                        </div>
559                        {(ref.projectName || ref.projectRoot || ref.note || typeof ref.exists === 'boolean') && (
560                          <div className="text-text-3/55 mt-1">
561                            {ref.projectName ? `project: ${ref.projectName} ` : ''}
562                            {ref.projectRoot ? `root: ${ref.projectRoot} ` : ''}
563                            {typeof ref.exists === 'boolean' ? (ref.exists ? 'exists' : 'missing') : ''}
564                            {ref.note ? ` — ${ref.note}` : ''}
565                          </div>
566                        )}
567                      </div>
568                    ))}
569                  </div>
570                )}
571              </div>
572            )}
573  
574            {/* Metadata (disclosure) */}
575            <div className="pt-2">
576              <button
577                onClick={() => setMetaExpanded(!metaExpanded)}
578                className="flex items-center gap-1.5 text-[11px] font-600 text-text-3/60 uppercase tracking-[0.06em] bg-transparent border-none cursor-pointer p-0 hover:text-text-3 transition-colors"
579                style={{ fontFamily: 'inherit' }}
580              >
581                <svg
582                  width="10" height="10" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.5" strokeLinecap="round"
583                  className={`transition-transform ${metaExpanded ? 'rotate-90' : ''}`}
584                >
585                  <polyline points="9 18 15 12 9 6" />
586                </svg>
587                Details
588              </button>
589              {metaExpanded && (
590                <div className="mt-3 pt-3 border-t border-white/[0.04]">
591                  <div className="grid grid-cols-2 gap-4 text-[11px]">
592                    <div>
593                      <span className="text-text-3/70 block mb-1">ID</span>
594                      <span className="text-text-3/60 font-mono">{entry.id}</span>
595                    </div>
596                    <div>
597                      <span className="text-text-3/70 block mb-1">Created</span>
598                      <span className="text-text-3/60 font-mono">{new Date(entry.createdAt).toLocaleString()}</span>
599                    </div>
600                    <div>
601                      <span className="text-text-3/70 block mb-1">Updated</span>
602                      <span className="text-text-3/60 font-mono">{new Date(entry.updatedAt).toLocaleString()}</span>
603                    </div>
604                    {entry.agentId && (
605                      <div>
606                        <span className="text-text-3/70 block mb-1">Owner</span>
607                        <span className="text-text-3/60 font-mono">{agentName}</span>
608                      </div>
609                    )}
610                    <div>
611                      <span className="text-text-3/70 block mb-1">Scope</span>
612                      <span className="text-text-3/60 font-mono">{getMemoryScopeLabel(scope)}</span>
613                    </div>
614                    <div>
615                      <span className="text-text-3/70 block mb-1">Tier</span>
616                      <span className="text-text-3/60 font-mono">{tier}</span>
617                    </div>
618                    {knowledgeSourceId && (
619                      <div>
620                        <span className="text-text-3/70 block mb-1">Knowledge Source</span>
621                        <span className="text-text-3/60 font-mono">{knowledgeSourceId}</span>
622                      </div>
623                    )}
624                    {entry.sessionId && (
625                      <div>
626                        <span className="text-text-3/70 block mb-1">Chat</span>
627                        <button
628                          onClick={handleNavigateToSession}
629                          className="text-accent-bright/60 hover:text-accent-bright font-mono bg-transparent border-none cursor-pointer p-0 text-[11px] transition-colors"
630                        >
631                          {sessionName}
632                        </button>
633                      </div>
634                    )}
635                  </div>
636                </div>
637              )}
638            </div>
639          </div>
640        </div>
641  
642        <ConfirmDialog
643          open={confirmDelete}
644          title="Delete Memory"
645          message={`Delete "${entry.title}"? This cannot be undone.`}
646          confirmLabel="Delete"
647          danger
648          onCancel={() => setConfirmDelete(false)}
649          onConfirm={() => { void handleDelete() }}
650        />
651      </div>
652    )
653  }