@@ -12,6 +12,7 @@ interface MemoryFile {
1212 inProject : boolean
1313 inAgentCache : boolean
1414 relatedMarkdown : boolean
15+ guidance ?: string
1516}
1617
1718interface MemoryGroup {
@@ -23,6 +24,7 @@ interface MemoryGroup {
2324interface CatalogResponse {
2425 groups : MemoryGroup [ ]
2526 files : MemoryFile [ ]
27+ modifications ?: MemoryModification [ ]
2628}
2729
2830interface FileContentResponse {
@@ -31,6 +33,34 @@ interface FileContentResponse {
3133 content : string
3234}
3335
36+ interface MemoryModification {
37+ id : string
38+ fileId : string
39+ filePath : string
40+ location : string
41+ oldContent : string
42+ newContent : string
43+ generatedContent : string
44+ }
45+
46+ function computeSimpleDiff ( oldText : string , newText : string ) : Array < { type : 'context' | 'add' | 'remove' ; text : string } > {
47+ const oldLines = oldText . split ( '\n' )
48+ const newLines = newText . split ( '\n' )
49+ const out : Array < { type : 'context' | 'add' | 'remove' ; text : string } > = [ ]
50+ let i = 0
51+ let j = 0
52+ while ( i < oldLines . length || j < newLines . length ) {
53+ if ( i < oldLines . length && j < newLines . length && oldLines [ i ] === newLines [ j ] ) {
54+ out . push ( { type : 'context' , text : oldLines [ i ] } )
55+ i += 1 ; j += 1
56+ continue
57+ }
58+ if ( i < oldLines . length ) { out . push ( { type : 'remove' , text : oldLines [ i ] ?? '' } ) ; i += 1 }
59+ if ( j < newLines . length ) { out . push ( { type : 'add' , text : newLines [ j ] ?? '' } ) ; j += 1 }
60+ }
61+ return out
62+ }
63+
3464type PageStatusState = 'opened' | 'active' | 'hidden'
3565
3666function renderMarkdownFriendly ( markdown : string ) {
@@ -114,6 +144,8 @@ export default function MemoryManagementPage() {
114144 const [ error , setError ] = useState ( '' )
115145 const [ agentDeleteRequest , setAgentDeleteRequest ] = useState ( '' )
116146 const [ waitingForRewrite , setWaitingForRewrite ] = useState ( false )
147+ const [ fileGuidance , setFileGuidance ] = useState < string > ( '' )
148+ const [ guidanceSaving , setGuidanceSaving ] = useState ( false )
117149 const pollRef = useRef < number | null > ( null )
118150
119151 const loadCatalog = useCallback ( async ( ) => {
@@ -199,14 +231,33 @@ export default function MemoryManagementPage() {
199231 return m
200232 } , [ catalog ] )
201233
234+ const modificationByFileId = useMemo ( ( ) => {
235+ const m = new Map < string , MemoryModification > ( )
236+ for ( const mod of catalog ?. modifications ?? [ ] ) m . set ( mod . fileId , mod )
237+ return m
238+ } , [ catalog ] )
239+
240+ const changedFileIds = useMemo ( ( ) => new Set ( Array . from ( modificationByFileId . keys ( ) ) ) , [ modificationByFileId ] )
241+
202242 const selectedFileId = useMemo ( ( ) => {
203243 if ( ! selectedPath ) return null
204244 const file = catalog ?. files . find ( f => f . path === selectedPath )
205245 return file ?. id ?? null
206246 } , [ catalog , selectedPath ] )
207247
248+ const selectedModification = useMemo ( ( ) => {
249+ if ( ! selectedFileId ) return null
250+ return modificationByFileId . get ( selectedFileId ) ?? null
251+ } , [ selectedFileId , modificationByFileId ] )
252+
253+ const diffLines = useMemo ( ( ) => {
254+ if ( ! selectedModification ) return [ ]
255+ return computeSimpleDiff ( selectedModification . oldContent , selectedModification . newContent )
256+ } , [ selectedModification ] )
257+
208258 const openFile = async ( file : MemoryFile ) => {
209259 setSelectedPath ( file . path )
260+ setFileGuidance ( file . guidance ?? '' )
210261 setAgentDeleteRequest ( '' )
211262 setFileLoading ( true )
212263 try {
@@ -222,8 +273,25 @@ export default function MemoryManagementPage() {
222273 }
223274 }
224275
276+ const saveGuidance = async ( filePath : string , value : string ) => {
277+ setGuidanceSaving ( true )
278+ try {
279+ await fetch ( '/api/memory/guidance' , {
280+ method : 'POST' ,
281+ headers : { 'Content-Type' : 'application/json' } ,
282+ body : JSON . stringify ( { path : filePath , guidance : value } ) ,
283+ } )
284+ } finally {
285+ setGuidanceSaving ( false )
286+ }
287+ }
288+
225289 const openFileByPath = ( relPath : string ) => {
226- const file = catalog ?. files . find ( f => f . relativePath === relPath )
290+ const file = catalog ?. files . find ( f =>
291+ f . relativePath === relPath ||
292+ f . relativePath === `/${ relPath } ` ||
293+ f . relativePath === relPath . replace ( / ^ \/ / , '' )
294+ )
227295 if ( file ) openFile ( file )
228296 }
229297
@@ -367,7 +435,7 @@ export default function MemoryManagementPage() {
367435 < div className = "mt-4 space-y-4" >
368436 { mindGroups . map ( group => {
369437 const collapsed = collapsedMindGroups . has ( group . id )
370- const treeNodes = buildMemoryTree ( group . files , new Set ( ) )
438+ const treeNodes = buildMemoryTree ( group . files , changedFileIds )
371439 return (
372440 < div key = { group . id } >
373441 < button
@@ -415,18 +483,24 @@ export default function MemoryManagementPage() {
415483 const file = fileMap . get ( fileId )
416484 if ( ! file ) return null
417485 const active = selectedPath === file . path
486+ const changed = changedFileIds . has ( file . id )
418487 return (
419488 < button
420489 key = { file . id }
421490 onClick = { ( ) => openFile ( file ) }
422491 className = { `w-full text-left px-2 py-1.5 rounded border ${
423492 active
424493 ? 'border-blue-300 dark:border-blue-700 bg-blue-50 dark:bg-blue-950'
425- : 'border-gray-100 dark:border-zinc-800 hover:bg-gray-50 dark:hover:bg-zinc-800'
494+ : changed
495+ ? 'border-amber-200 dark:border-amber-800 bg-amber-50 dark:bg-amber-950 hover:bg-amber-100 dark:hover:bg-amber-900'
496+ : 'border-gray-100 dark:border-zinc-800 hover:bg-gray-50 dark:hover:bg-zinc-800'
426497 } `}
427498 title = { file . path }
428499 >
429- < p className = "text-xs font-mono text-zinc-700 dark:text-slate-300 truncate" > { file . relativePath } </ p >
500+ < div className = "flex items-center gap-1.5 min-w-0" >
501+ { changed && < span className = "shrink-0 text-[10px] font-bold px-1 py-0.5 rounded bg-amber-200 dark:bg-amber-900 text-amber-700 dark:text-amber-300" > M</ span > }
502+ < p className = { `text-xs font-mono truncate ${ changed ? 'text-amber-700 dark:text-amber-300' : 'text-zinc-700 dark:text-slate-300' } ` } > { file . relativePath } </ p >
503+ </ div >
430504 < p className = "text-[11px] text-zinc-500 dark:text-slate-400 mt-0.5 truncate" > { file . preview } </ p >
431505 </ button >
432506 )
@@ -481,6 +555,49 @@ export default function MemoryManagementPage() {
481555 < p className = "text-xs text-amber-700 dark:text-amber-400" > { agentDeleteRequest } </ p >
482556 </ div >
483557 ) }
558+ { selectedModification && (
559+ < div className = "mt-4 mb-1" >
560+ < div className = "flex items-center gap-2 mb-2" >
561+ < span className = "text-xs font-bold px-1.5 py-0.5 rounded bg-amber-100 dark:bg-amber-950 text-amber-700 dark:text-amber-300" > M</ span >
562+ < p className = "text-xs font-medium text-zinc-700 dark:text-slate-300" > Proposed Changes</ p >
563+ </ div >
564+ < div className = "border border-gray-200 dark:border-zinc-700 rounded overflow-hidden max-h-80 overflow-y-auto mb-4" >
565+ < table className = "w-full text-xs font-mono border-collapse" >
566+ < tbody >
567+ { diffLines . map ( ( line , idx ) => (
568+ < tr key = { idx } className = { line . type === 'add' ? 'bg-green-50 dark:bg-green-950' : line . type === 'remove' ? 'bg-red-50 dark:bg-red-950' : '' } >
569+ < td className = "w-8 text-right px-2 py-0.5 text-zinc-400 dark:text-slate-500 select-none border-r border-gray-100 dark:border-zinc-800"
570+ style = { { borderLeft : line . type === 'add' ? '3px solid rgba(34,197,94,.8)' : line . type === 'remove' ? '3px solid rgba(239,68,68,.8)' : '3px solid transparent' } } >
571+ { idx + 1 }
572+ </ td >
573+ < td className = "w-5 px-1 py-0.5 text-center font-bold select-none"
574+ style = { { color : line . type === 'add' ? 'rgb(21,128,61)' : line . type === 'remove' ? 'rgb(185,28,28)' : 'transparent' } } >
575+ { line . type === 'add' ? '+' : line . type === 'remove' ? '−' : ' ' }
576+ </ td >
577+ < td className = { `px-2 py-0.5 whitespace-pre-wrap break-words ${ line . type === 'add' ? 'text-green-700 dark:text-green-300' : line . type === 'remove' ? 'text-red-700 dark:text-red-300' : 'text-zinc-600 dark:text-slate-400' } ` } >
578+ { line . text }
579+ </ td >
580+ </ tr >
581+ ) ) }
582+ </ tbody >
583+ </ table >
584+ </ div >
585+ </ div >
586+ ) }
587+ < div className = "mt-4" >
588+ < label className = "text-xs font-medium text-zinc-500 dark:text-slate-400 block mb-1" >
589+ Update Guidance
590+ { guidanceSaving && < span className = "ml-2 text-zinc-400 dark:text-slate-500" > saving…</ span > }
591+ </ label >
592+ < textarea
593+ className = "w-full text-xs border border-gray-200 dark:border-zinc-700 rounded px-2 py-1.5 bg-white dark:bg-zinc-900 text-zinc-700 dark:text-slate-300 resize-none"
594+ rows = { 3 }
595+ placeholder = "Describe how this file should be updated (e.g. 'always append new bugs as bullet points under Key Bugs'). Saved as preference."
596+ value = { fileGuidance }
597+ onChange = { e => setFileGuidance ( e . target . value ) }
598+ onBlur = { e => { if ( selectedContent ) void saveGuidance ( selectedContent . path , e . target . value ) } }
599+ />
600+ </ div >
484601 < div className = "mt-3 max-h-[70vh] overflow-y-auto pr-1" >
485602 { renderMarkdownFriendly ( selectedContent . content ) }
486603 </ div >
0 commit comments