Path: blob/main/extensions/copilot/src/extension/conversation/vscode-node/conversationFeature.ts
13399 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/456import * as vscode from 'vscode';7import { IAuthenticationService } from '../../../platform/authentication/common/authentication';8import { IChatAgentService, terminalAgentName } from '../../../platform/chat/common/chatAgents';9import { IConversationOptions } from '../../../platform/chat/common/conversationOptions';10import { ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService';11import { DevContainerConfigGeneratorArguments, IDevContainerConfigurationService } from '../../../platform/devcontainer/common/devContainerConfigurationService';12import { ICombinedEmbeddingIndex } from '../../../platform/embeddings/common/vscodeIndex';13import { FEEDBACK_URL } from '../../../platform/endpoint/common/domainService';14import { IVSCodeExtensionContext } from '../../../platform/extContext/common/extensionContext';15import { IGitCommitMessageService } from '../../../platform/git/common/gitCommitMessageService';16import { ILogService } from '../../../platform/log/common/logService';17import { ISettingsEditorSearchService } from '../../../platform/settingsEditor/common/settingsEditorSearchService';18import { ITelemetryService } from '../../../platform/telemetry/common/telemetry';19import { ChatExtGlobalPerfMark, markChatExtGlobal } from '../../../util/common/performance';20import { isUri } from '../../../util/common/types';21import { DeferredPromise } from '../../../util/vs/base/common/async';22import { CancellationToken } from '../../../util/vs/base/common/cancellation';23import { DisposableStore, IDisposable, combinedDisposable } from '../../../util/vs/base/common/lifecycle';24import { URI } from '../../../util/vs/base/common/uri';25import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';26import { ContributionCollection, IExtensionContribution } from '../../common/contributions';27import { vscodeNodeChatContributions } from '../../extension/vscode-node/contributions';28import { IMergeConflictService } from '../../git/common/mergeConflictService';29import { registerInlineChatCommands } from '../../inlineChat/vscode-node/inlineChatCommands';30import { INewWorkspacePreviewContentManager } from '../../intents/node/newIntent';31import { FindInFilesArgs } from '../../intents/node/searchIntent';32import { TerminalExplainIntent } from '../../intents/node/terminalExplainIntent';33import { ILinkifyService } from '../../linkify/common/linkifyService';34import { registerLinkCommands } from '../../linkify/vscode-node/commands';35import { InlineCodeSymbolLinkifier } from '../../linkify/vscode-node/inlineCodeSymbolLinkifier';36import { NotebookCellLinkifier } from '../../linkify/vscode-node/notebookCellLinkifier';37import { SymbolLinkifier } from '../../linkify/vscode-node/symbolLinkifier';38import { IntentDetector } from '../../prompt/node/intentDetector';39import { SemanticSearchTextSearchProvider } from '../../workspaceSemanticSearch/node/semanticSearchTextSearchProvider';40import { GitHubPullRequestProviders } from '../node/githubPullRequestProviders';41import { startFeedbackCollection } from './feedbackCollection';42import { registerNewWorkspaceIntentCommand } from './newWorkspaceFollowup';43import { generateTerminalFixes, setLastCommandMatchResult } from './terminalFixGenerator';4445/**46* Class that checks if users are allowed to use the conversation feature,47* and registers the relevant providers if they are.48*/49export class ConversationFeature implements IExtensionContribution {50/** Disposables that exist for the lifetime of this object */51private readonly _disposables = new DisposableStore();52/** Disposables that are cleared whenever feature enablement is toggled */53private readonly _activatedDisposables = new DisposableStore();54/** For the conversation features to be enabled, the proxy needs to return a token with k/v pair: chat=1 */55public _enabled;56/** The feature is marked as active the first time it is enabled. */57private _activated;5859/** Whether or not the search provider has been registered */60private _searchProviderRegistered = false;61/** Whether or not the settings search provider has been registered */62private _settingsSearchProviderRegistered = false;6364readonly id = 'conversationFeature';65readonly activationBlocker?: Promise<void>;6667constructor(68@IInstantiationService private instantiationService: IInstantiationService,69@ILogService private readonly logService: ILogService,70@IConfigurationService private configurationService: IConfigurationService,71@IConversationOptions private conversationOptions: IConversationOptions,72@IChatAgentService private chatAgentService: IChatAgentService,73@ITelemetryService private telemetryService: ITelemetryService,74@IAuthenticationService private readonly authenticationService: IAuthenticationService,75@ICombinedEmbeddingIndex private readonly embeddingIndex: ICombinedEmbeddingIndex,76@IDevContainerConfigurationService private readonly devContainerConfigurationService: IDevContainerConfigurationService,77@IGitCommitMessageService private readonly gitCommitMessageService: IGitCommitMessageService,78@IMergeConflictService private readonly mergeConflictService: IMergeConflictService,79@ILinkifyService private readonly linkifyService: ILinkifyService,80@IVSCodeExtensionContext private readonly extensionContext: IVSCodeExtensionContext,81@INewWorkspacePreviewContentManager private readonly newWorkspacePreviewContentManager: INewWorkspacePreviewContentManager,82@ISettingsEditorSearchService private readonly settingsEditorSearchService: ISettingsEditorSearchService,83) {84this._enabled = false;85this._activated = false;8687// Register Copilot token listener88this.registerCopilotTokenListener();8990const activationBlockerDeferred = new DeferredPromise<void>();91this.activationBlocker = activationBlockerDeferred.p;92if (authenticationService.copilotToken) {93this.logService.info(`ConversationFeature: Copilot token already available`);94this.activated = true;95activationBlockerDeferred.complete();96} else {97markChatExtGlobal(ChatExtGlobalPerfMark.WillWaitForCopilotToken);98this.logService.info(`ConversationFeature: Waiting for copilot token to activate conversation feature`);99}100101this._disposables.add(authenticationService.onDidAuthenticationChange(async () => {102const hasSession = !!authenticationService.copilotToken;103this.logService.info(`ConversationFeature: onDidAuthenticationChange has token: ${hasSession}`);104if (hasSession) {105markChatExtGlobal(ChatExtGlobalPerfMark.DidWaitForCopilotToken);106this.activated = true;107} else {108this.activated = false;109}110111activationBlockerDeferred.complete();112}));113}114115get enabled() {116return this._enabled;117}118119set enabled(value: boolean) {120if (value && !this.activated) {121this.activated = true;122}123this._enabled = value;124125// Set context value that is used to show/hide th sidebar icon126vscode.commands.executeCommand('setContext', 'github.copilot.interactiveSession.disabled', !value);127}128129get activated() {130return this._activated;131}132133set activated(value: boolean) {134if (this._activated === value) {135return;136}137this._activated = value;138139if (!value) {140this.logService.info('ConversationFeature: Deactivating contributions');141this._activatedDisposables.clear();142} else {143this.logService.info('ConversationFeature: Activating contributions');144const options: IConversationOptions = this.conversationOptions;145146this._activatedDisposables.add(this.registerProviders());147this._activatedDisposables.add(this.registerCommands(options));148this._activatedDisposables.add(this.registerRelatedInformationProviders());149this._activatedDisposables.add(this.registerParticipants(options));150this._activatedDisposables.add(this.instantiationService.createInstance(ContributionCollection, vscodeNodeChatContributions));151}152}153154dispose(): void {155this._activated = false;156this._activatedDisposables.dispose();157this._disposables?.dispose();158}159160public [Symbol.dispose]() { this.dispose(); }161162private registerParticipants(options: IConversationOptions): IDisposable {163return this.chatAgentService.register(options);164}165166private registerSearchProvider(): IDisposable | undefined {167if (this._searchProviderRegistered) {168return;169} else {170this._searchProviderRegistered = true;171172// Don't register for no auth user173if (this.authenticationService.copilotToken?.isNoAuthUser) {174this.logService.debug('ConversationFeature: Skipping search provider registration - no GitHub session available');175return;176}177178return vscode.workspace.registerAITextSearchProvider('file', this.instantiationService.createInstance(SemanticSearchTextSearchProvider));179}180}181182private registerSettingsSearchProvider(): IDisposable | undefined {183if (this._settingsSearchProviderRegistered) {184return;185}186187this._settingsSearchProviderRegistered = true;188return vscode.ai.registerSettingsSearchProvider(this.settingsEditorSearchService);189}190191private registerProviders(): IDisposable {192const disposables = new DisposableStore();193try {194const detectionProvider = this.registerParticipantDetectionProvider();195if (detectionProvider) {196disposables.add(detectionProvider);197}198199const searchDisposable = this.registerSearchProvider();200if (searchDisposable) {201disposables.add(searchDisposable);202}203204const settingsSearchDisposable = this.registerSettingsSearchProvider();205if (settingsSearchDisposable) {206disposables.add(settingsSearchDisposable);207}208} catch (err) {209this.logService.error(err, 'Registration of interactive providers failed');210}211return disposables;212}213214private registerParticipantDetectionProvider() {215if ('registerChatParticipantDetectionProvider' in vscode.chat) {216const provider = this.instantiationService.createInstance(IntentDetector);217return vscode.chat.registerChatParticipantDetectionProvider(provider);218}219}220221private registerCommands(options: IConversationOptions): IDisposable {222const disposables = new DisposableStore();223224[225vscode.commands.registerCommand('github.copilot.interactiveSession.feedback', async () => {226return vscode.env.openExternal(vscode.Uri.parse(FEEDBACK_URL));227}),228vscode.commands.registerCommand('github.copilot.chat.compact', () => vscode.commands.executeCommand('workbench.action.chat.open', { query: '/compact' })),229vscode.commands.registerCommand('github.copilot.terminal.explainTerminalLastCommand', async () => this.triggerTerminalChat({ query: `/${TerminalExplainIntent.intentName} #terminalLastCommand` })),230vscode.commands.registerCommand('github.copilot.terminal.fixTerminalLastCommand', async () => generateTerminalFixes(this.instantiationService)),231vscode.commands.registerCommand('github.copilot.terminal.generateCommitMessage', async () => {232const workspaceFolders = vscode.workspace.workspaceFolders;233234if (!workspaceFolders?.length) {235return;236}237const uri = workspaceFolders.length === 1 ? workspaceFolders[0].uri : await vscode.window.showWorkspaceFolderPick().then(folder => folder?.uri);238if (!uri) {239return;240}241242const repository = await this.gitCommitMessageService.getRepository(uri);243if (!repository) {244return;245}246247const commitMessage = await this.gitCommitMessageService.generateCommitMessage(repository, CancellationToken.None);248if (commitMessage) {249// Sanitize the message by escaping double quotes, backslashes, and $ characters250const sanitizedMessage = commitMessage.replace(/"/g, '\\"').replace(/\\/g, '\\\\').replace(/\$/g, '\\$'); // CodeQL [SM02383] Backslashes are escaped as part of the second replace.251const message = `git commit -m "${sanitizedMessage}"`;252vscode.window.activeTerminal?.sendText(message, false);253}254}),255vscode.commands.registerCommand('github.copilot.git.generateCommitMessage', async (rootUri: vscode.Uri | undefined, _: unknown, cancellationToken: vscode.CancellationToken | undefined) => {256const repository = await this.gitCommitMessageService.getRepository(rootUri);257if (!repository) {258return;259}260261const commitMessage = await this.gitCommitMessageService.generateCommitMessage(repository, cancellationToken);262if (commitMessage) {263repository.inputBox.value = commitMessage;264}265}),266vscode.commands.registerCommand('github.copilot.git.resolveMergeConflicts', async (...resourceStates: (vscode.Uri | vscode.SourceControlResourceState)[]) => {267const resources = resourceStates.filter(r => !!r).map(r => isUri(r) ? r : r.resourceUri);268await this.mergeConflictService.resolveMergeConflicts(resources, undefined);269}),270vscode.commands.registerCommand('github.copilot.devcontainer.generateDevContainerConfig', async (args: DevContainerConfigGeneratorArguments, cancellationToken?: vscode.CancellationToken) => {271if (cancellationToken) {272return this.devContainerConfigurationService.generateConfiguration(args, cancellationToken);273}274275const tokenSource = new vscode.CancellationTokenSource();276try {277return this.devContainerConfigurationService.generateConfiguration(args, tokenSource.token);278} finally {279tokenSource.dispose();280}281}),282vscode.commands.registerCommand('github.copilot.chat.openUserPreferences', async () => {283const uri = URI.joinPath(this.extensionContext.globalStorageUri, 'copilotUserPreferences.md');284return vscode.commands.executeCommand('vscode.open', uri);285}),286this.instantiationService.invokeFunction(startFeedbackCollection),287registerLinkCommands(this.telemetryService),288this.linkifyService.registerGlobalLinkifier({289create: () => this.instantiationService.createInstance(InlineCodeSymbolLinkifier)290}),291this.linkifyService.registerGlobalLinkifier({292create: () => this.instantiationService.createInstance(SymbolLinkifier)293}),294this.linkifyService.registerGlobalLinkifier({295create: () => disposables.add(this.instantiationService.createInstance(NotebookCellLinkifier))296}),297this.instantiationService.invokeFunction(registerInlineChatCommands),298this.registerTerminalQuickFixProviders(),299registerNewWorkspaceIntentCommand(this.newWorkspacePreviewContentManager, this.logService, options),300registerGitHubPullRequestTitleAndDescriptionProvider(this.instantiationService),301registerSearchIntentCommand(),302].forEach(d => disposables.add(d));303return disposables;304}305306private async triggerTerminalChat(options: { query: string; isPartialQuery?: boolean }) {307const chatLocation = this.configurationService.getConfig(ConfigKey.TerminalChatLocation);308let commandId: string;309switch (chatLocation) {310case 'quickChat':311commandId = 'workbench.action.quickchat.toggle';312options.query = `@${terminalAgentName} ` + options.query;313break;314case 'terminal':315commandId = 'workbench.action.terminal.chat.start';316// HACK: Currently @terminal is hardcoded in core317break;318case 'chatView':319default:320commandId = 'workbench.action.chat.open';321options.query = `@${terminalAgentName} ` + options.query;322break;323}324await vscode.commands.executeCommand(commandId, options);325}326327private registerRelatedInformationProviders(): IDisposable {328const disposables = new DisposableStore();329[330vscode.ai.registerRelatedInformationProvider(331vscode.RelatedInformationType.CommandInformation,332this.embeddingIndex.commandIdIndex333),334vscode.ai.registerRelatedInformationProvider(335vscode.RelatedInformationType.SettingInformation,336this.embeddingIndex.settingsIndex337)338].forEach(d => disposables.add(d));339return disposables;340}341342private registerCopilotTokenListener() {343this._disposables.add(this.authenticationService.onDidAuthenticationChange(() => {344const chatEnabled = this.authenticationService.copilotToken !== undefined;345this.logService.info(`copilot token sku: ${this.authenticationService.copilotToken?.sku ?? ''}`);346this.enabled = chatEnabled ?? false;347}));348}349350private registerTerminalQuickFixProviders() {351const isEnabled = () => this.enabled;352return combinedDisposable(353vscode.window.registerTerminalQuickFixProvider('copilot-chat.fixWithCopilot', {354provideTerminalQuickFixes(commandMatchResult, token) {355if (!isEnabled() || commandMatchResult.commandLine.endsWith('^C')) {356return [];357}358setLastCommandMatchResult(commandMatchResult);359return [360{361command: 'github.copilot.terminal.fixTerminalLastCommand',362title: vscode.l10n.t('Fix using Copilot')363},364{365command: 'github.copilot.terminal.explainTerminalLastCommand',366title: vscode.l10n.t('Explain using Copilot')367}368];369}370}),371vscode.window.registerTerminalQuickFixProvider('copilot-chat.generateCommitMessage', {372provideTerminalQuickFixes: (commandMatchResult, token) => {373return this.enabled ? [{374command: 'github.copilot.terminal.generateCommitMessage',375title: vscode.l10n.t('Generate Commit Message')376}] : [];377},378})379);380}381}382383function registerSearchIntentCommand(): IDisposable {384return vscode.commands.registerCommand('github.copilot.executeSearch', async (arg: FindInFilesArgs) => {385const show = arg.filesToExclude.length > 0 || arg.filesToInclude.length > 0;386vscode.commands.executeCommand('workbench.view.search.focus').then(() =>387vscode.commands.executeCommand('workbench.action.search.toggleQueryDetails', { show })388);389vscode.commands.executeCommand('workbench.action.findInFiles', arg);390});391}392393function registerGitHubPullRequestTitleAndDescriptionProvider(instantiationService: IInstantiationService): IDisposable {394return instantiationService.createInstance(GitHubPullRequestProviders);395}396397398