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