@@ -102,6 +102,61 @@ export class CustomModesManager {
102102 return exists ? roomodesPath : undefined
103103 }
104104
105+ /**
106+ * Get all .roomodes files in the hierarchy from workspace root up to parent directories
107+ * @returns Array of .roomodes file paths, ordered from most general (parent) to most specific (workspace)
108+ */
109+ private async getHierarchicalRoomodes ( ) : Promise < string [ ] > {
110+ const workspaceFolders = vscode . workspace . workspaceFolders
111+
112+ if ( ! workspaceFolders || workspaceFolders . length === 0 ) {
113+ return [ ]
114+ }
115+
116+ const workspaceRoot = getWorkspacePath ( )
117+ const roomodesFiles : string [ ] = [ ]
118+ const visitedPaths = new Set < string > ( )
119+ const homeDir = os . homedir ( )
120+ let currentPath = path . resolve ( workspaceRoot )
121+
122+ // Walk up the directory tree from workspace root
123+ while ( currentPath && currentPath !== path . dirname ( currentPath ) ) {
124+ // Avoid infinite loops
125+ if ( visitedPaths . has ( currentPath ) ) {
126+ break
127+ }
128+ visitedPaths . add ( currentPath )
129+
130+ // Don't look for .roomodes in the home directory
131+ if ( currentPath === homeDir ) {
132+ break
133+ }
134+
135+ // Check if .roomodes exists at this level
136+ const roomodesPath = path . join ( currentPath , ROOMODES_FILENAME )
137+ if ( await fileExistsAtPath ( roomodesPath ) ) {
138+ roomodesFiles . push ( roomodesPath )
139+ }
140+
141+ // Move to parent directory
142+ const parentPath = path . dirname ( currentPath )
143+
144+ // Stop if we've reached the root or if parent is the same as current
145+ if (
146+ parentPath === currentPath ||
147+ parentPath === "/" ||
148+ ( process . platform === "win32" && parentPath === path . parse ( currentPath ) . root )
149+ ) {
150+ break
151+ }
152+
153+ currentPath = parentPath
154+ }
155+
156+ // Return in order from most general (parent) to most specific (workspace)
157+ return roomodesFiles . reverse ( )
158+ }
159+
105160 /**
106161 * Regex pattern for problematic characters that need to be cleaned from YAML content
107162 * Includes:
@@ -293,12 +348,17 @@ export class CustomModesManager {
293348 return
294349 }
295350
296- // Get modes from .roomodes if it exists (takes precedence)
297- const roomodesPath = await this . getWorkspaceRoomodes ( )
298- const roomodesModes = roomodesPath ? await this . loadModesFromFile ( roomodesPath ) : [ ]
351+ // Get modes from hierarchical .roomodes
352+ const hierarchicalRoomodes = await this . getHierarchicalRoomodes ( )
353+ const allRoomodesModes : ModeConfig [ ] = [ ]
354+
355+ for ( const roomodesPath of hierarchicalRoomodes ) {
356+ const modes = await this . loadModesFromFile ( roomodesPath )
357+ allRoomodesModes . push ( ...modes )
358+ }
299359
300- // Merge modes from both sources (.roomodes takes precedence)
301- const mergedModes = await this . mergeCustomModes ( roomodesModes , result . data . customModes )
360+ // Merge modes from all sources
361+ const mergedModes = await this . mergeCustomModes ( allRoomodesModes , result . data . customModes )
302362 await this . context . globalState . update ( "customModes" , mergedModes )
303363 this . clearCache ( )
304364 await this . onUpdate ( )
@@ -312,19 +372,28 @@ export class CustomModesManager {
312372 this . disposables . push ( settingsWatcher . onDidDelete ( handleSettingsChange ) )
313373 this . disposables . push ( settingsWatcher )
314374
315- // Watch .roomodes file - watch the path even if it doesn't exist yet
375+ // Watch .roomodes files in hierarchy
316376 const workspaceFolders = vscode . workspace . workspaceFolders
317377 if ( workspaceFolders && workspaceFolders . length > 0 ) {
318- const workspaceRoot = getWorkspacePath ( )
319- const roomodesPath = path . join ( workspaceRoot , ROOMODES_FILENAME )
320- const roomodesWatcher = vscode . workspace . createFileSystemWatcher ( roomodesPath )
378+ // Create a generic pattern to watch all .roomodes files in the workspace tree
379+ const roomodesPattern = new vscode . RelativePattern ( workspaceFolders [ 0 ] , "**/.roomodes" )
380+ const roomodesWatcher = vscode . workspace . createFileSystemWatcher ( roomodesPattern )
321381
322382 const handleRoomodesChange = async ( ) => {
323383 try {
324384 const settingsModes = await this . loadModesFromFile ( settingsPath )
325- const roomodesModes = await this . loadModesFromFile ( roomodesPath )
326- // .roomodes takes precedence
327- const mergedModes = await this . mergeCustomModes ( roomodesModes , settingsModes )
385+
386+ // Get modes from hierarchical .roomodes
387+ const hierarchicalRoomodes = await this . getHierarchicalRoomodes ( )
388+ const allRoomodesModes : ModeConfig [ ] = [ ]
389+
390+ for ( const roomodesPath of hierarchicalRoomodes ) {
391+ const modes = await this . loadModesFromFile ( roomodesPath )
392+ allRoomodesModes . push ( ...modes )
393+ }
394+
395+ // Merge modes from all sources
396+ const mergedModes = await this . mergeCustomModes ( allRoomodesModes , settingsModes )
328397 await this . context . globalState . update ( "customModes" , mergedModes )
329398 this . clearCache ( )
330399 await this . onUpdate ( )
@@ -335,19 +404,7 @@ export class CustomModesManager {
335404
336405 this . disposables . push ( roomodesWatcher . onDidChange ( handleRoomodesChange ) )
337406 this . disposables . push ( roomodesWatcher . onDidCreate ( handleRoomodesChange ) )
338- this . disposables . push (
339- roomodesWatcher . onDidDelete ( async ( ) => {
340- // When .roomodes is deleted, refresh with only settings modes
341- try {
342- const settingsModes = await this . loadModesFromFile ( settingsPath )
343- await this . context . globalState . update ( "customModes" , settingsModes )
344- this . clearCache ( )
345- await this . onUpdate ( )
346- } catch ( error ) {
347- console . error ( `[CustomModesManager] Error handling .roomodes file deletion:` , error )
348- }
349- } ) ,
350- )
407+ this . disposables . push ( roomodesWatcher . onDidDelete ( handleRoomodesChange ) )
351408 this . disposables . push ( roomodesWatcher )
352409 }
353410 }
@@ -360,37 +417,35 @@ export class CustomModesManager {
360417 return this . cachedModes
361418 }
362419
363- // Get modes from settings file.
420+ // Get modes from settings file (global)
364421 const settingsPath = await this . getCustomModesFilePath ( )
365422 const settingsModes = await this . loadModesFromFile ( settingsPath )
366423
367- // Get modes from .roomodes if it exists.
368- const roomodesPath = await this . getWorkspaceRoomodes ( )
369- const roomodesModes = roomodesPath ? await this . loadModesFromFile ( roomodesPath ) : [ ]
424+ // Get modes from hierarchical .roomodes files
425+ const hierarchicalRoomodes = await this . getHierarchicalRoomodes ( )
426+ const allRoomodesModes : ModeConfig [ ] = [ ]
370427
371- // Create maps to store modes by source.
372- const projectModes = new Map < string , ModeConfig > ( )
373- const globalModes = new Map < string , ModeConfig > ( )
374-
375- // Add project modes (they take precedence).
376- for ( const mode of roomodesModes ) {
377- projectModes . set ( mode . slug , { ...mode , source : "project" as const } )
428+ // Load modes from each .roomodes file in hierarchy
429+ for ( const roomodesPath of hierarchicalRoomodes ) {
430+ const modes = await this . loadModesFromFile ( roomodesPath )
431+ allRoomodesModes . push ( ...modes )
378432 }
379433
380- // Add global modes.
434+ // Create a map to handle mode precedence (more specific overrides more general)
435+ const modesMap = new Map < string , ModeConfig > ( )
436+
437+ // Add global modes first
381438 for ( const mode of settingsModes ) {
382- if ( ! projectModes . has ( mode . slug ) ) {
383- globalModes . set ( mode . slug , { ...mode , source : "global" as const } )
384- }
439+ modesMap . set ( mode . slug , { ...mode , source : "global" as const } )
440+ }
441+
442+ // Add hierarchical .roomodes modes (will override global and parent modes)
443+ for ( const mode of allRoomodesModes ) {
444+ modesMap . set ( mode . slug , { ...mode , source : "project" as const } )
385445 }
386446
387- // Combine modes in the correct order: project modes first, then global modes.
388- const mergedModes = [
389- ...roomodesModes . map ( ( mode ) => ( { ...mode , source : "project" as const } ) ) ,
390- ...settingsModes
391- . filter ( ( mode ) => ! projectModes . has ( mode . slug ) )
392- . map ( ( mode ) => ( { ...mode , source : "global" as const } ) ) ,
393- ]
447+ // Convert map to array
448+ const mergedModes = Array . from ( modesMap . values ( ) )
394449
395450 await this . context . globalState . update ( "customModes" , mergedModes )
396451
@@ -493,11 +548,18 @@ export class CustomModesManager {
493548
494549 private async refreshMergedState ( ) : Promise < void > {
495550 const settingsPath = await this . getCustomModesFilePath ( )
496- const roomodesPath = await this . getWorkspaceRoomodes ( )
497-
498551 const settingsModes = await this . loadModesFromFile ( settingsPath )
499- const roomodesModes = roomodesPath ? await this . loadModesFromFile ( roomodesPath ) : [ ]
500- const mergedModes = await this . mergeCustomModes ( roomodesModes , settingsModes )
552+
553+ // Get modes from hierarchical .roomodes
554+ const hierarchicalRoomodes = await this . getHierarchicalRoomodes ( )
555+ const allRoomodesModes : ModeConfig [ ] = [ ]
556+
557+ for ( const roomodesPath of hierarchicalRoomodes ) {
558+ const modes = await this . loadModesFromFile ( roomodesPath )
559+ allRoomodesModes . push ( ...modes )
560+ }
561+
562+ const mergedModes = await this . mergeCustomModes ( allRoomodesModes , settingsModes )
501563
502564 await this . context . globalState . update ( "customModes" , mergedModes )
503565
0 commit comments