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 }