diff --git a/src/vs/platform/extensions/common/extensionsApiProposals.ts b/src/vs/platform/extensions/common/extensionsApiProposals.ts index 5e5e982cdfa..c825f45db15 100644 --- a/src/vs/platform/extensions/common/extensionsApiProposals.ts +++ b/src/vs/platform/extensions/common/extensionsApiProposals.ts @@ -12,6 +12,9 @@ const _allApiProposals = { aiRelatedInformation: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.aiRelatedInformation.d.ts', }, + aiSettingsSearch: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.aiSettingsSearch.d.ts', + }, aiTextSearchProvider: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.aiTextSearchProvider.d.ts', version: 2 diff --git a/src/vs/workbench/api/browser/extensionHost.contribution.ts b/src/vs/workbench/api/browser/extensionHost.contribution.ts index 7a778aa030b..d5430634469 100644 --- a/src/vs/workbench/api/browser/extensionHost.contribution.ts +++ b/src/vs/workbench/api/browser/extensionHost.contribution.ts @@ -88,6 +88,7 @@ import './mainThreadShare.js'; import './mainThreadProfileContentHandlers.js'; import './mainThreadAiRelatedInformation.js'; import './mainThreadAiEmbeddingVector.js'; +import './mainThreadAiSettingsSearch.js'; import './mainThreadMcp.js'; import './mainThreadChatStatus.js'; diff --git a/src/vs/workbench/api/browser/mainThreadAiSettingsSearch.ts b/src/vs/workbench/api/browser/mainThreadAiSettingsSearch.ts new file mode 100644 index 00000000000..8f7dde42d91 --- /dev/null +++ b/src/vs/workbench/api/browser/mainThreadAiSettingsSearch.ts @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable, DisposableMap } from '../../../base/common/lifecycle.js'; +import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js'; +import { AiSettingsSearchResult, IAiSettingsSearchProvider, IAiSettingsSearchService } from '../../services/aiSettingsSearch/common/aiSettingsSearch.js'; +import { ExtHostContext, ExtHostAiSettingsSearchShape, MainContext, MainThreadAiSettingsSearchShape, } from '../common/extHost.protocol.js'; + +@extHostNamedCustomer(MainContext.MainThreadAiSettingsSearch) +export class MainThreadAiSettingsSearch extends Disposable implements MainThreadAiSettingsSearchShape { + private readonly _proxy: ExtHostAiSettingsSearchShape; + private readonly _registrations = this._register(new DisposableMap()); + + constructor( + context: IExtHostContext, + @IAiSettingsSearchService private readonly _settingsSearchService: IAiSettingsSearchService, + ) { + super(); + this._proxy = context.getProxy(ExtHostContext.ExtHostAiSettingsSearch); + } + + $registerAiSettingsSearchProvider(handle: number): void { + const provider: IAiSettingsSearchProvider = { + searchSettings: (query, option, token) => { + return this._proxy.$startSearch(handle, query, option, token); + } + }; + this._registrations.set(handle, this._settingsSearchService.registerSettingsSearchProvider(provider)); + } + + $unregisterAiSettingsSearchProvider(handle: number): void { + this._registrations.deleteAndDispose(handle); + } + + $handleSearchResult(handle: number, result: AiSettingsSearchResult): void { + if (!this._registrations.has(handle)) { + throw new Error(`No AI settings search provider found`); + } + + this._settingsSearchService.handleSearchResult(result); + } +} diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index b5699214188..522a47cfc8f 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -110,6 +110,7 @@ import { ExtHostWebviewPanels } from './extHostWebviewPanels.js'; import { ExtHostWebviewViews } from './extHostWebviewView.js'; import { IExtHostWindow } from './extHostWindow.js'; import { IExtHostWorkspace } from './extHostWorkspace.js'; +import { ExtHostAiSettingsSearch } from './extHostAiSettingsSearch.js'; export interface IExtensionRegistries { mine: ExtensionDescriptionRegistry; @@ -218,6 +219,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostChatAgents2 = rpcProtocol.set(ExtHostContext.ExtHostChatAgents2, new ExtHostChatAgents2(rpcProtocol, extHostLogService, extHostCommands, extHostDocuments, extHostLanguageModels, extHostDiagnostics, extHostLanguageModelTools)); const extHostAiRelatedInformation = rpcProtocol.set(ExtHostContext.ExtHostAiRelatedInformation, new ExtHostRelatedInformation(rpcProtocol)); const extHostAiEmbeddingVector = rpcProtocol.set(ExtHostContext.ExtHostAiEmbeddingVector, new ExtHostAiEmbeddingVector(rpcProtocol)); + const extHostAiSettingsSearch = rpcProtocol.set(ExtHostContext.ExtHostAiSettingsSearch, new ExtHostAiSettingsSearch(rpcProtocol)); const extHostStatusBar = rpcProtocol.set(ExtHostContext.ExtHostStatusBar, new ExtHostStatusBar(rpcProtocol, extHostCommands.converter)); const extHostSpeech = rpcProtocol.set(ExtHostContext.ExtHostSpeech, new ExtHostSpeech(rpcProtocol)); const extHostEmbeddings = rpcProtocol.set(ExtHostContext.ExtHostEmbeddings, new ExtHostEmbeddings(rpcProtocol)); @@ -1438,6 +1440,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I registerEmbeddingVectorProvider(model: string, provider: vscode.EmbeddingVectorProvider) { checkProposedApiEnabled(extension, 'aiRelatedInformation'); return extHostAiEmbeddingVector.registerEmbeddingVectorProvider(extension, model, provider); + }, + registerSettingsSearchProvider(provider: vscode.SettingsSearchProvider) { + checkProposedApiEnabled(extension, 'aiSettingsSearch'); + return extHostAiSettingsSearch.registerSettingsSearchProvider(extension, provider); } }; @@ -1835,6 +1841,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I ChatErrorLevel: extHostTypes.ChatErrorLevel, McpHttpServerDefinition: extHostTypes.McpHttpServerDefinition, McpStdioServerDefinition: extHostTypes.McpStdioServerDefinition, + SettingsSearchResultKind: extHostTypes.SettingsSearchResultKind }; }; } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index d72909f4cae..214bb774b90 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -86,6 +86,7 @@ import { CandidatePort } from '../../services/remote/common/tunnelModel.js'; import { IFileQueryBuilderOptions, ITextQueryBuilderOptions } from '../../services/search/common/queryBuilder.js'; import * as search from '../../services/search/common/search.js'; import { AISearchKeyword, TextSearchCompleteMessage } from '../../services/search/common/searchExtTypes.js'; +import { AiSettingsSearchProviderOptions, AiSettingsSearchResult } from '../../services/aiSettingsSearch/common/aiSettingsSearch.js'; import { ISaveProfileResult } from '../../services/userDataProfile/common/userDataProfile.js'; import { TerminalShellExecutionCommandLineConfidence } from './extHostTypes.js'; import * as tasks from './shared/tasks.js'; @@ -1986,6 +1987,16 @@ export interface MainThreadAiRelatedInformationShape { $unregisterAiRelatedInformationProvider(handle: number): void; } +export interface ExtHostAiSettingsSearchShape { + $startSearch(handle: number, query: string, option: AiSettingsSearchProviderOptions, token: CancellationToken): Promise; +} + +export interface MainThreadAiSettingsSearchShape { + $registerAiSettingsSearchProvider(handle: number): void; + $unregisterAiSettingsSearchProvider(handle: number): void; + $handleSearchResult(handle: number, result: AiSettingsSearchResult): void; +} + export interface ExtHostAiEmbeddingVectorShape { $provideAiEmbeddingVector(handle: number, strings: string[], token: CancellationToken): Promise; } @@ -3155,6 +3166,7 @@ export const MainContext = { MainThreadAiRelatedInformation: createProxyIdentifier('MainThreadAiRelatedInformation'), MainThreadAiEmbeddingVector: createProxyIdentifier('MainThreadAiEmbeddingVector'), MainThreadChatStatus: createProxyIdentifier('MainThreadChatStatus'), + MainThreadAiSettingsSearch: createProxyIdentifier('MainThreadAiSettingsSearch'), }; export const ExtHostContext = { @@ -3217,6 +3229,7 @@ export const ExtHostContext = { ExtHostEmbeddings: createProxyIdentifier('ExtHostEmbeddings'), ExtHostAiRelatedInformation: createProxyIdentifier('ExtHostAiRelatedInformation'), ExtHostAiEmbeddingVector: createProxyIdentifier('ExtHostAiEmbeddingVector'), + ExtHostAiSettingsSearch: createProxyIdentifier('ExtHostAiSettingsSearch'), ExtHostTheming: createProxyIdentifier('ExtHostTheming'), ExtHostTunnelService: createProxyIdentifier('ExtHostTunnelService'), ExtHostManagedSockets: createProxyIdentifier('ExtHostManagedSockets'), diff --git a/src/vs/workbench/api/common/extHostAiSettingsSearch.ts b/src/vs/workbench/api/common/extHostAiSettingsSearch.ts new file mode 100644 index 00000000000..c7c2d32f33d --- /dev/null +++ b/src/vs/workbench/api/common/extHostAiSettingsSearch.ts @@ -0,0 +1,52 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type { SettingsSearchProvider, SettingsSearchResult } from 'vscode'; +import { CancellationToken } from '../../../base/common/cancellation.js'; +import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; +import { AiSettingsSearchProviderOptions } from '../../services/aiSettingsSearch/common/aiSettingsSearch.js'; +import { ExtHostAiSettingsSearchShape, IMainContext, MainContext, MainThreadAiSettingsSearchShape } from './extHost.protocol.js'; +import { Disposable } from './extHostTypes.js'; +import { Progress } from '../../../platform/progress/common/progress.js'; +import { AiSettingsSearch } from './extHostTypeConverters.js'; + +export class ExtHostAiSettingsSearch implements ExtHostAiSettingsSearchShape { + private _settingsSearchProviders: Map = new Map(); + private _nextHandle = 0; + + private readonly _proxy: MainThreadAiSettingsSearchShape; + + constructor(mainContext: IMainContext) { + this._proxy = mainContext.getProxy(MainContext.MainThreadAiSettingsSearch); + } + + async $startSearch(handle: number, query: string, option: AiSettingsSearchProviderOptions, token: CancellationToken): Promise { + if (this._settingsSearchProviders.size === 0) { + throw new Error('No related information providers registered'); + } + + const provider = this._settingsSearchProviders.get(handle); + if (!provider) { + throw new Error('Settings search provider not found'); + } + + const progressReporter = new Progress((data) => { + this._proxy.$handleSearchResult(handle, AiSettingsSearch.fromSettingsSearchResult(data)); + }); + + return provider.provideSettingsSearchResults(query, option, progressReporter, token); + } + + registerSettingsSearchProvider(extension: IExtensionDescription, provider: SettingsSearchProvider): Disposable { + const handle = this._nextHandle; + this._nextHandle++; + this._settingsSearchProviders.set(handle, provider); + this._proxy.$registerAiSettingsSearchProvider(handle); + return new Disposable(() => { + this._proxy.$unregisterAiSettingsSearchProvider(handle); + this._settingsSearchProviders.delete(handle); + }); + } +} diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 3177861cb19..1bab9eeac00 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -63,6 +63,7 @@ import { getPrivateApiFor } from './extHostTestingPrivateApi.js'; import * as types from './extHostTypes.js'; import { LanguageModelPromptTsxPart, LanguageModelTextPart } from './extHostTypes.js'; import { ChatAgentLocation } from '../../contrib/chat/common/constants.js'; +import { AiSettingsSearchResult, AiSettingsSearchResultKind } from '../../services/aiSettingsSearch/common/aiSettingsSearch.js'; import { McpServerLaunch, McpServerTransportType } from '../../contrib/mcp/common/mcpTypes.js'; export namespace Command { @@ -3278,6 +3279,29 @@ export namespace IconPath { } } +export namespace AiSettingsSearch { + export function fromSettingsSearchResult(result: vscode.SettingsSearchResult): AiSettingsSearchResult { + return { + query: result.query, + kind: fromSettingsSearchResultKind(result.kind), + settings: result.settings + }; + } + + function fromSettingsSearchResultKind(kind: number): AiSettingsSearchResultKind { + switch (kind) { + case AiSettingsSearchResultKind.EMBEDDED: + return AiSettingsSearchResultKind.EMBEDDED; + case AiSettingsSearchResultKind.LLM_RANKED: + return AiSettingsSearchResultKind.LLM_RANKED; + case AiSettingsSearchResultKind.CANCELED: + return AiSettingsSearchResultKind.CANCELED; + default: + throw new Error('Unknown AiSettingsSearchResultKind'); + } + } +} + export namespace McpServerDefinition { function isHttpConfig(candidate: vscode.McpServerDefinition): candidate is vscode.McpHttpServerDefinition { return !!(candidate as vscode.McpHttpServerDefinition).uri; diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index c7b7ea6ed80..7956f27742c 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -5108,6 +5108,12 @@ export enum RelatedInformationType { SettingInformation = 4 } +export enum SettingsSearchResultKind { + EMBEDDED = 1, + LLM_RANKED = 2, + CANCELED = 3, +} + //#endregion //#region Speech diff --git a/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts b/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts index 762a4622a6b..833ba3ec78e 100644 --- a/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts +++ b/src/vs/workbench/contrib/preferences/browser/preferencesSearch.ts @@ -17,10 +17,10 @@ import { CancellationToken } from '../../../../base/common/cancellation.js'; import { ExtensionType } from '../../../../platform/extensions/common/extensions.js'; import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; -import { IAiRelatedInformationService, RelatedInformationType, SettingInformationResult } from '../../../services/aiRelatedInformation/common/aiRelatedInformation.js'; import { TfIdfCalculator, TfIdfDocument } from '../../../../base/common/tfIdf.js'; import { IStringDictionary } from '../../../../base/common/collections.js'; import { nullRange } from '../../../services/preferences/common/preferencesModels.js'; +import { IAiSettingsSearchService } from '../../../services/aiSettingsSearch/common/aiSettingsSearch.js'; export interface IEndpointDetails { urlBase?: string; @@ -350,14 +350,11 @@ export class SettingMatches { } } -class AiRelatedInformationSearchKeysProvider { - private settingKeys: string[] = []; +class AiSettingsSearchKeysProvider { private settingsRecord: IStringDictionary = {}; private currentPreferencesModel: ISettingsEditorModel | undefined; - constructor( - private readonly aiRelatedInformationService: IAiRelatedInformationService - ) { } + constructor() { } updateModel(preferencesModel: ISettingsEditorModel) { if (preferencesModel === this.currentPreferencesModel) { @@ -369,13 +366,9 @@ class AiRelatedInformationSearchKeysProvider { } private refresh() { - this.settingKeys = []; this.settingsRecord = {}; - if ( - !this.currentPreferencesModel || - !this.aiRelatedInformationService.isEnabled() - ) { + if (!this.currentPreferencesModel) { return; } @@ -385,32 +378,25 @@ class AiRelatedInformationSearchKeysProvider { } for (const section of group.sections) { for (const setting of section.settings) { - this.settingKeys.push(setting.key); this.settingsRecord[setting.key] = setting; } } } } - getSettingKeys(): string[] { - return this.settingKeys; - } - getSettingsRecord(): IStringDictionary { return this.settingsRecord; } } -class AiRelatedInformationSearchProvider implements IRemoteSearchProvider { - private static readonly AI_RELATED_INFORMATION_MAX_PICKS = 5; +class AiSettingsSearchProvider implements IRemoteSearchProvider { + private static readonly AI_SETTINGS_SEARCH_MAX_PICKS = 5; - private readonly _keysProvider: AiRelatedInformationSearchKeysProvider; + private readonly _keysProvider: AiSettingsSearchKeysProvider; private _filter: string = ''; - constructor( - @IAiRelatedInformationService private readonly aiRelatedInformationService: IAiRelatedInformationService - ) { - this._keysProvider = new AiRelatedInformationSearchKeysProvider(aiRelatedInformationService); + constructor(private readonly aiSettingsSearchService: IAiSettingsSearchService) { + this._keysProvider = new AiSettingsSearchKeysProvider(); } setFilter(filter: string) { @@ -420,41 +406,38 @@ class AiRelatedInformationSearchProvider implements IRemoteSearchProvider { async searchModel(preferencesModel: ISettingsEditorModel, token: CancellationToken): Promise { if ( !this._filter || - !this.aiRelatedInformationService.isEnabled() + !this.aiSettingsSearchService.isEnabled() ) { return null; } this._keysProvider.updateModel(preferencesModel); + this.aiSettingsSearchService.startSearch(this._filter, token); return { - filterMatches: await this.getAiRelatedInformationItems(token), + filterMatches: await this.getAiSettingsSearchItems(token), exactMatch: false }; } - private async getAiRelatedInformationItems(token: CancellationToken) { + private async getAiSettingsSearchItems(token: CancellationToken): Promise { const settingsRecord = this._keysProvider.getSettingsRecord(); - const filterMatches: ISettingMatch[] = []; - const relatedInformation = await this.aiRelatedInformationService.getRelatedInformation( - this._filter, - [RelatedInformationType.SettingInformation], - token - ) as SettingInformationResult[]; - relatedInformation.sort((a, b) => b.weight - a.weight); + const settings = await this.aiSettingsSearchService.getEmbeddingsResults(this._filter, token); + if (!settings) { + return []; + } - for (const info of relatedInformation) { - if (filterMatches.length === AiRelatedInformationSearchProvider.AI_RELATED_INFORMATION_MAX_PICKS) { + for (const settingKey of settings) { + if (filterMatches.length === AiSettingsSearchProvider.AI_SETTINGS_SEARCH_MAX_PICKS) { break; } - const pick = info.setting; filterMatches.push({ - setting: settingsRecord[pick], - matches: [settingsRecord[pick].range], + setting: settingsRecord[settingKey], + matches: [settingsRecord[settingKey].range], matchType: SettingMatchType.RemoteMatch, keyMatchScore: 0, - score: info.weight + score: 0 // the results are sorted upstream. }); } @@ -560,29 +543,21 @@ class TfIdfSearchProvider implements IRemoteSearchProvider { } class RemoteSearchProvider implements IRemoteSearchProvider { - private adaSearchProvider: AiRelatedInformationSearchProvider | undefined; - private tfIdfSearchProvider: TfIdfSearchProvider | undefined; + private aiSettingsSearchProvider: AiSettingsSearchProvider; + private tfIdfSearchProvider: TfIdfSearchProvider; private filter: string = ''; constructor( - @IAiRelatedInformationService private readonly aiRelatedInformationService: IAiRelatedInformationService + @IAiSettingsSearchService private readonly aiSettingsSearchService: IAiSettingsSearchService ) { - } - - private initializeSearchProviders() { - if (this.aiRelatedInformationService.isEnabled()) { - this.adaSearchProvider ??= new AiRelatedInformationSearchProvider(this.aiRelatedInformationService); - } - this.tfIdfSearchProvider ??= new TfIdfSearchProvider(); + this.aiSettingsSearchProvider = new AiSettingsSearchProvider(this.aiSettingsSearchService); + this.tfIdfSearchProvider = new TfIdfSearchProvider(); } setFilter(filter: string): void { - this.initializeSearchProviders(); this.filter = filter; - if (this.adaSearchProvider) { - this.adaSearchProvider.setFilter(filter); - } - this.tfIdfSearchProvider!.setFilter(filter); + this.tfIdfSearchProvider.setFilter(filter); + this.aiSettingsSearchProvider.setFilter(filter); } async searchModel(preferencesModel: ISettingsEditorModel, token: CancellationToken): Promise { @@ -590,17 +565,16 @@ class RemoteSearchProvider implements IRemoteSearchProvider { return null; } - if (!this.adaSearchProvider) { - return this.tfIdfSearchProvider!.searchModel(preferencesModel, token); + if (!this.aiSettingsSearchService.isEnabled()) { + return this.tfIdfSearchProvider.searchModel(preferencesModel, token); } - // Use TF-IDF search as a fallback, ref https://github.com/microsoft/vscode/issues/224946 - let results = await this.adaSearchProvider.searchModel(preferencesModel, token); + let results = await this.aiSettingsSearchProvider.searchModel(preferencesModel, token); if (results?.filterMatches.length) { return results; } if (!token.isCancellationRequested) { - results = await this.tfIdfSearchProvider!.searchModel(preferencesModel, token); + results = await this.tfIdfSearchProvider.searchModel(preferencesModel, token); if (results?.filterMatches.length) { return results; } diff --git a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts index e5de67fb278..49e22168be0 100644 --- a/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts +++ b/src/vs/workbench/contrib/preferences/browser/settingsEditor2.ts @@ -68,6 +68,7 @@ import { IEditorProgressService } from '../../../../platform/progress/common/pro import { IExtensionManifest } from '../../../../platform/extensions/common/extensions.js'; import { CodeWindow } from '../../../../base/browser/window.js'; import { IUserDataProfileService } from '../../../services/userDataProfile/common/userDataProfile.js'; +import { IAiSettingsSearchService } from '../../../services/aiSettingsSearch/common/aiSettingsSearch.js'; export const enum SettingsFocusContext { @@ -249,6 +250,7 @@ export class SettingsEditor2 extends EditorPane { @IExtensionGalleryService private readonly extensionGalleryService: IExtensionGalleryService, @IEditorProgressService private readonly editorProgressService: IEditorProgressService, @IUserDataProfileService userDataProfileService: IUserDataProfileService, + @IAiSettingsSearchService private readonly aiSettingsSearchService: IAiSettingsSearchService ) { super(SettingsEditor2.ID, group, telemetryService, themeService, storageService); this.searchDelayer = new Delayer(300); @@ -1700,13 +1702,31 @@ export class SettingsEditor2 extends EditorPane { return; } const localResults = await this.localFilterPreferences(query, searchInProgress.token); + let remoteResults = null; if (localResults && !localResults.exactMatch && !searchInProgress.token.isCancellationRequested) { - await this.remoteSearchPreferences(query, searchInProgress.token); + remoteResults = await this.remoteSearchPreferences(query, searchInProgress.token); + } + + if (searchInProgress.token.isCancellationRequested) { + return; } // Update UI only after all the search results are in // ref https://github.com/microsoft/vscode/issues/224946 this.onDidFinishSearch(); + + if (remoteResults) { + if (this.aiSettingsSearchService.isEnabled() && !searchInProgress.token.isCancellationRequested) { + const rankedResults = await this.aiSettingsSearchService.getLLMRankedResults(query, searchInProgress.token); + if (!searchInProgress.token.isCancellationRequested) { + if (rankedResults === null) { + this.logService.trace('No ranked results found'); + } else { + this.logService.trace(`Got ranked results ${rankedResults.join(', ')}`); + } + } + } + } }); } diff --git a/src/vs/workbench/services/aiSettingsSearch/common/aiSettingsSearch.ts b/src/vs/workbench/services/aiSettingsSearch/common/aiSettingsSearch.ts new file mode 100644 index 00000000000..f5d3a3b50bc --- /dev/null +++ b/src/vs/workbench/services/aiSettingsSearch/common/aiSettingsSearch.ts @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { IDisposable } from '../../../../base/common/lifecycle.js'; +import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; + +export const IAiSettingsSearchService = createDecorator('IAiSettingsSearchService'); + +export enum AiSettingsSearchResultKind { + EMBEDDED = 1, + LLM_RANKED = 2, + CANCELED = 3, +} + +export interface AiSettingsSearchResult { + query: string; + kind: AiSettingsSearchResultKind; + settings: string[]; +} + +export interface AiSettingsSearchProviderOptions { + limit: number; +} + +export interface IAiSettingsSearchService { + readonly _serviceBrand: undefined; + + // Called from the Settings editor + isEnabled(): boolean; + startSearch(query: string, token: CancellationToken): void; + getEmbeddingsResults(query: string, token: CancellationToken): Promise; + getLLMRankedResults(query: string, token: CancellationToken): Promise; + + // Called from the main thread + registerSettingsSearchProvider(provider: IAiSettingsSearchProvider): IDisposable; + handleSearchResult(results: AiSettingsSearchResult): void; +} + +export interface IAiSettingsSearchProvider { + searchSettings(query: string, option: AiSettingsSearchProviderOptions, token: CancellationToken): void; +} diff --git a/src/vs/workbench/services/aiSettingsSearch/common/aiSettingsSearchService.ts b/src/vs/workbench/services/aiSettingsSearch/common/aiSettingsSearchService.ts new file mode 100644 index 00000000000..d34ec08f301 --- /dev/null +++ b/src/vs/workbench/services/aiSettingsSearch/common/aiSettingsSearchService.ts @@ -0,0 +1,87 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { DeferredPromise, raceCancellation } from '../../../../base/common/async.js'; +import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { IDisposable } from '../../../../base/common/lifecycle.js'; +import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; +import { AiSettingsSearchResult, AiSettingsSearchResultKind, IAiSettingsSearchProvider, IAiSettingsSearchService } from './aiSettingsSearch.js'; + +export class AiSettingsSearchService implements IAiSettingsSearchService { + readonly _serviceBrand: undefined; + private static readonly MAX_PICKS = 5; + + private _providers: IAiSettingsSearchProvider[] = []; + private _llmRankedResultsPromises: Map> = new Map(); + private _embeddingsResultsPromises: Map> = new Map(); + + isEnabled(): boolean { + return this._providers.length > 0; + } + + registerSettingsSearchProvider(provider: IAiSettingsSearchProvider): IDisposable { + this._providers.push(provider); + return { + dispose: () => { + const index = this._providers.indexOf(provider); + if (index !== -1) { + this._providers.splice(index, 1); + } + } + }; + } + + startSearch(query: string, token: CancellationToken): void { + if (!this.isEnabled()) { + throw new Error('No settings search providers registered'); + } + + this._providers.forEach(provider => provider.searchSettings(query, { limit: AiSettingsSearchService.MAX_PICKS }, token)); + } + + async getEmbeddingsResults(query: string, token: CancellationToken): Promise { + if (!this.isEnabled()) { + throw new Error('No settings search providers registered'); + } + + const promise = new DeferredPromise(); + this._embeddingsResultsPromises.set(query, promise); + const result = await raceCancellation(promise.p, token); + return result ?? null; + } + + async getLLMRankedResults(query: string, token: CancellationToken): Promise { + if (!this.isEnabled()) { + throw new Error('No settings search providers registered'); + } + + const promise = new DeferredPromise(); + this._llmRankedResultsPromises.set(query, promise); + const result = await raceCancellation(promise.p, token); + return result ?? null; + } + + handleSearchResult(result: AiSettingsSearchResult): void { + if (!this.isEnabled()) { + return; + } + + if (result.kind === AiSettingsSearchResultKind.EMBEDDED) { + const promise = this._embeddingsResultsPromises.get(result.query); + if (promise) { + promise.complete(result.settings); + this._embeddingsResultsPromises.delete(result.query); + } + } else if (result.kind === AiSettingsSearchResultKind.LLM_RANKED) { + const promise = this._llmRankedResultsPromises.get(result.query); + if (promise) { + promise.complete(result.settings); + this._llmRankedResultsPromises.delete(result.query); + } + } + } +} + +registerSingleton(IAiSettingsSearchService, AiSettingsSearchService, InstantiationType.Delayed); diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index fc9c3ef222a..93e4c4fc944 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -69,6 +69,7 @@ import './services/editor/browser/editorService.js'; import './services/editor/browser/editorResolverService.js'; import './services/aiEmbeddingVector/common/aiEmbeddingVectorService.js'; import './services/aiRelatedInformation/common/aiRelatedInformationService.js'; +import './services/aiSettingsSearch/common/aiSettingsSearchService.js'; import './services/history/browser/historyService.js'; import './services/activity/browser/activityService.js'; import './services/keybinding/browser/keybindingService.js'; diff --git a/src/vscode-dts/vscode.proposed.aiSettingsSearch.d.ts b/src/vscode-dts/vscode.proposed.aiSettingsSearch.d.ts new file mode 100644 index 00000000000..373c5a42ae0 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.aiSettingsSearch.d.ts @@ -0,0 +1,30 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + export enum SettingsSearchResultKind { + EMBEDDED = 1, + LLM_RANKED = 2, + CANCELED = 3 + } + + export interface SettingsSearchResult { + query: string; + kind: SettingsSearchResultKind; + settings: string[]; + } + + export interface SettingsSearchProviderOptions { + limit: number; + } + + export interface SettingsSearchProvider { + provideSettingsSearchResults(query: string, option: SettingsSearchProviderOptions, progress: Progress, token: CancellationToken): Thenable; + } + + export namespace ai { + export function registerSettingsSearchProvider(provider: SettingsSearchProvider): Disposable; + } +}