Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/extensions/copilot/src/extension/inlineEdits/vscode-node/components/inlineEditDebugComponent.ts
13405 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
import { Command, commands, ThemeIcon, window } from 'vscode';
7
import { ConfigKey } from '../../../../platform/configuration/common/configurationService';
8
import { InlineEditRequestLogContext } from '../../../../platform/inlineEdits/common/inlineEditLogContext';
9
import { TsExpr } from '../../../../platform/inlineEdits/common/utils/tsExpr';
10
import { LogEntry } from '../../../../platform/workspaceRecorder/common/workspaceLog';
11
import { assertNever } from '../../../../util/vs/base/common/assert';
12
import { Disposable } from '../../../../util/vs/base/common/lifecycle';
13
import { IObservable, ISettableObservable } from '../../../../util/vs/base/common/observableInternal';
14
import { basename, extname } from '../../../../util/vs/base/common/path';
15
import { openIssueReporter } from '../../../conversation/vscode-node/feedbackReporter';
16
import { XtabProvider } from '../../../xtab/node/xtabProvider';
17
import { defaultNextEditProviderId } from '../../node/createNextEditProvider';
18
import { DebugRecorder } from '../../node/debugRecorder';
19
20
export const reportFeedbackCommandId = 'github.copilot.debug.inlineEdit.reportFeedback';
21
const pickProviderId = 'github.copilot.debug.inlineEdit.pickProvider';
22
23
export type InlineCompletionCommand = { command: Command; icon: ThemeIcon };
24
25
export class InlineEditDebugComponent extends Disposable {
26
27
constructor(
28
private readonly _internalActionsEnabled: IObservable<boolean>,
29
private readonly _inlineEditsEnabled: IObservable<boolean>,
30
private readonly _debugRecorder: DebugRecorder,
31
private readonly _inlineEditsProviderId: ISettableObservable<string | undefined>,
32
) {
33
super();
34
35
this._register(commands.registerCommand(reportFeedbackCommandId, async (args: { logContext: InlineEditRequestLogContext }) => {
36
if (!this._inlineEditsEnabled.get()) {
37
return;
38
}
39
const isInternalUser = this._internalActionsEnabled.get();
40
41
const data = new SimpleMarkdownBuilder();
42
43
data.appendLine(`# Inline Edits Debug Info`);
44
45
if (!isInternalUser) {
46
// Public users
47
data.appendLine(args.logContext.toMinimalLog());
48
} else {
49
// Internal users
50
data.appendLine(args.logContext.toLogDocument());
51
52
let logFilteredForSensitiveFiles: LogEntry[] | undefined;
53
{
54
const bookmark = args.logContext.recordingBookmark;
55
const log = this._debugRecorder.getRecentLog(bookmark);
56
57
let hasRemovedSensitiveFilesFromHistory = false;
58
let sectionContent;
59
if (log === undefined) {
60
sectionContent = ['Could not get recording to generate stest (likely because there was no corresponding workspaceRoot for this file)'];
61
} else {
62
logFilteredForSensitiveFiles = filterLogForSensitiveFiles(log);
63
hasRemovedSensitiveFilesFromHistory = log.length !== logFilteredForSensitiveFiles.length;
64
const stest = generateSTest(logFilteredForSensitiveFiles);
65
66
sectionContent = [
67
'```typescript',
68
stest,
69
'```'
70
];
71
}
72
const header = hasRemovedSensitiveFilesFromHistory ? 'STest (sensitive files removed)' : 'STest';
73
data.appendSection(header, sectionContent);
74
data.appendLine('');
75
}
76
77
{
78
if (logFilteredForSensitiveFiles !== undefined) {
79
data.appendSection('Recording', ['```json', JSON.stringify(logFilteredForSensitiveFiles, undefined, 2), '```']);
80
}
81
}
82
83
{
84
const uiRepro = await extractInlineEditRepro();
85
if (uiRepro) {
86
data.appendSection('UI Repro', ['```', uiRepro, '```']);
87
}
88
}
89
}
90
91
await openIssueReporter({
92
title: '',
93
data: data.toString(),
94
issueBody: '# Description\nPlease describe the expected outcome and attach a screenshot!',
95
public: !isInternalUser
96
});
97
}));
98
99
this._register(commands.registerCommand(pickProviderId, async (args: unknown) => {
100
if (!this._inlineEditsEnabled.get()) { return; }
101
if (!this._internalActionsEnabled.get()) { return; }
102
103
const selectedProvider = await window.showQuickPick(this._getAvailableProviderIds(), { placeHolder: 'Select inline edits provider' });
104
if (!selectedProvider || selectedProvider === this._inlineEditsProviderId.get()) { return; }
105
106
this._inlineEditsProviderId.set(selectedProvider, undefined);
107
108
const pick = await window.showWarningMessage(`Inline edits provider set to ${selectedProvider}. Reloading will undo this change. Set "github.copilot.${ConfigKey.TeamInternal.InlineEditsProviderId.id}": "${selectedProvider}" in your settings file to make the change persistent.`, 'Open settings (JSON)');
109
if (!pick) { return; }
110
111
await commands.executeCommand('workbench.action.openSettingsJson', { revealSetting: { key: `github.copilot.${ConfigKey.TeamInternal.InlineEditsProviderId.id}`, edit: true } });
112
}));
113
}
114
115
getCommands(logContext: InlineEditRequestLogContext): InlineCompletionCommand[] {
116
const menuCommands: InlineCompletionCommand[] = [];
117
menuCommands.push({
118
command: {
119
command: reportFeedbackCommandId,
120
title: 'Feedback',
121
arguments: [{ logContext }],
122
},
123
icon: new ThemeIcon('feedback')
124
});
125
126
if (this._internalActionsEnabled.get()) {
127
if (this._getAvailableProviderIds().length > 1) {
128
menuCommands.push({
129
command: {
130
command: pickProviderId,
131
title: `Model: ${this._inlineEditsProviderId.get() ?? defaultNextEditProviderId}`,
132
},
133
icon: new ThemeIcon('wand'),
134
});
135
}
136
}
137
138
return menuCommands;
139
}
140
141
private _getAvailableProviderIds(): string[] {
142
const providers = [XtabProvider.ID];
143
144
const providerId = this._inlineEditsProviderId.get();
145
if (providerId && !providers.includes(providerId)) {
146
providers.push(providerId);
147
}
148
149
return providers;
150
}
151
}
152
153
function generateSTest(log: LogEntry[]): string {
154
return TsExpr.str`
155
stest({ description: 'MyTest', language: 'typescript' }, collection => tester.runAndScoreTestFromRecording(collection,
156
loadFile({
157
fileName: "MyTest/recording.w.json",
158
fileContents: ${JSON.stringify({ log })},
159
})
160
));
161
`.toString();
162
}
163
164
/**
165
* Sensitive file patterns that should be filtered from logs to prevent
166
* accidental exposure of secrets, credentials, or private configuration.
167
*/
168
const SENSITIVE_FILE_PATTERNS = {
169
// Exact basename matches (case-insensitive)
170
exactNames: new Set([
171
'settings.json', // VS Code settings
172
'keybindings.json', // VS Code keybindings (may contain custom bindings)
173
'launch.json', // Debug configs often contain env vars with secrets
174
'.npmrc', // npm auth tokens
175
'.netrc', // Network credentials
176
'.htpasswd', // HTTP auth passwords
177
'.gitconfig', // Git config can contain tokens
178
'credentials', // Generic credentials file
179
'credentials.json',
180
'secrets.json',
181
'config.json', // Often contains API keys
182
'password.txt', // Plain text password files
183
'passwords.txt',
184
'password.json',
185
'passwords.json',
186
'token.json', // Token storage files
187
'tokens.json',
188
'token.txt',
189
'tokens.txt',
190
]),
191
192
// File extensions that are sensitive (checked with endsWith)
193
extensions: [
194
'.env', // Files ending with .env (e.g., app.env, local.env)
195
'.pem', // Private keys
196
'.key', // Private keys
197
'.p12', // PKCS#12 certificates
198
'.pfx', // PKCS#12 certificates
199
],
200
201
// Prefixes for dotfiles that are sensitive (e.g., .env, .env.local, .env.production)
202
sensitiveDotfilePrefixes: [
203
'.env', // Environment files (.env, .env.local, .env.development, etc.)
204
],
205
206
// Path segments that indicate sensitive directories
207
sensitivePathSegments: [
208
'.aws', // AWS credentials
209
'.ssh', // SSH keys
210
'.gnupg', // GPG keys
211
'.docker', // Docker config with registry auth
212
],
213
214
// Filename patterns (using includes)
215
patterns: [
216
'id_rsa', // SSH private keys
217
'id_ed25519', // SSH private keys
218
'id_ecdsa', // SSH private keys
219
'id_dsa', // SSH private keys
220
'.secret', // Files with .secret in name
221
'_secret', // Files with _secret in name
222
],
223
};
224
225
/**
226
* Check if a file path represents a sensitive file that should be filtered.
227
*/
228
function isSensitiveFile(relativePath: string): boolean {
229
// Normalize path separators for consistent handling across platforms
230
const normalizedPath = relativePath.replace(/\\/g, '/');
231
const pathParts = normalizedPath.split('/');
232
233
// Use basename/extname on normalized path for robust filename extraction
234
const fileName = basename(normalizedPath);
235
const fileNameLower = fileName.toLowerCase();
236
const fileExt = extname(normalizedPath).toLowerCase();
237
238
// Check exact filename matches (case-insensitive)
239
if (SENSITIVE_FILE_PATTERNS.exactNames.has(fileNameLower)) {
240
return true;
241
}
242
243
// Check file extensions (e.g., .pem, .key, .p12, .pfx, files ending in .env like app.env)
244
for (const ext of SENSITIVE_FILE_PATTERNS.extensions) {
245
if (fileExt === ext || fileNameLower.endsWith(ext)) {
246
return true;
247
}
248
}
249
250
// Check sensitive dotfile prefixes (e.g., .env, .env.local, .env.production)
251
for (const prefix of SENSITIVE_FILE_PATTERNS.sensitiveDotfilePrefixes) {
252
if (fileNameLower === prefix || fileNameLower.startsWith(prefix + '.')) {
253
return true;
254
}
255
}
256
257
// Check sensitive path segments
258
for (const segment of SENSITIVE_FILE_PATTERNS.sensitivePathSegments) {
259
if (pathParts.some(part => part === segment)) {
260
return true;
261
}
262
}
263
264
// Check filename patterns
265
for (const pattern of SENSITIVE_FILE_PATTERNS.patterns) {
266
if (fileNameLower.includes(pattern)) {
267
return true;
268
}
269
}
270
271
return false;
272
}
273
274
export function filterLogForSensitiveFiles(log: LogEntry[]): LogEntry[] {
275
const sensitiveFileIds = new Set<number>();
276
277
const safeEntries: LogEntry[] = [];
278
279
for (const entry of log) {
280
switch (entry.kind) {
281
// safe entry
282
case 'meta':
283
case 'header':
284
case 'applicationStart':
285
case 'event':
286
case 'bookmark':
287
safeEntries.push(entry);
288
break;
289
290
// check if newly encountered document is sensitive
291
// if so, add it to the sensitive file ids
292
// otherwise, add it to the safe entries
293
case 'documentEncountered': {
294
if (isSensitiveFile(entry.relativePath)) {
295
sensitiveFileIds.add(entry.id);
296
} else {
297
safeEntries.push(entry);
298
}
299
break;
300
}
301
302
// ensure the entry doesn't belong to a sensitive file
303
case 'setContent':
304
case 'storeContent':
305
case 'restoreContent':
306
case 'opened':
307
case 'closed':
308
case 'changed':
309
case 'focused':
310
case 'selectionChanged':
311
case 'documentEvent': {
312
if (!sensitiveFileIds.has(entry.id)) {
313
safeEntries.push(entry);
314
}
315
break;
316
}
317
318
default: {
319
assertNever(entry);
320
}
321
}
322
}
323
324
return safeEntries;
325
}
326
327
328
async function extractInlineEditRepro() {
329
const commandId = 'editor.action.inlineSuggest.dev.extractRepro';
330
const result: { reproCase: string } | undefined = await commands.executeCommand(commandId);
331
return result?.reproCase;
332
}
333
334
class SimpleMarkdownBuilder {
335
private readonly _lines: string[] = [];
336
337
constructor() {
338
}
339
340
appendLine(line: string): void {
341
this._lines.push(line);
342
}
343
344
toString(): string {
345
return this._lines.join('\n');
346
}
347
348
appendSection(header: string, lines: string[]): void {
349
this._lines.push(
350
`<details><summary>${header}</summary>`,
351
'', // we need separation between the summary and the content
352
...lines,
353
`</details>`
354
);
355
}
356
}
357
358