mirror of
https://github.com/microsoft/vscode.git
synced 2026-09-29 18:03:25 +01:00
Add "Organization" label for prompt files (#289722)
This commit is contained in:
@@ -7,7 +7,7 @@ import { localize } from '../../../../../../nls.js';
|
||||
import { URI } from '../../../../../../base/common/uri.js';
|
||||
import { Codicon } from '../../../../../../base/common/codicons.js';
|
||||
import { ThemeIcon } from '../../../../../../base/common/themables.js';
|
||||
import { IPromptPath, IPromptsService, PromptsStorage } from '../../../common/promptSyntax/service/promptsService.js';
|
||||
import { IExtensionPromptPath, IPromptPath, IPromptsService, PromptsStorage } from '../../../common/promptSyntax/service/promptsService.js';
|
||||
import { dirname, extUri, joinPath } from '../../../../../../base/common/resources.js';
|
||||
import { DisposableStore } from '../../../../../../base/common/lifecycle.js';
|
||||
import { IFileService } from '../../../../../../platform/files/common/files.js';
|
||||
@@ -25,7 +25,9 @@ import { askForPromptSourceFolder } from './askForPromptSourceFolder.js';
|
||||
import { ILabelService } from '../../../../../../platform/label/common/label.js';
|
||||
import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js';
|
||||
import { PromptsConfig } from '../../../common/promptSyntax/config/config.js';
|
||||
import { IProductService } from '../../../../../../platform/product/common/productService.js';
|
||||
import { PromptFileRewriter } from '../promptFileRewriter.js';
|
||||
import { isOrganizationPromptFile } from '../../../common/promptSyntax/utils/promptsServiceUtils.js';
|
||||
|
||||
/**
|
||||
* Options for the {@link askToSelectInstructions} function.
|
||||
@@ -122,6 +124,13 @@ function isPromptFileItem(item: IPromptPickerQuickPickItem | IQuickPickSeparator
|
||||
return item.type === 'item' && !!item.promptFileUri;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type guard for extension prompt paths.
|
||||
*/
|
||||
function isExtensionPromptPath(prompt: IPromptPath): prompt is IExtensionPromptPath {
|
||||
return prompt.storage === PromptsStorage.extension && !!prompt.extension;
|
||||
}
|
||||
|
||||
type IPromptQuickPick = IQuickPick<IPromptPickerQuickPickItem, { useSeparators: true }>;
|
||||
|
||||
/**
|
||||
@@ -259,6 +268,7 @@ export class PromptFilePickers {
|
||||
@IPromptsService private readonly _promptsService: IPromptsService,
|
||||
@ILabelService private readonly _labelService: ILabelService,
|
||||
@IConfigurationService private readonly _configurationService: IConfigurationService,
|
||||
@IProductService private readonly _productService: IProductService,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -408,9 +418,8 @@ export class PromptFilePickers {
|
||||
result.push(...sortByLabel(await Promise.all(agentInstructionFiles.map(l => this._createPromptPickItem(l, agentButtons, getVisibility(l), token)))));
|
||||
}
|
||||
|
||||
const exts = await this._promptsService.listPromptFilesForStorage(options.type, PromptsStorage.extension, token);
|
||||
const exts = (await this._promptsService.listPromptFilesForStorage(options.type, PromptsStorage.extension, token)).filter(isExtensionPromptPath);
|
||||
if (exts.length) {
|
||||
result.push({ type: 'separator', label: localize('separator.extensions', "Extensions") });
|
||||
const extButtons: IQuickInputButton[] = [];
|
||||
if (options.optionEdit !== false) {
|
||||
extButtons.push(EDIT_BUTTON);
|
||||
@@ -418,7 +427,21 @@ export class PromptFilePickers {
|
||||
if (options.optionCopy !== false) {
|
||||
extButtons.push(COPY_BUTTON);
|
||||
}
|
||||
result.push(...sortByLabel(await Promise.all(exts.map(e => this._createPromptPickItem(e, extButtons, getVisibility(e), token)))));
|
||||
|
||||
const groupedExts = new Map<string, IPromptPath[]>();
|
||||
for (const ext of exts) {
|
||||
const groupLabel = this._getExtensionGroupLabel(ext);
|
||||
if (!groupedExts.has(groupLabel)) {
|
||||
groupedExts.set(groupLabel, []);
|
||||
}
|
||||
groupedExts.get(groupLabel)!.push(ext);
|
||||
}
|
||||
|
||||
const sortedGroupedExts = Array.from(groupedExts.entries()).sort((a, b) => a[0].localeCompare(b[0]));
|
||||
for (const [groupLabel, groupExts] of sortedGroupedExts) {
|
||||
result.push({ type: 'separator', label: groupLabel });
|
||||
result.push(...sortByLabel(await Promise.all(groupExts.map(e => this._createPromptPickItem(e, extButtons, getVisibility(e), token)))));
|
||||
}
|
||||
}
|
||||
const users = await this._promptsService.listPromptFilesForStorage(options.type, PromptsStorage.user, token);
|
||||
if (users.length) {
|
||||
@@ -428,6 +451,16 @@ export class PromptFilePickers {
|
||||
return result;
|
||||
}
|
||||
|
||||
private _getExtensionGroupLabel(extPath: IExtensionPromptPath): string {
|
||||
if (isOrganizationPromptFile(extPath.uri, extPath.extension.identifier, this._productService)) {
|
||||
return localize('separator.organization', "Organization");
|
||||
}
|
||||
|
||||
// By default, extension prompt files are grouped under "Extensions"
|
||||
return localize('separator.extensions', "Extensions");
|
||||
|
||||
}
|
||||
|
||||
private _getNewItems(type: PromptsType): IPromptPickerQuickPickItem[] {
|
||||
switch (type) {
|
||||
case PromptsType.prompt:
|
||||
|
||||
@@ -25,8 +25,9 @@ import { IKeybindingService } from '../../../../../../platform/keybinding/common
|
||||
import { IProductService } from '../../../../../../platform/product/common/productService.js';
|
||||
import { IChatAgentService } from '../../../common/participants/chatAgents.js';
|
||||
import { ChatMode, IChatMode, IChatModeService } from '../../../common/chatModes.js';
|
||||
import { isOrganizationPromptFile } from '../../../common/promptSyntax/utils/promptsServiceUtils.js';
|
||||
import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../../common/constants.js';
|
||||
import { ExtensionAgentSourceType, PromptsStorage } from '../../../common/promptSyntax/service/promptsService.js';
|
||||
import { PromptsStorage } from '../../../common/promptSyntax/service/promptsService.js';
|
||||
import { getOpenChatActionIdForMode } from '../../actions/chatActions.js';
|
||||
import { IToggleChatModeArgs, ToggleAgentModeActionId } from '../../actions/chatExecuteActions.js';
|
||||
import { ChatInputPickerActionViewItem, IChatInputPickerOptions } from './chatInputPickerActionItem.js';
|
||||
@@ -256,5 +257,19 @@ function isModeConsideredBuiltIn(mode: IChatMode, productService: IProductServic
|
||||
if (mode.isBuiltin) {
|
||||
return true;
|
||||
}
|
||||
return mode.source?.storage === PromptsStorage.extension && mode.source.extensionId.value === productService.defaultChatAgent?.chatExtensionId && mode.source.type === ExtensionAgentSourceType.contribution;
|
||||
// Not built-in if not from the built-in chat extension
|
||||
if (mode.source?.storage !== PromptsStorage.extension) {
|
||||
return false;
|
||||
}
|
||||
const chatExtensionId = productService.defaultChatAgent?.chatExtensionId;
|
||||
if (!chatExtensionId || mode.source.extensionId.value !== chatExtensionId) {
|
||||
return false;
|
||||
}
|
||||
// Organization-provided agents (under /github/ path) are also not considered built-in
|
||||
const modeUri = mode.uri?.get();
|
||||
if (!modeUri) {
|
||||
// If somehow there is no URI, but it's from the built-in chat extension, consider it built-in
|
||||
return true;
|
||||
}
|
||||
return !isOrganizationPromptFile(modeUri, mode.source.extensionId, productService);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { URI } from '../../../../../../base/common/uri.js';
|
||||
import { ExtensionIdentifier } from '../../../../../../platform/extensions/common/extensions.js';
|
||||
import { IProductService } from '../../../../../../platform/product/common/productService.js';
|
||||
|
||||
/**
|
||||
* Checks if a prompt file is organization-provided.
|
||||
* Organization-provided prompt files come from the built-in chat extension
|
||||
* and are located under a `/github/` path.
|
||||
*
|
||||
* @param uri The URI of the prompt file
|
||||
* @param extensionId The extension identifier that provides the prompt file
|
||||
* @param productService The product service to get the built-in chat extension ID
|
||||
* @returns `true` if the prompt file is organization-provided, `false` otherwise
|
||||
*/
|
||||
export function isOrganizationPromptFile(uri: URI, extensionId: ExtensionIdentifier, productService: IProductService): boolean {
|
||||
const chatExtensionId = productService.defaultChatAgent?.chatExtensionId;
|
||||
if (!chatExtensionId) {
|
||||
return false;
|
||||
}
|
||||
const isFromBuiltinChatExtension = ExtensionIdentifier.equals(extensionId, chatExtensionId);
|
||||
const pathContainsGithub = uri.path.includes('/github/');
|
||||
return isFromBuiltinChatExtension && pathContainsGithub;
|
||||
}
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import assert from 'assert';
|
||||
import { URI } from '../../../../../../../base/common/uri.js';
|
||||
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../../base/test/common/utils.js';
|
||||
import { ExtensionIdentifier } from '../../../../../../../platform/extensions/common/extensions.js';
|
||||
import { IProductService } from '../../../../../../../platform/product/common/productService.js';
|
||||
import { isOrganizationPromptFile } from '../../../../common/promptSyntax/utils/promptsServiceUtils.js';
|
||||
import { mockService } from './mock.js';
|
||||
|
||||
suite('promptsServiceUtils', () => {
|
||||
ensureNoDisposablesAreLeakedInTestSuite();
|
||||
|
||||
suite('isOrganizationPromptFile', () => {
|
||||
const CHAT_EXTENSION_ID = 'github.copilot-chat';
|
||||
|
||||
function createProductService(chatExtensionId: string | undefined): IProductService {
|
||||
return mockService<IProductService>({
|
||||
defaultChatAgent: chatExtensionId ? { chatExtensionId } : undefined,
|
||||
} as Partial<IProductService>);
|
||||
}
|
||||
|
||||
test('returns false when no chatExtensionId is configured', () => {
|
||||
const uri = URI.file('/some/path/github/prompt.md');
|
||||
const extensionId = new ExtensionIdentifier(CHAT_EXTENSION_ID);
|
||||
const productService = createProductService(undefined);
|
||||
|
||||
assert.strictEqual(
|
||||
isOrganizationPromptFile(uri, extensionId, productService),
|
||||
false,
|
||||
'Should return false when chatExtensionId is not configured',
|
||||
);
|
||||
});
|
||||
|
||||
test('returns false when extension ID does not match', () => {
|
||||
const uri = URI.file('/some/path/github/prompt.md');
|
||||
const extensionId = new ExtensionIdentifier('some.other-extension');
|
||||
const productService = createProductService(CHAT_EXTENSION_ID);
|
||||
|
||||
assert.strictEqual(
|
||||
isOrganizationPromptFile(uri, extensionId, productService),
|
||||
false,
|
||||
'Should return false when extension ID does not match the built-in chat extension',
|
||||
);
|
||||
});
|
||||
|
||||
test('returns false when path does not contain /github/', () => {
|
||||
const uri = URI.file('/some/path/to/prompt.md');
|
||||
const extensionId = new ExtensionIdentifier(CHAT_EXTENSION_ID);
|
||||
const productService = createProductService(CHAT_EXTENSION_ID);
|
||||
|
||||
assert.strictEqual(
|
||||
isOrganizationPromptFile(uri, extensionId, productService),
|
||||
false,
|
||||
'Should return false when path does not contain /github/',
|
||||
);
|
||||
});
|
||||
|
||||
test('returns true when extension matches and path contains /github/', () => {
|
||||
const uri = URI.file('/some/path/github/prompts/prompt.md');
|
||||
const extensionId = new ExtensionIdentifier(CHAT_EXTENSION_ID);
|
||||
const productService = createProductService(CHAT_EXTENSION_ID);
|
||||
|
||||
assert.strictEqual(
|
||||
isOrganizationPromptFile(uri, extensionId, productService),
|
||||
true,
|
||||
'Should return true when extension matches and path contains /github/',
|
||||
);
|
||||
});
|
||||
|
||||
test('extension ID comparison is case-insensitive', () => {
|
||||
const uri = URI.file('/some/github/prompt.md');
|
||||
const extensionId = new ExtensionIdentifier('GITHUB.COPILOT-CHAT');
|
||||
const productService = createProductService('github.copilot-chat');
|
||||
|
||||
assert.strictEqual(
|
||||
isOrganizationPromptFile(uri, extensionId, productService),
|
||||
true,
|
||||
'Extension ID comparison should be case-insensitive',
|
||||
);
|
||||
});
|
||||
|
||||
test('returns false when defaultChatAgent exists but chatExtensionId is empty', () => {
|
||||
const uri = URI.file('/some/github/prompt.md');
|
||||
const extensionId = new ExtensionIdentifier(CHAT_EXTENSION_ID);
|
||||
const productService = mockService<IProductService>({
|
||||
defaultChatAgent: { chatExtensionId: '' },
|
||||
} as Partial<IProductService>);
|
||||
|
||||
assert.strictEqual(
|
||||
isOrganizationPromptFile(uri, extensionId, productService),
|
||||
false,
|
||||
'Should return false when chatExtensionId is empty string',
|
||||
);
|
||||
});
|
||||
|
||||
test('returns false for similar but incorrect paths', () => {
|
||||
const extensionId = new ExtensionIdentifier(CHAT_EXTENSION_ID);
|
||||
const productService = createProductService(CHAT_EXTENSION_ID);
|
||||
|
||||
const invalidPaths = [
|
||||
'/some/githubs/prompt.md', // extra 's'
|
||||
'/some/github-org/prompt.md', // hyphenated
|
||||
'/some/mygithub/prompt.md', // prefix
|
||||
'/some/githubstuff/prompt.md', // suffix
|
||||
'/some/GITHUB/prompt.md', // uppercase (path matching is case-sensitive)
|
||||
'/some/Github/prompt.md', // mixed case
|
||||
];
|
||||
|
||||
for (const path of invalidPaths) {
|
||||
const uri = URI.file(path);
|
||||
assert.strictEqual(
|
||||
isOrganizationPromptFile(uri, extensionId, productService),
|
||||
false,
|
||||
`Should return false for path: ${path}`,
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user