@@ -352,59 +352,128 @@ export class CustomModesManager {
352352 }
353353
354354 public async getCustomModes ( ) : Promise < ModeConfig [ ] > {
355- // Check if we have a valid cached result.
356- const now = Date . now ( )
355+ try {
356+ // Check if we have a valid cached result.
357+ const now = Date . now ( )
357358
358- if ( this . cachedModes && now - this . cachedAt < CustomModesManager . cacheTTL ) {
359- return this . cachedModes
360- }
359+ if ( this . cachedModes && now - this . cachedAt < CustomModesManager . cacheTTL ) {
360+ logger . debug ( "Returning cached custom modes" , { count : this . cachedModes . length } )
361+ return this . cachedModes
362+ }
361363
362- // Get modes from settings file.
363- const settingsPath = await this . getCustomModesFilePath ( )
364- const settingsModes = await this . loadModesFromFile ( settingsPath )
364+ logger . debug ( "Loading custom modes from files" )
365365
366- // Get modes from .roomodes if it exists.
367- const roomodesPath = await this . getWorkspaceRoomodes ( )
368- const roomodesModes = roomodesPath ? await this . loadModesFromFile ( roomodesPath ) : [ ]
366+ // Get modes from settings file.
367+ const settingsPath = await this . getCustomModesFilePath ( )
368+ const settingsModes = await this . loadModesFromFile ( settingsPath )
369+
370+ // Get modes from .roomodes if it exists.
371+ const roomodesPath = await this . getWorkspaceRoomodes ( )
372+ const roomodesModes = roomodesPath ? await this . loadModesFromFile ( roomodesPath ) : [ ]
369373
370- // Create maps to store modes by source.
371- const projectModes = new Map < string , ModeConfig > ( )
372- const globalModes = new Map < string , ModeConfig > ( )
374+ logger . debug ( "Loaded modes from files" , {
375+ settingsCount : settingsModes . length ,
376+ roomodesCount : roomodesModes . length ,
377+ settingsPath,
378+ roomodesPath
379+ } )
373380
374- // Add project modes (they take precedence).
375- for ( const mode of roomodesModes ) {
376- projectModes . set ( mode . slug , { ...mode , source : "project" as const } )
377- }
381+ // Create maps to store modes by source.
382+ const projectModes = new Map < string , ModeConfig > ( )
383+ const globalModes = new Map < string , ModeConfig > ( )
378384
379- // Add global modes.
380- for ( const mode of settingsModes ) {
381- if ( ! projectModes . has ( mode . slug ) ) {
382- globalModes . set ( mode . slug , { ...mode , source : "global" as const } )
385+ // Add project modes (they take precedence).
386+ for ( const mode of roomodesModes ) {
387+ if ( ! mode . slug ) {
388+ logger . warn ( "Found mode without slug in roomodes, skipping" , { mode } )
389+ continue
390+ }
391+ projectModes . set ( mode . slug , { ...mode , source : "project" as const } )
383392 }
384- }
385393
386- // Combine modes in the correct order: project modes first, then global modes.
387- const mergedModes = [
388- ...roomodesModes . map ( ( mode ) => ( { ...mode , source : "project" as const } ) ) ,
389- ...settingsModes
390- . filter ( ( mode ) => ! projectModes . has ( mode . slug ) )
391- . map ( ( mode ) => ( { ...mode , source : "global" as const } ) ) ,
392- ]
394+ // Add global modes.
395+ for ( const mode of settingsModes ) {
396+ if ( ! mode . slug ) {
397+ logger . warn ( "Found mode without slug in settings, skipping" , { mode } )
398+ continue
399+ }
400+ if ( ! projectModes . has ( mode . slug ) ) {
401+ globalModes . set ( mode . slug , { ...mode , source : "global" as const } )
402+ }
403+ }
393404
394- await this . context . globalState . update ( "customModes" , mergedModes )
405+ // Combine modes in the correct order: project modes first, then global modes.
406+ const mergedModes = [
407+ ...roomodesModes
408+ . filter ( ( mode ) => mode . slug ) // Defensive check
409+ . map ( ( mode ) => ( { ...mode , source : "project" as const } ) ) ,
410+ ...settingsModes
411+ . filter ( ( mode ) => mode . slug && ! projectModes . has ( mode . slug ) ) // Defensive checks
412+ . map ( ( mode ) => ( { ...mode , source : "global" as const } ) ) ,
413+ ]
414+
415+ logger . debug ( "Merged custom modes" , {
416+ totalCount : mergedModes . length ,
417+ projectCount : projectModes . size ,
418+ globalCount : globalModes . size
419+ } )
420+
421+ // Validate merged modes before updating state
422+ const validModes = mergedModes . filter ( ( mode ) => {
423+ const isValid = mode . slug && mode . name && mode . roleDefinition && Array . isArray ( mode . groups )
424+ if ( ! isValid ) {
425+ logger . warn ( "Found invalid mode, filtering out" , { mode } )
426+ }
427+ return isValid
428+ } )
429+
430+ if ( validModes . length !== mergedModes . length ) {
431+ logger . warn ( "Filtered out invalid modes" , {
432+ original : mergedModes . length ,
433+ valid : validModes . length
434+ } )
435+ }
436+
437+ // Update global state with validated modes
438+ await this . context . globalState . update ( "customModes" , validModes )
395439
396- this . cachedModes = mergedModes
397- this . cachedAt = now
440+ // Update cache
441+ this . cachedModes = validModes
442+ this . cachedAt = now
398443
399- return mergedModes
444+ logger . info ( "Successfully loaded custom modes" , { count : validModes . length } )
445+ return validModes
446+ } catch ( error ) {
447+ const errorMessage = error instanceof Error ? error . message : String ( error )
448+ logger . error ( "Failed to get custom modes" , { error : errorMessage } )
449+
450+ // Try to recover from global state
451+ try {
452+ const fallbackModes = this . context . globalState . get ( "customModes" , [ ] )
453+ logger . info ( "Recovered custom modes from global state" , { count : fallbackModes . length } )
454+
455+ // Update cache with fallback data
456+ this . cachedModes = fallbackModes
457+ this . cachedAt = Date . now ( )
458+
459+ return fallbackModes
460+ } catch ( recoveryError ) {
461+ logger . error ( "Failed to recover custom modes from global state" , {
462+ error : recoveryError instanceof Error ? recoveryError . message : String ( recoveryError )
463+ } )
464+
465+ // Return empty array as last resort
466+ return [ ]
467+ }
468+ }
400469 }
401470
402471 public async updateCustomMode ( slug : string , config : ModeConfig ) : Promise < void > {
403472 try {
404473 // Validate the mode configuration before saving
405474 const validationResult = modeConfigSchema . safeParse ( config )
406475 if ( ! validationResult . success ) {
407- const errors = validationResult . error . errors . map ( ( e ) => e . message ) . join ( ", " )
476+ const errors = validationResult . error . errors . map ( ( e : any ) => e . message ) . join ( ", " )
408477 logger . error ( `Invalid mode configuration for ${ slug } ` , { errors : validationResult . error . errors } )
409478 throw new Error ( `Invalid mode configuration: ${ errors } ` )
410479 }
@@ -433,25 +502,43 @@ export class CustomModesManager {
433502 }
434503
435504 await this . queueWrite ( async ( ) => {
436- // Ensure source is set correctly based on target file.
437- const modeWithSource = {
438- ...config ,
439- source : isProjectMode ? ( "project" as const ) : ( "global" as const ) ,
440- }
505+ try {
506+ // Ensure source is set correctly based on target file.
507+ const modeWithSource = {
508+ ...config ,
509+ source : isProjectMode ? ( "project" as const ) : ( "global" as const ) ,
510+ }
441511
442- await this . updateModesInFile ( targetPath , ( modes ) => {
443- const updatedModes = modes . filter ( ( m ) => m . slug !== slug )
444- updatedModes . push ( modeWithSource )
445- return updatedModes
446- } )
512+ await this . updateModesInFile ( targetPath , ( modes ) => {
513+ const updatedModes = modes . filter ( ( m ) => m . slug !== slug )
514+ updatedModes . push ( modeWithSource )
515+ return updatedModes
516+ } )
447517
448- this . clearCache ( )
449- await this . refreshMergedState ( )
518+ // Refresh state before clearing cache to ensure consistency
519+ await this . refreshMergedState ( )
520+ this . clearCache ( )
521+
522+ // Log successful update for debugging
523+ logger . info ( `Successfully updated custom mode: ${ slug } ` , {
524+ source : modeWithSource . source ,
525+ targetPath
526+ } )
527+ } catch ( writeError ) {
528+ // If file write fails, ensure cache is still cleared to prevent stale data
529+ this . clearCache ( )
530+ throw writeError
531+ }
450532 } )
451533 } catch ( error ) {
452534 const errorMessage = error instanceof Error ? error . message : String ( error )
453535 logger . error ( "Failed to update custom mode" , { slug, error : errorMessage } )
536+
537+ // Clear cache on error to prevent inconsistent state
538+ this . clearCache ( )
539+
454540 vscode . window . showErrorMessage ( t ( "common:customModes.errors.updateFailed" , { error : errorMessage } ) )
541+ throw error // Re-throw to allow caller to handle
455542 }
456543 }
457544
@@ -487,18 +574,52 @@ export class CustomModesManager {
487574 }
488575
489576 private async refreshMergedState ( ) : Promise < void > {
490- const settingsPath = await this . getCustomModesFilePath ( )
491- const roomodesPath = await this . getWorkspaceRoomodes ( )
577+ try {
578+ const settingsPath = await this . getCustomModesFilePath ( )
579+ const roomodesPath = await this . getWorkspaceRoomodes ( )
580+
581+ logger . debug ( "Refreshing merged state" , { settingsPath, roomodesPath } )
582+
583+ const settingsModes = await this . loadModesFromFile ( settingsPath )
584+ const roomodesModes = roomodesPath ? await this . loadModesFromFile ( roomodesPath ) : [ ]
585+ const mergedModes = await this . mergeCustomModes ( roomodesModes , settingsModes )
492586
493- const settingsModes = await this . loadModesFromFile ( settingsPath )
494- const roomodesModes = roomodesPath ? await this . loadModesFromFile ( roomodesPath ) : [ ]
495- const mergedModes = await this . mergeCustomModes ( roomodesModes , settingsModes )
587+ logger . debug ( "Merged modes loaded" , {
588+ settingsCount : settingsModes . length ,
589+ roomodesCount : roomodesModes . length ,
590+ mergedCount : mergedModes . length
591+ } )
496592
497- await this . context . globalState . update ( "customModes" , mergedModes )
593+ // Update global state with merged modes
594+ await this . context . globalState . update ( "customModes" , mergedModes )
498595
499- this . clearCache ( )
596+ // Update cache with fresh data
597+ this . cachedModes = mergedModes
598+ this . cachedAt = Date . now ( )
500599
501- await this . onUpdate ( )
600+ // Notify listeners of the update
601+ await this . onUpdate ( )
602+ } catch ( error ) {
603+ const errorMessage = error instanceof Error ? error . message : String ( error )
604+ logger . error ( "Failed to refresh merged state" , { error : errorMessage } )
605+
606+ // On error, clear cache to prevent stale data
607+ this . clearCache ( )
608+
609+ // Try to recover by loading from global state
610+ try {
611+ const fallbackModes = this . context . globalState . get ( "customModes" , [ ] )
612+ logger . info ( "Recovered custom modes from global state" , { count : fallbackModes . length } )
613+ this . cachedModes = fallbackModes
614+ this . cachedAt = Date . now ( )
615+ } catch ( recoveryError ) {
616+ logger . error ( "Failed to recover custom modes from global state" , {
617+ error : recoveryError instanceof Error ? recoveryError . message : String ( recoveryError )
618+ } )
619+ }
620+
621+ throw error
622+ }
502623 }
503624
504625 public async deleteCustomMode ( slug : string ) : Promise < void > {
@@ -729,10 +850,10 @@ export class CustomModesManager {
729850
730851 // Merge custom prompts if provided
731852 if ( customPrompts ) {
732- if ( customPrompts . roleDefinition ) exportMode . roleDefinition = customPrompts . roleDefinition
733- if ( customPrompts . description ) exportMode . description = customPrompts . description
734- if ( customPrompts . whenToUse ) exportMode . whenToUse = customPrompts . whenToUse
735- if ( customPrompts . customInstructions ) exportMode . customInstructions = customPrompts . customInstructions
853+ if ( customPrompts . roleDefinition ) ( exportMode as any ) . roleDefinition = customPrompts . roleDefinition
854+ if ( customPrompts . description ) ( exportMode as any ) . description = customPrompts . description
855+ if ( customPrompts . whenToUse ) ( exportMode as any ) . whenToUse = customPrompts . whenToUse
856+ if ( customPrompts . customInstructions ) ( exportMode as any ) . customInstructions = customPrompts . customInstructions
736857 }
737858
738859 // Add rules files if any exist
@@ -799,24 +920,24 @@ export class CustomModesManager {
799920 // Validate the mode configuration
800921 const validationResult = modeConfigSchema . safeParse ( modeConfig )
801922 if ( ! validationResult . success ) {
802- logger . error ( `Invalid mode configuration for ${ modeConfig . slug } ` , {
923+ logger . error ( `Invalid mode configuration for ${ ( modeConfig as any ) . slug } ` , {
803924 errors : validationResult . error . errors ,
804925 } )
805926 return {
806927 success : false ,
807- error : `Invalid mode configuration for ${ modeConfig . slug } : ${ validationResult . error . errors . map ( ( e ) => e . message ) . join ( ", " ) } ` ,
928+ error : `Invalid mode configuration for ${ ( modeConfig as any ) . slug } : ${ validationResult . error . errors . map ( ( e : any ) => e . message ) . join ( ", " ) } ` ,
808929 }
809930 }
810931
811932 // Check for existing mode conflicts
812933 const existingModes = await this . getCustomModes ( )
813- const existingMode = existingModes . find ( ( m ) => m . slug === importMode . slug )
934+ const existingMode = existingModes . find ( ( m ) => m . slug === ( importMode as any ) . slug )
814935 if ( existingMode ) {
815- logger . info ( `Overwriting existing mode: ${ importMode . slug } ` )
936+ logger . info ( `Overwriting existing mode: ${ ( importMode as any ) . slug } ` )
816937 }
817938
818939 // Import the mode configuration with the specified source
819- await this . updateCustomMode ( importMode . slug , {
940+ await this . updateCustomMode ( ( importMode as any ) . slug , {
820941 ...modeConfig ,
821942 source : source , // Use the provided source parameter
822943 } )
@@ -827,13 +948,13 @@ export class CustomModesManager {
827948
828949 // Always remove the existing rules folder for this mode if it exists
829950 // This ensures that if the imported mode has no rules, the folder is cleaned up
830- const rulesFolderPath = path . join ( workspacePath , ".roo" , `rules-${ importMode . slug } ` )
951+ const rulesFolderPath = path . join ( workspacePath , ".roo" , `rules-${ ( importMode as any ) . slug } ` )
831952 try {
832953 await fs . rm ( rulesFolderPath , { recursive : true , force : true } )
833- logger . info ( `Removed existing rules folder for mode ${ importMode . slug } ` )
954+ logger . info ( `Removed existing rules folder for mode ${ ( importMode as any ) . slug } ` )
834955 } catch ( error ) {
835956 // It's okay if the folder doesn't exist
836- logger . debug ( `No existing rules folder to remove for mode ${ importMode . slug } ` )
957+ logger . debug ( `No existing rules folder to remove for mode ${ ( importMode as any ) . slug } ` )
837958 }
838959
839960 // Only create new rules files if they exist in the import
@@ -875,13 +996,13 @@ export class CustomModesManager {
875996
876997 // Always remove the existing rules folder for this mode if it exists
877998 // This ensures that if the imported mode has no rules, the folder is cleaned up
878- const rulesFolderPath = path . join ( globalRooDir , `rules-${ importMode . slug } ` )
999+ const rulesFolderPath = path . join ( globalRooDir , `rules-${ ( importMode as any ) . slug } ` )
8791000 try {
8801001 await fs . rm ( rulesFolderPath , { recursive : true , force : true } )
881- logger . info ( `Removed existing global rules folder for mode ${ importMode . slug } ` )
1002+ logger . info ( `Removed existing global rules folder for mode ${ ( importMode as any ) . slug } ` )
8821003 } catch ( error ) {
8831004 // It's okay if the folder doesn't exist
884- logger . debug ( `No existing global rules folder to remove for mode ${ importMode . slug } ` )
1005+ logger . debug ( `No existing global rules folder to remove for mode ${ ( importMode as any ) . slug } ` )
8851006 }
8861007
8871008 // Import the new rules files with path validation
0 commit comments