Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,7 @@ export class AICustomizationItemNormalizer {
uri: item.uri,
name: item.name,
filename: item.uri.scheme === Schemas.file
? this.labelService.getUriLabel(item.uri, { relative: isWorkspaceItem })
? this.labelService.getUriLabel(item.uri, { relative: isWorkspaceItem, noPrefix: isWorkspaceItem })
: basename(item.uri),
Comment on lines 211 to 213
description: item.description,
source,
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { PromptsType } from '../../common/promptSyntax/promptTypes.js';

/**
* Truncates a description string to the first line.
* The UI applies CSS text-overflow ellipsis for width overflow.
Expand All @@ -20,12 +18,8 @@ export function truncateToFirstLine(text: string): string {
/**
* Returns the secondary text shown for a customization item.
*/
export function getCustomizationSecondaryText(description: string | undefined, filename: string, promptType: PromptsType): string {
if (!description) {
return filename;
}

return promptType === PromptsType.hook ? description : truncateToFirstLine(description);
export function getCustomizationSecondaryText(filename: string): string {
return filename;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1206,8 +1206,8 @@ export class AICustomizationManagementEditor extends EditorPane {
}));

// Handle manual create actions - open editor directly
this.editorDisposables.add(this.listWidget.onDidRequestCreateManual(({ type, target, rootFileName }) => {
this.createNewItemManual(type, target, rootFileName);
this.editorDisposables.add(this.listWidget.onDidRequestCreateManual(({ type, target, rootFileName, workspaceFolder }) => {
this.createNewItemManual(type, target, rootFileName, workspaceFolder);
}));

// Container for Models content (only in sessions)
Expand Down Expand Up @@ -2819,7 +2819,7 @@ export class AICustomizationManagementEditor extends EditorPane {
/**
* Creates a new prompt file and opens it in the embedded editor.
*/
private async createNewItemManual(type: PromptsType, target: 'local' | 'user' | 'workspace-root', rootFileName?: string): Promise<void> {
private async createNewItemManual(type: PromptsType, target: 'local' | 'user' | 'workspace-root', rootFileName?: string, workspaceFolder?: URI): Promise<void> {
this.telemetryService.publicLog2<CustomizationEditorCreateItemEvent, CustomizationEditorCreateItemClassification>('chatCustomizationEditor.createItem', {
section: this.selectedSection ?? 'welcome',
promptType: type,
Expand All @@ -2831,7 +2831,7 @@ export class AICustomizationManagementEditor extends EditorPane {
// rootFileName is passed from rootFileShortcuts; falls back to
// the section override's rootFile, then AGENTS.md as the default.
if (target === 'workspace-root') {
const projectRoot = this.workspaceService.getActiveProjectRoot();
const projectRoot = workspaceFolder ?? this.workspaceService.getActiveProjectRoot();
if (!projectRoot) {
return;
}
Expand Down Expand Up @@ -2860,6 +2860,7 @@ export class AICustomizationManagementEditor extends EditorPane {
},
target: Target.GitHubCopilot,
preferredStorage,
workspaceFolder,
});
} else {
// Core: use the default core behaviour
Expand All @@ -2869,6 +2870,7 @@ export class AICustomizationManagementEditor extends EditorPane {
return;
},
preferredStorage,
workspaceFolder,
});
}
return;
Expand All @@ -2879,6 +2881,7 @@ export class AICustomizationManagementEditor extends EditorPane {
sessionResource,
type,
target,
workspaceFolder,
);
if (targetDir === null) {
return; // User cancelled the picker
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ import { URI } from '../../../../../base/common/uri.js';
import { ICommandService } from '../../../../../platform/commands/common/commands.js';
import { IQuickInputService, IQuickPickItem } from '../../../../../platform/quickinput/common/quickInput.js';
import { localize } from '../../../../../nls.js';
import { ICustomizationHarnessService } from '../../common/customizationHarnessService.js';
import { ICustomizationHarnessService, ICustomizationSourceFolder } from '../../common/customizationHarnessService.js';
import { CancellationToken } from '../../../../../base/common/cancellation.js';
import { PromptsServiceCustomizationItemProvider } from './promptsServiceCustomizationItemProvider.js';
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
import { getChatSessionType } from '../../common/model/chatUri.js';
import { ILabelService } from '../../../../../platform/label/common/label.js';
import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js';
import { isEqual } from '../../../../../base/common/resources.js';

/**
* Service that opens an AI-guided chat session to help the user create
Expand Down Expand Up @@ -130,7 +132,8 @@ export class CustomizationLocationPicker {
@IQuickInputService private readonly quickInputService: IQuickInputService,
@ICustomizationHarnessService private readonly harnessService: ICustomizationHarnessService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@ILabelService private readonly labelService: ILabelService
@ILabelService private readonly labelService: ILabelService,
@IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService,
) { }

/**
Expand All @@ -145,7 +148,7 @@ export class CustomizationLocationPicker {
* @returns the resolved URI, `undefined` when no folder is available,
* or `null` when the user cancelled the picker.
*/
public async resolveTargetDirectoryWithPicker(sessionResource: URI, type: PromptsType, target: 'local' | 'user'): Promise<URI | undefined | null> {
public async resolveTargetDirectoryWithPicker(sessionResource: URI, type: PromptsType, target: 'local' | 'user', workspaceFolder?: URI): Promise<URI | undefined | null> {
const sessionType = getChatSessionType(sessionResource);
const descriptor = this.harnessService.findHarnessById(sessionType);
const provider = descriptor?.itemProvider ?? this.instantiationService.createInstance(PromptsServiceCustomizationItemProvider);
Expand All @@ -158,7 +161,7 @@ export class CustomizationLocationPicker {
return undefined;
}

const matchingFolders = allFolders.filter(f => f.source === target);
const matchingFolders = filterCustomizationSourceFolders(allFolders, target, workspaceFolder, this.workspaceContextService);
if (matchingFolders.length === 0) {
// No matching folders — return undefined so the command can fall
// back to askForPromptSourceFolder (not null which means cancellation)
Expand All @@ -170,20 +173,58 @@ export class CustomizationLocationPicker {
}

// Multiple directories — ask the user which one to use
const items: (IQuickPickItem & { uri: URI })[] = matchingFolders.map(folder => ({
label: folder.label,
description: this.labelService.getUriLabel(folder.uri, { relative: true }),
uri: folder.uri,
}));
const items = getCustomizationLocationPickItems(matchingFolders, this.labelService, this.workspaceContextService, workspaceFolder);

const picked = await this.quickInputService.pick(items, {
placeHolder: localize('selectTargetDirectory', "Select a directory for the new customization file"),
matchOnDescription: true,
});

return picked?.uri ?? null;
}
}

export function filterCustomizationSourceFolders(
folders: readonly ICustomizationSourceFolder[],
target: 'local' | 'user',
workspaceFolder: URI | undefined,
workspaceContextService: IWorkspaceContextService,
): readonly ICustomizationSourceFolder[] {
return folders.filter(folder => {
if (folder.source !== target) {
return false;
}
if (!workspaceFolder || target !== PromptsStorage.local) {
return true;
}
return isEqual(workspaceContextService.getWorkspaceFolder(folder.uri)?.uri, workspaceFolder);
});
}

export function getCustomizationLocationPickItems(
folders: readonly ICustomizationSourceFolder[],
labelService: ILabelService,
workspaceContextService: IWorkspaceContextService,
selectedWorkspaceFolder?: URI,
): (IQuickPickItem & { uri: URI })[] {
const isMultiRootWorkspace = workspaceContextService.getWorkspace().folders.length > 1;
return folders.map(folder => {
const workspaceFolder = folder.source === PromptsStorage.local ? workspaceContextService.getWorkspaceFolder(folder.uri) : undefined;
const relativePath = labelService.getUriLabel(folder.uri, { relative: true, noPrefix: !!workspaceFolder });
if (workspaceFolder && selectedWorkspaceFolder && isEqual(workspaceFolder.uri, selectedWorkspaceFolder)) {
return {
label: relativePath,
uri: folder.uri,
};
}
return {
label: isMultiRootWorkspace && workspaceFolder ? workspaceFolder.name : folder.label,
description: relativePath,
uri: folder.uri,
};
});
}

/**
* Resolves the workspace directory for a new customization file based on the active project root.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { McpCommandIds } from '../../../../contrib/mcp/common/mcpCommandIds.js';
import { autorun, derived, IObservable, observableSignalFromEvent } from '../../../../../base/common/observable.js';
import { IOpenerService } from '../../../../../platform/opener/common/opener.js';
import { URI } from '../../../../../base/common/uri.js';
import { isEqualOrParent } from '../../../../../base/common/resources.js';
import { InputBox, MessageType } from '../../../../../base/browser/ui/inputbox/inputBox.js';
import { IContextMenuService, IContextViewService } from '../../../../../platform/contextview/browser/contextView.js';
import { CancellationTokenSource } from '../../../../../base/common/cancellation.js';
Expand Down Expand Up @@ -60,6 +61,7 @@ import { createCustomizationCardPrimaryAction, CustomizationCardListController,
import { DomScrollableElement } from '../../../../../base/browser/ui/scrollbar/scrollableElement.js';
import { ScrollbarVisibility } from '../../../../../base/common/scrollable.js';
import { WorkbenchList } from '../../../../../platform/list/browser/listService.js';
import { ILabelService } from '../../../../../platform/label/common/label.js';

const $ = DOM.$;

Expand All @@ -76,6 +78,17 @@ function getPluginUriFromCollectionId(collectionId: string | undefined): string
return collectionId?.startsWith(PLUGIN_COLLECTION_PREFIX) ? collectionId.slice(PLUGIN_COLLECTION_PREFIX.length) : undefined;
}

export function getMcpServerSecondaryText(sourceUri: URI | undefined, pluginLabel: string | undefined, labelService: ILabelService): string | undefined {
if (pluginLabel) {
return localize('fromPlugin', "Plugin: {0}", pluginLabel);
}
return sourceUri ? labelService.getUriLabel(sourceUri, { relative: true, noPrefix: true }) : undefined;
}

export function getMcpServerHoverContent(description: string | undefined, secondaryText: string | undefined): string | undefined {
return description?.trim() || secondaryText;
}

/**
* Represents an individual MCP server item in the list.
*/
Expand Down Expand Up @@ -201,6 +214,7 @@ export class McpServerItemRenderer implements IListRenderer<IMcpServerItemEntry
@IAICustomizationWorkspaceService private readonly workspaceService: IAICustomizationWorkspaceService,
@IAgentPluginService private readonly agentPluginService: IAgentPluginService,
@IHoverService private readonly hoverService: IHoverService,
@ILabelService private readonly labelService: ILabelService,
@IAgentHostCustomizationService private readonly agentHostCustomizationService: IAgentHostCustomizationService,
@ICustomizationHarnessService private readonly customizationHarnessService: ICustomizationHarnessService,
@IOutputService private readonly outputService: IOutputService,
Expand Down Expand Up @@ -251,43 +265,29 @@ export class McpServerItemRenderer implements IListRenderer<IMcpServerItemEntry
}
// Always re-created: these capture `element`, which is a fresh object on every refresh.
templateData.elementDisposables.clear();
const secondaryText = this.getSecondaryText(element);
templateData.description.textContent = secondaryText ?? '';
templateData.description.style.display = secondaryText ? '' : 'none';
Comment on lines +268 to +270
const hoverContent = getMcpServerHoverContent(this.getDescription(element), secondaryText);
if (hoverContent) {
templateData.elementDisposables.add(this.hoverService.setupDelayedHover(templateData.container, () => ({
content: hoverContent,
appearance: { compact: true, skipFadeInAnimation: true },
})));
}

if (element.type === 'builtin-item') {
templateData.container.classList.add('builtin');
templateData.container.classList.toggle('has-detail', false);
templateData.name.textContent = formatDisplayName(element.label);
if (element.description) {
templateData.description.textContent = truncateToFirstLine(element.description);
templateData.description.style.display = '';
} else {
templateData.description.textContent = '';
templateData.description.style.display = 'none';
}
this.updateKnownServerStatus(templateData, element);

// Add hover with plugin provenance for plugin-sourced builtin items
const pluginUriStr = getPluginUriFromCollectionId(element.collectionId);
if (pluginUriStr) {
templateData.elementDisposables.add(this.hoverService.setupDelayedHover(templateData.container, () => {
const plugin = this.agentPluginService.plugins.get().find(p => p.uri.toString() === pluginUriStr);
if (plugin) {
return {
content: `${element.label}\n${localize('fromPlugin', "Plugin: {0}", plugin.label)}`,
appearance: { compact: true, skipFadeInAnimation: true },
};
}
return { content: element.label, appearance: { compact: true, skipFadeInAnimation: true } };
}));
}
return;
}

if (element.type === 'session-server-item') {
templateData.container.classList.remove('builtin');
templateData.container.classList.toggle('has-detail', false);
templateData.name.textContent = formatDisplayName(element.server.name);
templateData.description.textContent = '';
templateData.description.style.display = 'none';
this.updateActiveSessionStatus(templateData, element);
return;
}
Expand All @@ -301,14 +301,6 @@ export class McpServerItemRenderer implements IListRenderer<IMcpServerItemEntry
const isGallery = !element.server.local;
const hasDetail = !!description || isGallery;
templateData.container.classList.toggle('has-detail', hasDetail);
if (description) {
templateData.description.textContent = truncateToFirstLine(description);
templateData.description.style.display = '';
} else {
templateData.description.textContent = '';
templateData.description.style.display = 'none';
}

if (element.activeSessionServer !== undefined) {
this.updateKnownServerStatus(templateData, element);
} else if (this.workspaceService.isSessionsWindow) {
Expand All @@ -323,6 +315,29 @@ export class McpServerItemRenderer implements IListRenderer<IMcpServerItemEntry
}
}

private getDescription(element: IMcpServerItemEntry | IMcpSessionServerItemEntry | IMcpBuiltinItemEntry): string | undefined {
return element.type === 'server-item'
? element.server.description
: element.type === 'builtin-item'
? element.description
: undefined;
}

private getSecondaryText(element: IMcpServerItemEntry | IMcpSessionServerItemEntry | IMcpBuiltinItemEntry): string | undefined {
const activeSessionServer = getActiveSessionServer(element);
const localServer = element.type === 'session-server-item' ? undefined : element.localServer;
const pluginUriString = getPluginUriFromCollectionId(localServer?.collection.id);
const sourceUri = createInstalledMcpServerDetailInput(element).source?.uri;
const plugin = pluginUriString
? this.agentPluginService.plugins.get().find(candidate => candidate.uri.toString() === pluginUriString)
: activeSessionServer?.isPluginProvided && sourceUri
? this.agentPluginService.plugins.get().find(candidate => isEqualOrParent(sourceUri, candidate.uri))
: undefined;
const disabledReason = activeSessionServer?.disabledReason;
const pluginLabel = plugin?.label ?? (disabledReason?.source === 'plugin' ? disabledReason.plugin.name : undefined);
return getMcpServerSecondaryText(sourceUri, pluginLabel, this.labelService);
}

private updateKnownServerStatus(templateData: IMcpServerItemTemplateData, element: IMcpServerItemEntry | IMcpBuiltinItemEntry): void {
let localDisabled = false;
const update = () => {
Expand Down
Loading