Skip to content

Commit 297ec44

Browse files
[Cherry-Pick] customization UI: polish source folder pickers (#323527) (#324847)
1 parent 9cc1676 commit 297ec44

7 files changed

Lines changed: 38 additions & 103 deletions

File tree

src/vs/workbench/api/browser/mainThreadChatAgents2.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -809,6 +809,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA
809809
return folders.map(folder => ({
810810
uri: URI.revive(folder.uri),
811811
label: folder.label,
812+
source: folder.source,
812813
}));
813814
},
814815
};

src/vs/workbench/contrib/chat/browser/agentSessions/agentHost/agentCustomizationItemProvider.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,8 @@ export class AgentCustomizationItemProvider extends Disposable implements ICusto
159159
}
160160

161161
async provideSourceFolders(sessionResource: URI, type: PromptsType, _token: CancellationToken): Promise<readonly ICustomizationSourceFolder[]> {
162+
const workingDirectory = this._customAgentsService.getWorkingDirectory(sessionResource);
163+
162164
const folders: ICustomizationSourceFolder[] = [];
163165
for (const customization of this._customAgentsService.getCustomizations(sessionResource)) {
164166
if (!isDirectoryCustomization(customization) || !customization.writable) {
@@ -167,9 +169,11 @@ export class AgentCustomizationItemProvider extends Disposable implements ICusto
167169
if (toPromptsType(customization.contents) !== type) {
168170
continue;
169171
}
172+
const source = workingDirectory && customization.uri.startsWith(workingDirectory + '/') ? AICustomizationSources.local : AICustomizationSources.user;
170173
folders.push({
171174
uri: this.toRemoteUri(customization.uri),
172175
label: customization.name,
176+
source,
173177
});
174178
}
175179
return folders;

src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationListWidget.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -616,8 +616,8 @@ export class AICustomizationListWidget extends Disposable {
616616
private readonly _onDidRequestCreate = this._register(new Emitter<PromptsType>());
617617
readonly onDidRequestCreate: Event<PromptsType> = this._onDidRequestCreate.event;
618618

619-
private readonly _onDidRequestCreateManual = this._register(new Emitter<{ type: PromptsType; target: 'workspace' | 'user' | 'workspace-root'; rootFileName?: string }>());
620-
readonly onDidRequestCreateManual: Event<{ type: PromptsType; target: 'workspace' | 'user' | 'workspace-root'; rootFileName?: string }> = this._onDidRequestCreateManual.event;
619+
private readonly _onDidRequestCreateManual = this._register(new Emitter<{ type: PromptsType; target: 'local' | 'user' | 'workspace-root'; rootFileName?: string }>());
620+
readonly onDidRequestCreateManual: Event<{ type: PromptsType; target: 'local' | 'user' | 'workspace-root'; rootFileName?: string }> = this._onDidRequestCreateManual.event;
621621

622622
constructor(
623623
@IInstantiationService private readonly instantiationService: IInstantiationService,
@@ -1098,7 +1098,7 @@ export class AICustomizationListWidget extends Disposable {
10981098
actions.push({
10991099
label: `$(${Codicon.add.id}) ${localize('configureHooks', "Configure Hooks")}`,
11001100
enabled: true,
1101-
run: () => { this._onDidRequestCreateManual.fire({ type: promptType, target: 'workspace' }); },
1101+
run: () => { this._onDidRequestCreateManual.fire({ type: promptType, target: 'local' }); },
11021102
});
11031103
}
11041104
} else if (!override?.commandId) {
@@ -1107,7 +1107,7 @@ export class AICustomizationListWidget extends Disposable {
11071107
label: `$(${Codicon.add.id}) ${localize('configureHooks', "Configure Hooks")}`,
11081108
enabled: hasWorkspace,
11091109
tooltip: hasWorkspace ? undefined : localize('configureHooksDisabled', "Open a workspace folder to configure hooks."),
1110-
run: () => { this._onDidRequestCreateManual.fire({ type: promptType, target: 'workspace' }); },
1110+
run: () => { this._onDidRequestCreateManual.fire({ type: promptType, target: 'local' }); },
11111111
});
11121112
}
11131113
return actions;
@@ -1129,7 +1129,7 @@ export class AICustomizationListWidget extends Disposable {
11291129
actions.push({
11301130
label: `$(${Codicon.add.id}) New ${createTypeLabel} (Workspace)`,
11311131
enabled: true,
1132-
run: () => { this._onDidRequestCreateManual.fire({ type: promptType, target: 'workspace' }); },
1132+
run: () => { this._onDidRequestCreateManual.fire({ type: promptType, target: 'local' }); },
11331133
});
11341134
addedTargets.add('workspace');
11351135
} else {
@@ -1148,7 +1148,7 @@ export class AICustomizationListWidget extends Disposable {
11481148
actions.push({
11491149
label: `$(${Codicon.folder.id}) New ${createTypeLabel} (Workspace)`,
11501150
enabled: true,
1151-
run: () => { this._onDidRequestCreateManual.fire({ type: promptType, target: 'workspace' }); },
1151+
run: () => { this._onDidRequestCreateManual.fire({ type: promptType, target: 'local' }); },
11521152
});
11531153
}
11541154

src/vs/workbench/contrib/chat/browser/aiCustomization/aiCustomizationManagementEditor.ts

Lines changed: 22 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ import { IListVirtualDelegate, IListRenderer } from '../../../../../base/browser
3333
import { ThemeIcon } from '../../../../../base/common/themables.js';
3434
import { Codicon } from '../../../../../base/common/codicons.js';
3535
import { 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';
3737
import { URI } from '../../../../../base/common/uri.js';
3838
import { AICustomizationManagementEditorInput } from './aiCustomizationManagementEditorInput.js';
3939
import { AICustomizationListWidget } from './aiCustomizationListWidget.js';
@@ -67,7 +67,7 @@ import { AGENT_MD_FILENAME } from '../../common/promptSyntax/config/promptFileLo
6767
import { getAttributeDefinition, getTarget } from '../../common/promptSyntax/languageProviders/promptFileAttributes.js';
6868
import { INewPromptOptions, NEW_PROMPT_COMMAND_ID, NEW_INSTRUCTIONS_COMMAND_ID, NEW_AGENT_COMMAND_ID, NEW_SKILL_COMMAND_ID } from '../promptSyntax/newPromptFileActions.js';
6969
import { showConfigureHooksQuickPick } from '../promptSyntax/hookActions.js';
70-
import { resolveWorkspaceTargetDirectory, resolveUserTargetDirectory } from './customizationCreatorService.js';
70+
import { resolveWorkspaceTargetDirectory, resolveUserTargetDirectory, CustomizationLocationPicker } from './customizationCreatorService.js';
7171
import { ICommandService } from '../../../../../platform/commands/common/commands.js';
7272
import { AICustomizationSources, IAICustomizationWorkspaceService } from '../../common/aiCustomizationWorkspaceService.js';
7373
import { CodeEditorWidget } from '../../../../../editor/browser/widget/codeEditor/codeEditorWidget.js';
@@ -90,13 +90,12 @@ import { IExtension } from '../../../extensions/common/extensions.js';
9090
import { EmbeddedMcpServerDetail } from './embeddedMcpServerDetail.js';
9191
import { EmbeddedAgentPluginDetail } from './embeddedAgentPluginDetail.js';
9292
import { EmbeddedExtensionToolsDetail } from './embeddedExtensionToolsDetail.js';
93-
import { ICustomizationHarnessService, ICustomizationSourceFolder } from '../../common/customizationHarnessService.js';
93+
import { ICustomizationHarnessService } from '../../common/customizationHarnessService.js';
9494
import { ChatConfiguration } from '../../common/constants.js';
9595
import { AICustomizationWelcomePage } from './aiCustomizationWelcomePage.js';
9696
import { IViewsService } from '../../../../services/views/common/viewsService.js';
97-
import { ResourceSet } from '../../../../../base/common/map.js';
98-
import { PromptsServiceCustomizationItemProvider } from './promptsServiceCustomizationItemProvider.js';
9997
import { ILabelService } from '../../../../../platform/label/common/label.js';
98+
import { showNoFoldersDialog } from '../promptSyntax/pickers/askForPromptSourceFolder.js';
10099

101100
const $ = 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.

src/vs/workbench/contrib/chat/browser/aiCustomization/customizationCreatorService.ts

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,10 @@ import { PromptsType } from '../../common/promptSyntax/promptTypes.js';
1111
import { getPromptFileDefaultLocations } from '../../common/promptSyntax/config/promptFileLocations.js';
1212
import { IPromptsService, PromptsStorage } from '../../common/promptSyntax/service/promptsService.js';
1313
import { URI } from '../../../../../base/common/uri.js';
14-
import { isEqualOrParent } from '../../../../../base/common/resources.js';
15-
import { ResourceSet } from '../../../../../base/common/map.js';
1614
import { ICommandService } from '../../../../../platform/commands/common/commands.js';
1715
import { IQuickInputService, IQuickPickItem } from '../../../../../platform/quickinput/common/quickInput.js';
1816
import { localize } from '../../../../../nls.js';
19-
import { ICustomizationHarnessService, ICustomizationSourceFolder } from '../../common/customizationHarnessService.js';
17+
import { ICustomizationHarnessService } from '../../common/customizationHarnessService.js';
2018
import { CancellationToken } from '../../../../../base/common/cancellation.js';
2119
import { PromptsServiceCustomizationItemProvider } from './promptsServiceCustomizationItemProvider.js';
2220
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
@@ -132,7 +130,6 @@ export class CustomizationLocationPicker {
132130
@IQuickInputService private readonly quickInputService: IQuickInputService,
133131
@ICustomizationHarnessService private readonly harnessService: ICustomizationHarnessService,
134132
@IInstantiationService private readonly instantiationService: IInstantiationService,
135-
@IAICustomizationWorkspaceService private readonly workspaceService: IAICustomizationWorkspaceService,
136133
@ILabelService private readonly labelService: ILabelService
137134
) { }
138135

@@ -161,18 +158,7 @@ export class CustomizationLocationPicker {
161158
return undefined;
162159
}
163160

164-
const projectRoot = this.workspaceService.getActiveProjectRoot();
165-
const matchingFolders: ICustomizationSourceFolder[] = [];
166-
const seenFolders = new ResourceSet();
167-
for (const folder of allFolders) {
168-
const isProjectFolder = !!projectRoot && isEqualOrParent(folder.uri, projectRoot);
169-
if ((target === 'local' && isProjectFolder) || (target === 'user' && !isProjectFolder)) {
170-
if (!seenFolders.has(folder.uri)) {
171-
seenFolders.add(folder.uri);
172-
matchingFolders.push(folder);
173-
}
174-
}
175-
}
161+
const matchingFolders = allFolders.filter(f => f.source === target);
176162
if (matchingFolders.length === 0) {
177163
// No matching folders — return undefined so the command can fall
178164
// back to askForPromptSourceFolder (not null which means cancellation)

src/vs/workbench/contrib/chat/browser/aiCustomization/promptsServiceCustomizationItemProvider.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ export class PromptsServiceCustomizationItemProvider implements ICustomizationIt
6767
return folders.map(folder => ({
6868
uri: folder.uri,
6969
label: this.promptsService.getPromptLocationLabel(folder),
70+
source: folder.storage
7071
}));
7172
}
7273

src/vs/workbench/contrib/chat/common/customizationHarnessService.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,8 @@ export interface ICustomizationSourceFolder {
235235
readonly uri: URI;
236236
/** Display label for the picker when multiple folders are offered. */
237237
readonly label: string;
238+
/** Customization source for this folder (typically 'local' or 'user' for writable creation locations). */
239+
readonly source: AICustomizationSource;
238240
}
239241

240242
/**

0 commit comments

Comments
 (0)