@@ -33,7 +33,7 @@ import { IListVirtualDelegate, IListRenderer } from '../../../../../base/browser
3333import { ThemeIcon } from '../../../../../base/common/themables.js' ;
3434import { Codicon } from '../../../../../base/common/codicons.js' ;
3535import { IOpenerService } from '../../../../../platform/opener/common/opener.js' ;
36- import { basename , dirname , isEqual , isEqualOrParent } from '../../../../../base/common/resources.js' ;
36+ import { basename , dirname , isEqual } from '../../../../../base/common/resources.js' ;
3737import { URI } from '../../../../../base/common/uri.js' ;
3838import { AICustomizationManagementEditorInput } from './aiCustomizationManagementEditorInput.js' ;
3939import { AICustomizationListWidget } from './aiCustomizationListWidget.js' ;
@@ -67,7 +67,7 @@ import { AGENT_MD_FILENAME } from '../../common/promptSyntax/config/promptFileLo
6767import { getAttributeDefinition , getTarget } from '../../common/promptSyntax/languageProviders/promptFileAttributes.js' ;
6868import { INewPromptOptions , NEW_PROMPT_COMMAND_ID , NEW_INSTRUCTIONS_COMMAND_ID , NEW_AGENT_COMMAND_ID , NEW_SKILL_COMMAND_ID } from '../promptSyntax/newPromptFileActions.js' ;
6969import { showConfigureHooksQuickPick } from '../promptSyntax/hookActions.js' ;
70- import { resolveWorkspaceTargetDirectory , resolveUserTargetDirectory } from './customizationCreatorService.js' ;
70+ import { resolveWorkspaceTargetDirectory , resolveUserTargetDirectory , CustomizationLocationPicker } from './customizationCreatorService.js' ;
7171import { ICommandService } from '../../../../../platform/commands/common/commands.js' ;
7272import { AICustomizationSources , IAICustomizationWorkspaceService } from '../../common/aiCustomizationWorkspaceService.js' ;
7373import { CodeEditorWidget } from '../../../../../editor/browser/widget/codeEditor/codeEditorWidget.js' ;
@@ -90,13 +90,12 @@ import { IExtension } from '../../../extensions/common/extensions.js';
9090import { EmbeddedMcpServerDetail } from './embeddedMcpServerDetail.js' ;
9191import { EmbeddedAgentPluginDetail } from './embeddedAgentPluginDetail.js' ;
9292import { EmbeddedExtensionToolsDetail } from './embeddedExtensionToolsDetail.js' ;
93- import { ICustomizationHarnessService , ICustomizationSourceFolder } from '../../common/customizationHarnessService.js' ;
93+ import { ICustomizationHarnessService } from '../../common/customizationHarnessService.js' ;
9494import { ChatConfiguration } from '../../common/constants.js' ;
9595import { AICustomizationWelcomePage } from './aiCustomizationWelcomePage.js' ;
9696import { IViewsService } from '../../../../services/views/common/viewsService.js' ;
97- import { ResourceSet } from '../../../../../base/common/map.js' ;
98- import { PromptsServiceCustomizationItemProvider } from './promptsServiceCustomizationItemProvider.js' ;
9997import { ILabelService } from '../../../../../platform/label/common/label.js' ;
98+ import { showNoFoldersDialog } from '../promptSyntax/pickers/askForPromptSourceFolder.js' ;
10099
101100const $ = DOM . $ ;
102101
@@ -1160,7 +1159,7 @@ export class AICustomizationManagementEditor extends EditorPane {
11601159 /**
11611160 * Creates a new prompt file and opens it in the embedded editor.
11621161 */
1163- private async createNewItemManual ( type : PromptsType , target : 'workspace ' | 'user' | 'workspace-root' , rootFileName ?: string ) : Promise < void > {
1162+ private async createNewItemManual ( type : PromptsType , target : 'local ' | 'user' | 'workspace-root' , rootFileName ?: string ) : Promise < void > {
11641163 this . telemetryService . publicLog2 < CustomizationEditorCreateItemEvent , CustomizationEditorCreateItemClassification > ( 'chatCustomizationEditor.createItem' , {
11651164 section : this . selectedSection ?? 'welcome' ,
11661165 promptType : type ,
@@ -1211,15 +1210,23 @@ export class AICustomizationManagementEditor extends EditorPane {
12111210 }
12121211 return ;
12131212 }
1214-
1215- const targetDir = await this . resolveTargetDirectoryWithPicker ( type , target ) ;
1213+ const sessionResource = this . harnessService . activeSessionResource . get ( ) ;
1214+ const picker = this . instantiationService . createInstance ( CustomizationLocationPicker ) ;
1215+ const targetDir = await picker . resolveTargetDirectoryWithPicker (
1216+ sessionResource ,
1217+ type ,
1218+ target ,
1219+ ) ;
12161220 if ( targetDir === null ) {
12171221 return ; // User cancelled the picker
12181222 }
1219- // targetDir may be undefined when no matching folder exists for the
1220- // requested storage type (e.g. skills have no user-storage folder).
1221- // Pass it through. The command handles undefined by showing its own
1222- // folder picker via askForPromptSourceFolder.
1223+
1224+ if ( targetDir === undefined ) {
1225+ // targetDir may be undefined when no matching folder exists for the
1226+ // requested storage type (e.g. skills have no user-storage folder).
1227+ await this . instantiationService . invokeFunction ( showNoFoldersDialog , type ) ;
1228+ return ;
1229+ }
12231230
12241231 // When the active harness overrides the file extension (e.g. Claude
12251232 // rules use .md instead of .instructions.md), pass it through so the
@@ -1228,11 +1235,11 @@ export class AICustomizationManagementEditor extends EditorPane {
12281235
12291236 const options : INewPromptOptions = {
12301237 targetFolder : targetDir ,
1231- targetStorage : target === ' user' ? PromptsStorage . user : PromptsStorage . local ,
1238+ targetStorage : target === AICustomizationSources . user ? PromptsStorage . user : PromptsStorage . local ,
12321239 fileExtension : override ?. fileExtension ,
12331240 openFile : async ( uri ) => {
1234- const isWorkspace = target === 'workspace' ;
1235- await this . showEmbeddedEditor ( uri , basename ( uri ) , type , target === 'user' ? PromptsStorage . user : PromptsStorage . local , isWorkspace ) ;
1241+ const isWorkspace = target === AICustomizationSources . local ;
1242+ await this . showEmbeddedEditor ( uri , basename ( uri ) , type , target , isWorkspace ) ;
12361243 return this . embeddedEditor ;
12371244 } ,
12381245 } ;
@@ -1250,72 +1257,6 @@ export class AICustomizationManagementEditor extends EditorPane {
12501257 this . listWidget . refresh ( ) ;
12511258 }
12521259
1253- /**
1254- * Resolves the target directory for creating a new customization file.
1255- * If multiple source folders exist for the given storage type, shows a
1256- * picker to let the user choose. Otherwise, returns the single match.
1257- *
1258- * Source folders come from the active harness's item provider (via the
1259- * items model). Each session can supply its own set of customization
1260- * locations through `ICustomizationItemProvider.provideSourceFolders`.
1261- *
1262- * @returns the resolved URI, `undefined` when no folder is available,
1263- * or `null` when the user cancelled the picker.
1264- */
1265- private async resolveTargetDirectoryWithPicker ( type : PromptsType , target : 'workspace' | 'user' ) : Promise < URI | undefined | null > {
1266- const sessionResource = this . harnessService . activeSessionResource . get ( ) ;
1267- const activeDescriptor = this . harnessService . getActiveDescriptor ( ) ;
1268- const provider = activeDescriptor . itemProvider ?? this . instantiationService . createInstance ( PromptsServiceCustomizationItemProvider ) ;
1269- if ( ! provider . provideSourceFolders ) {
1270- return undefined ;
1271- }
1272- const allFolders = await provider . provideSourceFolders ( sessionResource , type , CancellationToken . None ) ;
1273- if ( ! allFolders ) {
1274- // Provider returned no source folders for this type/session.
1275- return undefined ;
1276- }
1277-
1278- const projectRoot = this . workspaceService . getActiveProjectRoot ( ) ;
1279- const matchingFolders : ICustomizationSourceFolder [ ] = [ ] ;
1280- const hasSeen = new ResourceSet ( ) ;
1281- for ( const f of allFolders ) {
1282- if ( target === 'workspace' ) {
1283- if ( projectRoot && isEqualOrParent ( f . uri , projectRoot ) && ! hasSeen . has ( f . uri ) ) {
1284- hasSeen . add ( f . uri ) ;
1285- matchingFolders . push ( f ) ;
1286- }
1287- } else {
1288- if ( ( ! projectRoot || ! isEqualOrParent ( f . uri , projectRoot ) ) && ! hasSeen . has ( f . uri ) ) {
1289- hasSeen . add ( f . uri ) ;
1290- matchingFolders . push ( f ) ;
1291- }
1292- }
1293- }
1294-
1295- if ( matchingFolders . length === 0 ) {
1296- // No matching folders. Return undefined so the command can fall
1297- // back to askForPromptSourceFolder (not null which means cancellation)
1298- return undefined ;
1299- }
1300-
1301- if ( matchingFolders . length === 1 ) {
1302- return matchingFolders [ 0 ] . uri ;
1303- }
1304-
1305- // Multiple directories. Ask the user which one to use.
1306- const items : ( IQuickPickItem & { uri : URI } ) [ ] = matchingFolders . map ( folder => ( {
1307- label : folder . label ,
1308- description : this . labelService . getUriLabel ( folder . uri , { relative : true } ) ,
1309- uri : folder . uri ,
1310- } ) ) ;
1311-
1312- const picked = await this . quickInputService . pick ( items , {
1313- placeHolder : localize ( 'selectTargetDirectory' , "Select a directory for the new customization file" ) ,
1314- } ) ;
1315-
1316- return picked ?. uri ?? null ;
1317- }
1318-
13191260 override updateStyles ( ) : void {
13201261 // The modal provides its own panel chrome, so the split view separator
13211262 // is intentionally hidden here regardless of theme.
0 commit comments