Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/conversation/vscode-node/remoteAgents.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
import { RequestType } from '@vscode/copilot-api';
6
import { Raw } from '@vscode/prompt-tsx';
7
import { ChatCompletionItem, ChatContext, ChatPromptReference, ChatRequest, ChatRequestTurn, ChatResponseMarkdownPart, ChatResponseReferencePart, ChatResponseTurn, ChatResponseWarningPart, ChatVariableLevel, Disposable, DynamicChatParticipantProps, Location, MarkdownString, Position, Progress, Range, TextDocument, TextEditor, ThemeIcon, chat, commands, l10n } from 'vscode';
8
import { IAuthenticationService } from '../../../platform/authentication/common/authentication';
9
import { IAuthenticationChatUpgradeService } from '../../../platform/authentication/common/authenticationUpgrade';
10
import { ChatFetchResponseType, ChatLocation } from '../../../platform/chat/common/commonTypes';
11
import { ICAPIClientService, } from '../../../platform/endpoint/common/capiClient';
12
import { IEndpointProvider } from '../../../platform/endpoint/common/endpointProvider';
13
import { RemoteAgentChatEndpoint } from '../../../platform/endpoint/node/chatEndpoint';
14
import { IVSCodeExtensionContext } from '../../../platform/extContext/common/extensionContext';
15
import { IGitService, getGitHubRepoInfoFromContext, toGithubNwo } from '../../../platform/git/common/gitService';
16
import { IGithubRepositoryService } from '../../../platform/github/common/githubService';
17
import { HAS_IGNORED_FILES_MESSAGE, IIgnoreService } from '../../../platform/ignore/common/ignoreService';
18
import { ILogService } from '../../../platform/log/common/logService';
19
import { ICopilotReference } from '../../../platform/networking/common/fetch';
20
import { Response } from '../../../platform/networking/common/fetcherService';
21
import { ITabsAndEditorsService } from '../../../platform/tabs/common/tabsAndEditorsService';
22
import { IWorkspaceService } from '../../../platform/workspace/common/workspaceService';
23
import { DeferredPromise } from '../../../util/vs/base/common/async';
24
import { DisposableStore, IDisposable } from '../../../util/vs/base/common/lifecycle';
25
import * as path from '../../../util/vs/base/common/path';
26
import { URI } from '../../../util/vs/base/common/uri';
27
import { generateUuid } from '../../../util/vs/base/common/uuid';
28
import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';
29
import { ChatResponseReferencePart2, Uri } from '../../../vscodeTypes';
30
import { ICopilotChatResult, ICopilotChatResultIn } from '../../prompt/common/conversation';
31
import { IPromptVariablesService } from '../../prompt/node/promptVariablesService';
32
import { IUserFeedbackService } from './userActions';
33
34
interface IAgent {
35
name: string;
36
avatar_url: string;
37
owner_login: string;
38
owner_avatar_url: string;
39
description: string;
40
slug: string;
41
editor_context: boolean;
42
}
43
44
interface IAgentsResponse {
45
agents: IAgent[];
46
}
47
48
const agentRegistrations = new Map<string, Disposable>();
49
50
const GITHUB_PLATFORM_AGENT_NAME = 'github';
51
const GITHUB_PLATFORM_AGENT_ID = 'platform';
52
const GITHUB_PLATFORM_AGENT_SKILLS: { [key: string]: string } = {
53
web: 'bing-search',
54
};
55
56
type IPlatformReference = IFileReference | ISelectionReference | IGitHubRepositoryReference;
57
58
interface IFileReference {
59
type: 'client.file';
60
data: {
61
language: string;
62
content: string;
63
};
64
is_implicit: boolean;
65
id: string;
66
}
67
68
interface ISelectionReference {
69
type: 'client.selection';
70
data: {
71
start: { line: number; col: number };
72
end: { line: number; col: number };
73
content: string;
74
};
75
is_implicit: boolean;
76
id: string;
77
}
78
79
interface IGitHubRepositoryReference {
80
type: 'github.repository';
81
data: {
82
type: 'repository';
83
name: string; // name of the repository
84
ownerLogin: string; // owner of the repository
85
id: number;
86
};
87
id: string; // e.g. "microsoft/vscode"
88
}
89
90
export class RemoteAgentContribution implements IDisposable {
91
private readonly disposables = new DisposableStore();
92
private refreshRemoteAgentsP: Promise<void> | undefined;
93
private enabledSkillsPromise: Promise<Set<string>> | undefined;
94
95
constructor(
96
@ILogService private readonly logService: ILogService,
97
@IEndpointProvider private readonly endpointProvider: IEndpointProvider,
98
@ICAPIClientService private readonly capiClientService: ICAPIClientService,
99
@IPromptVariablesService private readonly promptVariablesService: IPromptVariablesService,
100
@IWorkspaceService private readonly workspaceService: IWorkspaceService,
101
@ITabsAndEditorsService private readonly tabsAndEditorsService: ITabsAndEditorsService,
102
@IIgnoreService private readonly ignoreService: IIgnoreService,
103
@IGitService private readonly gitService: IGitService,
104
@IGithubRepositoryService private readonly githubRepositoryService: IGithubRepositoryService,
105
@IVSCodeExtensionContext private readonly vscodeExtensionContext: IVSCodeExtensionContext,
106
@IAuthenticationService private readonly authenticationService: IAuthenticationService,
107
@IUserFeedbackService private readonly userFeedbackService: IUserFeedbackService,
108
@IInstantiationService private readonly instantiationService: IInstantiationService,
109
@IAuthenticationChatUpgradeService private readonly authenticationChatUpgradeService: IAuthenticationChatUpgradeService,
110
) {
111
this.disposables.add(new Disposable(() => agentRegistrations.forEach(agent => agent.dispose())));
112
113
this.refreshRemoteAgents();
114
// Refresh remote agents whenever auth changes, e.g. in case the user was initially not signed in
115
this.disposables.add(this.authenticationService.onDidAccessTokenChange(() => {
116
this.refreshRemoteAgents();
117
}));
118
}
119
120
dispose() {
121
this.disposables.dispose();
122
}
123
124
private async refreshRemoteAgents(): Promise<void> {
125
if (!this.refreshRemoteAgentsP) {
126
this.refreshRemoteAgentsP = this._doRefreshRemoteAgents();
127
}
128
129
return this.refreshRemoteAgentsP.finally(() => this.refreshRemoteAgentsP = undefined);
130
}
131
132
private async _doRefreshRemoteAgents(): Promise<void> {
133
const existingAgents = new Set(agentRegistrations.keys());
134
135
try {
136
const authToken = this.authenticationService.anyGitHubSession?.accessToken;
137
if (!authToken) {
138
// We have to silently wait for auth to become available so we can fetch remote agents
139
this.logService.warn('Unable to fetch remote agents because user is not signed in.');
140
return;
141
}
142
try {
143
// First try to register the default platform agent
144
if (!existingAgents.delete(GITHUB_PLATFORM_AGENT_ID)) { // Don't reregister it
145
this.logService.info('Registering default platform agent...');
146
agentRegistrations.set(GITHUB_PLATFORM_AGENT_ID, this.registerAgent(null));
147
}
148
} catch (ex) {
149
this.logService.info(`Encountered error while registering platform agent: ${JSON.stringify(ex)}`);
150
}
151
const response = await this.capiClientService.makeRequest<Response>({
152
method: 'GET',
153
headers: {
154
Authorization: `Bearer ${authToken}`
155
}
156
}, { type: RequestType.RemoteAgent });
157
const text = await response.text();
158
let newAgents: IAgent[];
159
try {
160
newAgents = (<IAgentsResponse>JSON.parse(text)).agents;
161
if (!Array.isArray(newAgents)) {
162
throw new Error(`Expected 'agents' to be an array`);
163
}
164
} catch (e) {
165
if (!text.includes('access denied')) {
166
this.logService.warn(`Invalid remote agent response: ${text} (${e})`);
167
}
168
return;
169
}
170
171
for (const agent of newAgents) {
172
if (!existingAgents.delete(agent.slug)) {
173
// only register if we haven't seen them yet
174
agentRegistrations.set(agent.slug, this.registerAgent(agent));
175
}
176
}
177
} catch (e) {
178
this.logService.error(e, 'Failed to load remote copilot agents');
179
}
180
181
for (const item of existingAgents) {
182
agentRegistrations.get(item)!.dispose();
183
agentRegistrations.delete(item);
184
}
185
}
186
187
private checkAuthorized(agent: string) {
188
if (agent === GITHUB_PLATFORM_AGENT_NAME) {
189
return true;
190
}
191
const key = `copilot.agent.${agent}.authorized`;
192
return this.vscodeExtensionContext.globalState.get<boolean>(key, false) || this.vscodeExtensionContext.workspaceState.get<boolean>(key, false);
193
}
194
195
private async setAuthorized(agent: string, isGlobal = false) {
196
const memento = isGlobal ? this.vscodeExtensionContext.globalState : this.vscodeExtensionContext.workspaceState;
197
await memento.update(`copilot.agent.${agent}.authorized`, true);
198
}
199
200
private registerAgent(agentData: IAgent | null): Disposable {
201
const store = new DisposableStore();
202
const participantId = `github.copilot-dynamic.${agentData?.slug ?? GITHUB_PLATFORM_AGENT_ID}`;
203
const slug = agentData?.slug ?? GITHUB_PLATFORM_AGENT_NAME;
204
const description = agentData?.description ?? l10n.t("Get answers grounded in web search and code search");
205
const dynamicProps: DynamicChatParticipantProps = {
206
name: slug,
207
description,
208
publisherName: agentData?.owner_login ?? 'GitHub',
209
fullName: agentData?.name ?? 'GitHub',
210
};
211
let hasShownImplicitContextAuthorizationForSession = false;
212
const agent = store.add(chat.createDynamicChatParticipant(participantId, dynamicProps, async (request, context, responseStream, token): Promise<ICopilotChatResult> => {
213
const sessionId = getOrCreateSessionId(context);
214
const responseId = generateUuid();
215
// This isn't used anywhere but is needed to fix the IChatResult shape which the remote agents follow
216
const modelMessageId = generateUuid();
217
const metadata: ICopilotChatResult['metadata'] & Record<string, unknown> = {
218
sessionId,
219
modelMessageId,
220
responseId,
221
agentId: participantId,
222
command: request.command,
223
};
224
225
let accessToken: string | undefined;
226
if (request.acceptedConfirmationData) {
227
for (const data of request.acceptedConfirmationData) {
228
if (data?.url) {
229
// Store that the user has authorized the agent
230
await this.setAuthorized(slug, request.prompt.startsWith(l10n.t('Authorize for all workspaces')));
231
await commands.executeCommand('vscode.open', Uri.parse(data.url));
232
responseStream.markdown(l10n.t('Please complete authorization in your browser and resend your question.'));
233
return { metadata } satisfies ICopilotChatResult;
234
} else if (data?.hasAcknowledgedImplicitReferences) {
235
// Store that the user has acknowledged implicit references
236
await this.setAuthorized(slug, request.prompt.startsWith(l10n.t('Allow for All Workspaces')));
237
responseStream.markdown(l10n.t('Your preference has been saved.'));
238
return { metadata } satisfies ICopilotChatResult;
239
// This property is set by the confirmation in the Upgrade service
240
} else if (data?.authPermissionPrompted) {
241
request = await this.authenticationChatUpgradeService.handleConfirmationRequest(responseStream, request, context.history);
242
metadata.command = request.command;
243
accessToken = (await this.authenticationService.getGitHubSession('permissive', { silent: true }))?.accessToken;
244
if (!accessToken) {
245
responseStream.markdown(l10n.t('The additional permissions are required for this feature.'));
246
return { metadata } satisfies ICopilotChatResult;
247
}
248
}
249
}
250
}
251
252
// Slugless means it's the platform agent
253
if (!agentData?.slug) {
254
accessToken = this.authenticationService.permissiveGitHubSession?.accessToken;
255
if (!accessToken) {
256
if (this.authenticationService.isMinimalMode) {
257
responseStream.markdown(l10n.t('Minimal mode is enabled. You will need to change `github.copilot.advanced.authPermissions` to `default` to use this feature.'));
258
responseStream.button({
259
title: l10n.t('Open Settings (JSON)'),
260
command: 'workbench.action.openSettingsJson',
261
arguments: [{ revealSetting: { key: 'github.copilot.advanced.authPermissions' } }]
262
});
263
} else {
264
// Otherwise, show the permissive session upgrade prompt because it's required
265
this.authenticationChatUpgradeService.showPermissiveSessionUpgradeInChat(
266
responseStream,
267
request,
268
l10n.t('`@github` requires access to your repositories on GitHub for handling requests.')
269
);
270
}
271
return { metadata } satisfies ICopilotChatResult;
272
}
273
}
274
275
// Use the basic access token as a fallback
276
if (!accessToken) {
277
accessToken = this.authenticationService.anyGitHubSession?.accessToken;
278
}
279
280
try {
281
const selectedEndpoint = await this.endpointProvider.getChatEndpoint(request);
282
// Converts the selected endpoint to a remote agent endpoint so we can request the model the user selected to the agent
283
const endpoint = this.instantiationService.createInstance(RemoteAgentChatEndpoint, {
284
model_picker_enabled: false,
285
is_chat_default: false,
286
vendor: selectedEndpoint.modelProvider,
287
billing: selectedEndpoint.isPremium !== undefined || selectedEndpoint.multiplier !== undefined ? { is_premium: selectedEndpoint.isPremium, multiplier: selectedEndpoint.multiplier, restricted_to: selectedEndpoint.restrictedToSkus } : undefined,
288
is_chat_fallback: false,
289
capabilities: {
290
supports: { tool_calls: selectedEndpoint.supportsToolCalls, vision: selectedEndpoint.supportsVision, streaming: true },
291
type: 'chat',
292
tokenizer: selectedEndpoint.tokenizer,
293
family: selectedEndpoint.family,
294
},
295
id: selectedEndpoint.model,
296
name: selectedEndpoint.name,
297
version: selectedEndpoint.version,
298
}, agentData ? { type: RequestType.RemoteAgentChat, slug: agentData.slug } : { type: RequestType.RemoteAgentChat });
299
300
// This flattens the docs agent's variables and ignores other variable values for now
301
const resolved = await this.promptVariablesService.resolveVariablesInPrompt(request.prompt, request.references);
302
303
// Collect copilot skills and references to be sent in the request
304
const copilotReferences = [];
305
const { copilot_skills } = await this.resolveCopilotSkills(slug, request);
306
307
let hasIgnoredFiles = false;
308
try {
309
const result = await this.prepareClientPlatformReferences([...request.references], slug);
310
hasIgnoredFiles = result.hasIgnoredFiles;
311
312
if (result.clientReferences) {
313
copilotReferences.push(...result.clientReferences);
314
}
315
for (const ref of result.vscodeReferences) {
316
responseStream.reference(ref);
317
}
318
} catch (ex) {
319
if (ex instanceof Error && ex.message.includes('File seems to be binary and cannot be opened as text')) {
320
responseStream.markdown(l10n.t("Sorry, binary files are not currently supported."));
321
return { metadata } satisfies ICopilotChatResult;
322
} else {
323
return {
324
errorDetails: { message: (ex.message) },
325
metadata
326
};
327
}
328
}
329
330
// Note: the platform agent will deal with token counting for us
331
const reportedReferences = new Map<string, ICopilotReference>();
332
const agentReferences: ICopilotReference[] = [];
333
const confirmations = prepareConfirmations(request);
334
let reportedProgress: Progress<ChatResponseWarningPart | ChatResponseReferencePart2> | undefined = undefined;
335
let pendingProgress: { resolvedMessage: string; deferred: DeferredPromise<string> } | undefined;
336
let hadCopilotErrorsOrConfirmations = false;
337
338
const response = await endpoint.makeChatRequest(
339
'remoteAgent',
340
[
341
...prepareRemoteAgentHistory(participantId, context),
342
{
343
role: Raw.ChatRole.User,
344
content: (request.acceptedConfirmationData?.length || request.rejectedConfirmationData?.length)
345
? []
346
: [{ type: Raw.ChatCompletionContentPartKind.Text, text: resolved.message }],
347
...(copilotReferences.length ? { copilot_references: copilotReferences } : undefined),
348
...(confirmations?.length ? { copilot_confirmations: confirmations } : undefined),
349
}
350
],
351
async (result, _, delta) => {
352
if (delta.copilotReferences) {
353
354
const processReference = (reference: ICopilotReference, parentReference?: ICopilotReference) => {
355
const url = 'url' in reference ? reference.url : 'url' in reference.data ? reference.data.url : 'html_url' in reference.data ? reference.data.html_url : undefined;
356
if (url && typeof url === 'string') {
357
if (!reportedReferences.has(url)) {
358
let icon: ChatResponseReferencePart['iconPath'] = undefined;
359
const parsed = new URL(url);
360
if (parsed.hostname === 'github.com') {
361
icon = new ThemeIcon('github');
362
} else {
363
icon = new ThemeIcon('globe');
364
}
365
if (reportedProgress) {
366
reportedProgress?.report(new ChatResponseReferencePart(Uri.parse(url), icon));
367
} else {
368
responseStream.reference(Uri.parse(url), icon);
369
}
370
371
// Keep track of the parent reference and not the individual URL used, as this will be sent again in history
372
reportedReferences.set(url, parentReference ?? reference);
373
}
374
} else if (reference.metadata) {
375
const icon = reference.metadata.display_icon ? Uri.parse(reference.metadata.display_icon) : new ThemeIcon('globe');
376
const value = reference.metadata.display_url ? Uri.parse(reference.metadata.display_url) : reference.metadata.display_name;
377
if (reportedProgress) {
378
reportedProgress.report(new ChatResponseReferencePart2(value, icon));
379
} else {
380
responseStream.reference2(value, icon);
381
}
382
reportedReferences.set(reference.metadata.display_url ?? reference.metadata.display_name, parentReference ?? reference);
383
}
384
};
385
386
// Report web references
387
for (const reference of delta.copilotReferences) {
388
if (Array.isArray(reference.data.results)) {
389
reference.data.results.forEach((r) => {
390
processReference(r, reference);
391
});
392
} else if (reference.data.type === 'github.agent') {
393
agentReferences.push(reference);
394
} else if (reference.type === 'github.text') {
395
continue;
396
} else if ('html_url' in reference.data || 'url' in reference.data && typeof reference.data.url === 'string' || reference.metadata) {
397
processReference(reference);
398
}
399
}
400
}
401
402
const reportProgress = (progress: Progress<ChatResponseWarningPart | ChatResponseReferencePart>, resolvedMessage: string) => {
403
pendingProgress?.deferred.complete(pendingProgress.resolvedMessage);
404
reportedProgress = progress;
405
const deferred = new DeferredPromise<string>();
406
pendingProgress = { deferred, resolvedMessage };
407
return deferred.p;
408
};
409
410
if (delta._deprecatedCopilotFunctionCalls) {
411
for (const call of delta._deprecatedCopilotFunctionCalls) {
412
switch (call.name) {
413
case 'bing-search': {
414
try {
415
const data: { query: string } = JSON.parse(call.arguments);
416
responseStream.progress(l10n.t('Searching Bing for "{0}"...', data.query), async (progress) => reportProgress(progress, l10n.t('Bing search results for "{0}"', data.query)));
417
} catch (ex) { }
418
break;
419
}
420
case 'codesearch': {
421
try {
422
const data: { query: string; scopingQuery: string } = JSON.parse(call.arguments);
423
responseStream.progress(l10n.t('Searching {0} for "{1}"...', data.scopingQuery, data.query), async (progress) =>
424
reportProgress(progress, l10n.t('Code search results for "{0}" in {1}', data.query, data.scopingQuery)));
425
} catch (ex) { }
426
break;
427
}
428
}
429
}
430
}
431
432
if (delta.copilotErrors && typeof responseStream.warning === 'function') {
433
hadCopilotErrorsOrConfirmations = true;
434
for (const error of delta.copilotErrors) {
435
if (reportedProgress) {
436
reportedProgress?.report(new ChatResponseWarningPart(error.message));
437
} else {
438
responseStream.warning(error.message);
439
}
440
}
441
}
442
443
if (delta.copilotConfirmation) {
444
hadCopilotErrorsOrConfirmations = true;
445
const confirm = delta.copilotConfirmation;
446
responseStream.confirmation(confirm.title, confirm.message, confirm.confirmation);
447
}
448
449
if (delta.text) {
450
pendingProgress?.deferred.complete(pendingProgress.resolvedMessage);
451
const md = new MarkdownString(delta.text);
452
md.supportHtml = true;
453
responseStream.markdown(md);
454
}
455
return undefined;
456
},
457
token,
458
ChatLocation.Panel,
459
undefined,
460
{
461
secretKey: accessToken,
462
copilot_thread_id: sessionId,
463
...(copilot_skills ? { copilot_skills } : undefined)
464
},
465
true,
466
{
467
messageSource: `serverAgent.${agentData?.slug ?? GITHUB_PLATFORM_AGENT_ID}`,
468
}
469
);
470
471
metadata['copilot_references'] = [...new Set(reportedReferences.values()).values(), ...agentReferences];
472
if (response.type === ChatFetchResponseType.Success && hasIgnoredFiles) {
473
responseStream.markdown(HAS_IGNORED_FILES_MESSAGE);
474
}
475
476
if (response.type !== ChatFetchResponseType.Success) {
477
this.logService.warn(`Bad response from remote agent "${slug}": ${response.type} ${response.reason}`);
478
if (response.reason.includes('400 no docs found')) {
479
return {
480
errorDetails: { message: 'No docs found' },
481
metadata
482
};
483
} else if (response.type === ChatFetchResponseType.AgentUnauthorized) {
484
const url = new URL(response.authorizationUrl);
485
const editorContext = agentData?.editor_context ? l10n.t('**@{0}** will read your active file and selection.', slug) : '';
486
responseStream.confirmation(
487
l10n.t('Authorize agent'),
488
editorContext + '\n' +
489
l10n.t({
490
message: 'Please authorize usage of **@{0}** on {1} and resend your question. [Learn more]({2}).',
491
args: [slug, url.hostname, 'https://aka.ms/vscode-github-chat-extension-editor-context'],
492
comment: [`{Locked=']({'}`]
493
}),
494
{ url: response.authorizationUrl },
495
[l10n.t("Authorize"), l10n.t('Authorize for All Workspaces')]
496
);
497
return { metadata, nextQuestion: { prompt: request.prompt, participant: participantId, command: request.command } } satisfies ICopilotChatResult;
498
} else if (response.type === ChatFetchResponseType.AgentFailedDependency) {
499
return {
500
errorDetails: { message: l10n.t('Sorry, an error occurred: {0}', response.reason) },
501
metadata
502
};
503
} else if (response.type !== ChatFetchResponseType.Unknown || !hadCopilotErrorsOrConfirmations) {
504
return {
505
errorDetails: { message: response.reason },
506
metadata
507
};
508
}
509
}
510
511
// Ask the user to authorize implicit context
512
if (!this.checkAuthorized(slug) && agentData?.editor_context && !hasShownImplicitContextAuthorizationForSession) {
513
responseStream.confirmation(
514
l10n.t('Grant access to editor context'),
515
l10n.t({
516
message: '**@{0}** would like to read your active file and selection. [Learn More]({1})',
517
args: [slug, 'https://aka.ms/vscode-github-chat-extension-editor-context'],
518
comment: [`{Locked=']({'}`]
519
}),
520
{ hasAcknowledgedImplicitReferences: true },
521
[l10n.t("Allow"), l10n.t("Allow for All Workspaces")]
522
);
523
hasShownImplicitContextAuthorizationForSession = true;
524
}
525
526
return { metadata } satisfies ICopilotChatResult;
527
} catch (e) {
528
this.logService.error(`/agents/${slug} failed: ${e}`);
529
return { metadata };
530
}
531
}));
532
agent.iconPath = agentData ? Uri.parse(agentData.avatar_url) : new ThemeIcon('github');
533
534
if (slug === GITHUB_PLATFORM_AGENT_NAME) {
535
agent.participantVariableProvider = {
536
triggerCharacters: ['#'],
537
provider: {
538
provideCompletionItems: async (query, token) => {
539
const items = await this.getPlatformAgentSkills();
540
return items.map<ChatCompletionItem>(i => {
541
const item = new ChatCompletionItem(`copilot.${i.name}`, '#' + i.name, [{ value: i.insertText, level: ChatVariableLevel.Full, description: i.description }]);
542
item.command = i.command;
543
item.detail = i.description;
544
return item;
545
});
546
},
547
}
548
};
549
}
550
551
store.add(
552
agent.onDidReceiveFeedback(e => this.userFeedbackService.handleFeedback(e, participantId)));
553
554
return store;
555
}
556
557
private async prepareClientPlatformReferences(variables: ChatPromptReference[], slug: string) {
558
const clientReferences: IPlatformReference[] = [];
559
const vscodeReferences: ({
560
variableName: string;
561
value?: Uri | Location | undefined;
562
} | Location | Uri)[] = [];
563
let hasIgnoredFiles = false;
564
let hasSentImplicitSelectionReference = false;
565
566
const redactFileContents = async (document: TextDocument, range?: Range) => {
567
const filename = path.basename(document.uri.toString());
568
let content = document.getText(range);
569
if (await this.ignoreService.isCopilotIgnored(document.uri)) {
570
hasIgnoredFiles = true;
571
content = 'content-exclusion';
572
} else if (filename.startsWith('.')) {
573
content = 'hidden-file'; // e.g. .env
574
} else if (Buffer.byteLength(content, 'utf8') > 1024 ** 3) {
575
content = 'file-too-large'; // exceeds 1GB
576
}
577
return content;
578
};
579
580
const getImplicitContextId = async (uri: Uri) => {
581
// The ID of the file should be relative to the root of the repository if we're in a repository
582
// falling back to a workspace folder-relative path if we're not in a repository
583
// and finally falling back to the file basename e.g. if it's an untracked file that doesn't belong to the open workspace or repo
584
const repository = await this.gitService.getRepository(uri);
585
const baseUri = repository ? repository.rootUri.toString() : this.workspaceService.getWorkspaceFolder(uri)?.toString();
586
return baseUri ? path.relative(baseUri, uri.toString()) : path.basename(uri.path);
587
};
588
589
const addFileReference = async (document: TextDocument, variableName?: string, isImplicit?: boolean) => {
590
clientReferences.push({
591
type: 'client.file',
592
data: {
593
language: document.languageId,
594
content: await redactFileContents(document)
595
},
596
is_implicit: Boolean(isImplicit),
597
id: await getImplicitContextId(document.uri)
598
});
599
600
vscodeReferences.push(variableName
601
? { variableName, value: document.uri }
602
: document.uri
603
);
604
};
605
606
const addSelectionReference = async (activeTextEditor: TextEditor, variableName?: string, reportReference = false, isImplicit?: boolean) => {
607
const selectionStart = activeTextEditor.selection.start.line;
608
const selection = activeTextEditor.selection.isEmpty ? new Range(new Position(selectionStart, 0), new Position(selectionStart + 1, 0)) : activeTextEditor.selection;
609
610
clientReferences.push({
611
type: 'client.selection',
612
data: {
613
start: { line: selection.start.line, col: selection.start.character },
614
end: { line: selection.end.line, col: selection.end.character },
615
content: await redactFileContents(activeTextEditor.document, selection)
616
},
617
is_implicit: Boolean(isImplicit),
618
id: await getImplicitContextId(activeTextEditor.document.uri)
619
});
620
621
if (reportReference) {
622
vscodeReferences.push(variableName
623
? { variableName, value: new Location(activeTextEditor.document.uri, selection) }
624
: new Location(activeTextEditor.document.uri, selection)
625
);
626
}
627
};
628
629
// Check whether we can send file and selection data implicitly
630
if (this.checkAuthorized(slug)) {
631
const { activeTextEditor } = this.tabsAndEditorsService;
632
if (activeTextEditor && variables.find(v => v.id.startsWith('vscode.implicit'))) {
633
await addFileReference(activeTextEditor.document, undefined, true);
634
await addSelectionReference(activeTextEditor, undefined, undefined, true);
635
hasSentImplicitSelectionReference = true;
636
}
637
}
638
639
for (const variable of variables) {
640
if (URI.isUri(variable.value)) {
641
const textDocument = await this.workspaceService.openTextDocument(variable.value);
642
await addFileReference(textDocument, variable.name);
643
} else if (variable.name === 'selection') {
644
const { activeTextEditor } = this.tabsAndEditorsService;
645
if (!activeTextEditor) {
646
throw new Error(l10n.t({ message: 'Please open a text editor to use the `#selection` variable.', comment: '{Locked=\'`#selection`\'}' }));
647
}
648
if (!hasSentImplicitSelectionReference) {
649
await addSelectionReference(activeTextEditor, variable.name, true);
650
}
651
} else if (variable.name === 'editor' && this.tabsAndEditorsService.activeTextEditor) {
652
await addFileReference(this.tabsAndEditorsService.activeTextEditor.document, variable.name);
653
}
654
}
655
656
// Always send the open GitHub repositories
657
if (!this.gitService.isInitialized) {
658
await this.gitService.initialize();
659
}
660
const repositories = this.gitService.repositories;
661
for (const repository of repositories) {
662
const repoId = getGitHubRepoInfoFromContext(repository)?.id;
663
if (!repoId) {
664
continue; // Not a GitHub repository
665
}
666
667
try {
668
const repo = await this.githubRepositoryService.getRepositoryInfo(repoId.org, repoId.repo);
669
clientReferences.push({
670
type: 'github.repository',
671
id: toGithubNwo(repoId),
672
data: {
673
type: 'repository',
674
name: repoId.repo,
675
ownerLogin: repoId.org,
676
id: repo.id
677
}
678
});
679
} catch (ex) {
680
if (ex instanceof Error && ex.message.includes('Failed to fetch repository info')) {
681
// TODO display a merged confirmation to reauthorize with the repo scope
682
// For now, raise a reauth badge so the user has a way out of this state
683
void this.authenticationService.getGitHubSession('permissive', { silent: true });
684
}
685
this.logService.error(ex, 'Failed to fetch info about current GitHub repository');
686
}
687
}
688
689
return { clientReferences, vscodeReferences, hasIgnoredFiles };
690
}
691
692
private async listEnabledSkills(authToken: string) {
693
if (!this.enabledSkillsPromise) {
694
this.enabledSkillsPromise = this.capiClientService.makeRequest<Response>({
695
method: 'GET',
696
headers: {
697
Authorization: `Bearer ${authToken}`,
698
}
699
}, { type: RequestType.ListSkills })
700
.then(response => response.json())
701
.then((json) => json?.['skills'].reduce((acc: Set<string>, skill: { slug: string }) => acc.add(skill.slug), new Set()));
702
}
703
return this.enabledSkillsPromise;
704
}
705
706
private async resolveCopilotSkills(agent: string, request: ChatRequest): Promise<{ copilot_skills: string[] }> {
707
if (agent === GITHUB_PLATFORM_AGENT_NAME) {
708
const skills = new Set<string>();
709
for (const variable of request.references) {
710
if (GITHUB_PLATFORM_AGENT_SKILLS[variable.name]) {
711
skills.add(GITHUB_PLATFORM_AGENT_SKILLS[variable.name]);
712
}
713
}
714
return { copilot_skills: [...skills] };
715
}
716
717
return { copilot_skills: [] };
718
}
719
720
private async getPlatformAgentSkills() {
721
const authToken = this.authenticationService.anyGitHubSession?.accessToken;
722
if (!authToken) {
723
return [];
724
}
725
726
// Register platform agent-specific native skills
727
const skills = await this.listEnabledSkills(authToken);
728
729
return [
730
{ name: 'web', insertText: `#web`, description: 'Search Bing for real-time context', kind: 'bing-search', command: undefined },
731
].filter((skill) => skills.has(skill.kind));
732
}
733
}
734
735
function prepareConfirmations(request: ChatRequest) {
736
const confirmations = [
737
...(request.acceptedConfirmationData?.map(c => ({ state: 'accepted', confirmation: c })) ?? []),
738
...(request.rejectedConfirmationData?.map(c => ({ state: 'dismissed', confirmation: c })) ?? []),
739
];
740
741
return confirmations;
742
}
743
744
function prepareRemoteAgentHistory(agentId: string, context: ChatContext): Raw.ChatMessage[] {
745
746
const result: Raw.ChatMessage[] = [];
747
748
for (const h of context.history) {
749
750
if (h.participant !== agentId) {
751
continue;
752
}
753
754
if (h instanceof ChatRequestTurn) {
755
result.push({
756
role: Raw.ChatRole.User,
757
content: [{ type: Raw.ChatCompletionContentPartKind.Text, text: h.prompt }],
758
});
759
}
760
761
if (h instanceof ChatResponseTurn) {
762
const copilot_references = h.result.metadata?.['copilot_references'];
763
const content = h.response.map(r => {
764
if (r instanceof ChatResponseMarkdownPart) {
765
return r.value.value;
766
} else if ('content' in r) {
767
return r.content;
768
} else {
769
return null;
770
}
771
}).filter(r => !!r).join('');
772
result.push({
773
role: Raw.ChatRole.Assistant,
774
content: [{ type: Raw.ChatCompletionContentPartKind.Text, text: content }],
775
...(copilot_references ? { copilot_references } : undefined)
776
});
777
}
778
}
779
780
return result;
781
}
782
783
function getOrCreateSessionId(context: ChatContext): string {
784
let sessionId: string | undefined;
785
for (const h of context.history) {
786
if (h instanceof ChatResponseTurn) {
787
const maybeSessionId = (h.result as ICopilotChatResultIn).metadata?.sessionId;
788
if (typeof maybeSessionId === 'string') {
789
sessionId = maybeSessionId;
790
break;
791
}
792
}
793
}
794
795
return sessionId ?? generateUuid();
796
}
797
798