introduce search extensions tool (#246721)

* introduce search extensions tool

* update tool reference name
This commit is contained in:
Sandeep Somavarapu
2025-04-16 16:21:04 +02:00
committed by GitHub
parent c7f69b4b93
commit cebb5e5a05
7 changed files with 185 additions and 7 deletions
@@ -19,6 +19,7 @@ import { Dto } from '../../services/extensions/common/proxyIdentifier.js';
import { ExtHostLanguageModelToolsShape, IMainContext, IToolDataDto, MainContext, MainThreadLanguageModelToolsShape } from './extHost.protocol.js';
import { ExtHostLanguageModels } from './extHostLanguageModels.js';
import * as typeConvert from './extHostTypeConverters.js';
import { SearchExtensionsToolId } from '../../contrib/extensions/common/searchExtensionsTool.js';
export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape {
/** A map of tools that were registered in this EH */
@@ -97,6 +98,7 @@ export class ExtHostLanguageModelTools implements ExtHostLanguageModelToolsShape
case InternalEditToolId:
case ExtensionEditToolId:
case InternalFetchWebPageToolId:
case SearchExtensionsToolId:
return isProposedApiEnabled(extension, 'chatParticipantPrivate');
default:
return true;
@@ -15,6 +15,9 @@ import { IChatRendererContent } from '../../common/chatViewModel.js';
import { ChatTreeItem, ChatViewId, IChatCodeBlockInfo } from '../chat.js';
import { IChatContentPart } from './chatContentParts.js';
import { PagedModel } from '../../../../../base/common/paging.js';
import { Codicon } from '../../../../../base/common/codicons.js';
import { ThemeIcon } from '../../../../../base/common/themables.js';
import { localize } from '../../../../../nls.js';
export class ChatExtensionsContentPart extends Disposable implements IChatContentPart {
public readonly domNode: HTMLElement;
@@ -38,10 +41,13 @@ export class ChatExtensionsContentPart extends Disposable implements IChatConten
super();
this.domNode = dom.$('.chat-extensions-content-part');
const loadingElement = dom.append(this.domNode, dom.$('.loading-extensions-element'));
dom.append(loadingElement, dom.$(ThemeIcon.asCSSSelector(ThemeIcon.modify(Codicon.loading, 'spin'))), dom.$('span.loading-message', undefined, localize('chat.extensions.loading', 'Loading extensions...')));
const extensionsList = dom.append(this.domNode, dom.$('.extensions-list'));
const list = this._register(instantiationService.createInstance(ExtensionsList, extensionsList, ChatViewId, { alwaysConsumeMouseWheel: false }, { onFocus: Event.None, onBlur: Event.None, filters: {} }));
getExtensions(extensionsContent.extensions, extensionsWorkbenchService).then(extensions => {
loadingElement.remove();
if (this._store.isDisposed) {
return;
}
@@ -45,6 +45,7 @@ import { CodeBlockPart, ICodeBlockData, ICodeBlockRenderOptions, localFileLangua
import '../media/chatCodeBlockPill.css';
import { IDisposableReference, ResourcePool } from './chatCollections.js';
import { IChatContentPart, IChatContentPartRenderContext } from './chatContentParts.js';
import { ChatExtensionsContentPart } from './chatExtensionsContentPart.js';
const $ = dom.$;
@@ -105,6 +106,11 @@ export class ChatMarkdownContentPart extends Disposable implements IChatContentP
hideEmptyCodeblock.style.display = 'none';
return hideEmptyCodeblock;
}
if (languageId === 'vscode-extensions') {
const chatExtensions = this._register(instantiationService.createInstance(ChatExtensionsContentPart, { kind: 'extensions', extensions: text.split(',') }));
this._register(chatExtensions.onDidChangeHeight(() => this._onDidChangeHeight.fire()));
return chatExtensions.domNode;
}
const globalIndex = globalCodeBlockIndexStart++;
const thisPartIndex = thisPartCodeBlockIndexStart++;
let textModel: Promise<ITextModel>;
@@ -12,3 +12,16 @@
.chat-extensions-content-part .extension-list-item {
border-bottom: 1px solid var(--vscode-chat-requestBorder);
}
.chat-extensions-content-part .loading-extensions-element {
line-height: 18px;
padding: 4px;
font-size: 12px;
color: var(--vscode-descriptionForeground);
user-select: none;
border-bottom: 1px solid var(--vscode-chat-requestBorder);
}
.chat-extensions-content-part .loading-extensions-element .loading-message {
padding-left: 4px;
}
@@ -313,13 +313,13 @@
font-weight: unset;
}
.interactive-item-container .value .rendered-markdown {
/* Codicons next to text need to be aligned with the text */
.codicon {
position: relative;
top: 2px;
}
/* Codicons next to text need to be aligned with the text */
.interactive-item-container .value .rendered-markdown:not(:has(.chat-extensions-content-part)) .codicon {
position: relative;
top: 2px;
}
.interactive-item-container .value .rendered-markdown {
.chat-codeblock-pill-widget .codicon {
top: -1px;
}
@@ -11,7 +11,7 @@ import { InstantiationType, registerSingleton } from '../../../../platform/insta
import { ExtensionsLocalizedLabel, IExtensionManagementService, IExtensionGalleryService, PreferencesLocalizedLabel, EXTENSION_INSTALL_SOURCE_CONTEXT, ExtensionInstallSource, UseUnpkgResourceApiConfigKey, SortBy, FilterType, VerifyExtensionSignatureConfigKey } from '../../../../platform/extensionManagement/common/extensionManagement.js';
import { EnablementState, IExtensionManagementServerService, IPublisherInfo, IWorkbenchExtensionEnablementService, IWorkbenchExtensionManagementService } from '../../../services/extensionManagement/common/extensionManagement.js';
import { IExtensionIgnoredRecommendationsService, IExtensionRecommendationsService } from '../../../services/extensionRecommendations/common/extensionRecommendations.js';
import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from '../../../common/contributions.js';
import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js';
import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js';
import { VIEWLET_ID, IExtensionsWorkbenchService, IExtensionsViewPaneContainer, TOGGLE_IGNORE_EXTENSION_ACTION_ID, INSTALL_EXTENSION_FROM_VSIX_COMMAND_ID, WORKSPACE_RECOMMENDATIONS_VIEW_ID, IWorkspaceRecommendedExtensionsView, AutoUpdateConfigurationKey, HasOutdatedExtensionsContext, SELECT_INSTALL_VSIX_EXTENSION_COMMAND_ID, LIST_WORKSPACE_UNSUPPORTED_EXTENSIONS_COMMAND_ID, ExtensionEditorTab, THEME_ACTIONS_GROUP, INSTALL_ACTIONS_GROUP, OUTDATED_EXTENSIONS_VIEW_ID, CONTEXT_HAS_GALLERY, extensionsSearchActionsMenu, UPDATE_ACTIONS_GROUP, IExtensionArg, ExtensionRuntimeActionType, EXTENSIONS_CATEGORY, AutoRestartConfigurationKey } from '../common/extensions.js';
import { InstallSpecificVersionOfExtensionAction, ConfigureWorkspaceRecommendedExtensionsAction, ConfigureWorkspaceFolderRecommendedExtensionsAction, SetColorThemeAction, SetFileIconThemeAction, SetProductIconThemeAction, ClearLanguageAction, ToggleAutoUpdateForExtensionAction, ToggleAutoUpdatesForPublisherAction, TogglePreReleaseExtensionAction, InstallAnotherVersionAction, InstallAction } from './extensionsActions.js';
@@ -81,6 +81,8 @@ import { IProductService } from '../../../../platform/product/common/productServ
import { IUserDataProfilesService } from '../../../../platform/userDataProfile/common/userDataProfile.js';
import product from '../../../../platform/product/common/product.js';
import { ExtensionGalleryResourceType, ExtensionGalleryServiceUrlConfigKey, getExtensionGalleryManifestResourceUri, IExtensionGalleryManifest, IExtensionGalleryManifestService } from '../../../../platform/extensionManagement/common/extensionGalleryManifest.js';
import { ILanguageModelToolsService } from '../../chat/common/languageModelToolsService.js';
import { SearchExtensionsTool, SearchExtensionsToolData } from '../common/searchExtensionsTool.js';
// Singletons
registerSingleton(IExtensionsWorkbenchService, ExtensionsWorkbenchService, InstantiationType.Eager /* Auto updates extensions */);
@@ -1963,6 +1965,21 @@ class TrustedPublishersInitializer implements IWorkbenchContribution {
}
}
class ExtensionToolsContribution extends Disposable implements IWorkbenchContribution {
static readonly ID = 'extensions.chat.toolsContribution';
constructor(
@ILanguageModelToolsService toolsService: ILanguageModelToolsService,
@IInstantiationService instantiationService: IInstantiationService,
) {
super();
const searchExtensionsTool = instantiationService.createInstance(SearchExtensionsTool);
this._register(toolsService.registerToolData(SearchExtensionsToolData));
this._register(toolsService.registerToolImplementation(SearchExtensionsToolData.id, searchExtensionsTool));
}
}
const workbenchRegistry = Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench);
workbenchRegistry.registerWorkbenchContribution(ExtensionsContributions, LifecyclePhase.Restored);
workbenchRegistry.registerWorkbenchContribution(StatusUpdater, LifecyclePhase.Eventually);
@@ -1979,6 +1996,8 @@ if (isWeb) {
workbenchRegistry.registerWorkbenchContribution(ExtensionStorageCleaner, LifecyclePhase.Eventually);
}
registerWorkbenchContribution2(ExtensionToolsContribution.ID, ExtensionToolsContribution, WorkbenchPhase.AfterRestored);
// Running Extensions
registerAction2(ShowRuntimeExtensionsAction);
@@ -0,0 +1,132 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { CancellationToken } from '../../../../base/common/cancellation.js';
import { Codicon } from '../../../../base/common/codicons.js';
import { ThemeIcon } from '../../../../base/common/themables.js';
import { localize } from '../../../../nls.js';
import { SortBy } from '../../../../platform/extensionManagement/common/extensionManagement.js';
import { EXTENSION_CATEGORIES } from '../../../../platform/extensions/common/extensions.js';
import { CountTokensCallback, IToolData, IToolImpl, IToolInvocation, IToolResult } from '../../chat/common/languageModelToolsService.js';
import { ExtensionState, IExtensionsWorkbenchService } from '../common/extensions.js';
export const SearchExtensionsToolId = 'vscode_searchExtensions_internal';
export const SearchExtensionsToolData: IToolData = {
id: SearchExtensionsToolId,
toolReferenceName: 'extensions',
canBeReferencedInPrompt: true,
icon: ThemeIcon.fromId(Codicon.extensions.id),
supportsToolPicker: true,
displayName: localize('searchExtensionsTool.displayName', 'Search Extensions'),
modelDescription: localize('searchExtensionsTool.modelDescription', "This tool helps the model search for VS Code extensions from the Marketplace. The model should specify the category of extensions and keywords to search for. Note that the results may include false positives, so further filtering by reviewing the results is recommended."),
source: { type: 'internal' },
inputSchema: {
type: 'object',
properties: {
category: {
type: 'string',
description: 'The category of extensions to search for',
enum: EXTENSION_CATEGORIES,
},
keywords: {
type: 'array',
items: {
type: 'string',
},
description: 'The keywords to search for',
},
},
}
};
type InputParams = {
category?: string;
keywords?: string;
};
type ExtensionData = {
id: string;
name: string;
description: string;
installed: boolean;
installCount: number;
rating: number;
categories: readonly string[];
tags: readonly string[];
};
export class SearchExtensionsTool implements IToolImpl {
constructor(
@IExtensionsWorkbenchService private readonly extensionWorkbenchService: IExtensionsWorkbenchService,
) { }
async invoke(invocation: IToolInvocation, _countTokens: CountTokensCallback, token: CancellationToken): Promise<IToolResult> {
const params = invocation.parameters as InputParams;
if (!params.keywords?.length && !params.category) {
return {
content: [{
kind: 'text',
value: localize('searchExtensionsTool.noInput', 'Please provide a category or keyword to search for.')
}]
};
}
const extensionsMap = new Map<string, ExtensionData>();
const queryAndAddExtensions = async (text: string) => {
const extensions = await this.extensionWorkbenchService.queryGallery({
text,
pageSize: 10,
sortBy: SortBy.InstallCount
}, token);
if (extensions.firstPage.length) {
for (const extension of extensions.firstPage) {
if (extension.deprecationInfo || extension.isMalicious) {
continue;
}
extensionsMap.set(extension.identifier.id.toLowerCase(), {
id: extension.identifier.id,
name: extension.displayName,
description: extension.description,
installed: extension.state === ExtensionState.Installed,
installCount: extension.installCount ?? 0,
rating: extension.rating ?? 0,
categories: extension.categories ?? [],
tags: extension.gallery?.tags ?? []
});
}
}
};
if (params.keywords?.length) {
for (const keyword of params.keywords ?? []) {
if (keyword === 'featured') {
await queryAndAddExtensions('featured');
} else {
let text = params.category ? `category:"${params.category}"` : '';
text = keyword ? `${text} ${keyword}`.trim() : text;
await queryAndAddExtensions(text);
}
}
} else {
await queryAndAddExtensions(`category:"${params.category}"`);
}
const result = Array.from(extensionsMap.values());
return {
content: [{
kind: 'text',
value: `Here are the list of extensions:\n${JSON.stringify(result)}\n. Use the following format to display extensions to the user because there is a renderer available to parse these extensions in this format and display them with all details. So, do not describe about the extensions to the user.\n\`\`\`vscode-extensions\nextensionId1,extensionId2\n\`\`\`\n.`
}],
toolResultDetails: {
input: JSON.stringify(params),
output: JSON.stringify(result.map(extension => extension.id))
}
};
}
}