Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/promptSyntax/promptUrlHandler.ts
3296 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 { streamToBuffer, VSBuffer } from '../../../../../base/common/buffer.js';
7
import { CancellationToken } from '../../../../../base/common/cancellation.js';
8
import { Disposable } from '../../../../../base/common/lifecycle.js';
9
import { URI } from '../../../../../base/common/uri.js';
10
import { IFileService } from '../../../../../platform/files/common/files.js';
11
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
12
import { INotificationService } from '../../../../../platform/notification/common/notification.js';
13
import { IOpenerService } from '../../../../../platform/opener/common/opener.js';
14
import { IRequestService } from '../../../../../platform/request/common/request.js';
15
import { IURLHandler, IURLService } from '../../../../../platform/url/common/url.js';
16
import { IWorkbenchContribution } from '../../../../common/contributions.js';
17
import { askForPromptFileName } from './pickers/askForPromptName.js';
18
import { askForPromptSourceFolder } from './pickers/askForPromptSourceFolder.js';
19
import { getCleanPromptName } from '../../common/promptSyntax/config/promptFileLocations.js';
20
import { PromptsType } from '../../common/promptSyntax/promptTypes.js';
21
import { ILogService } from '../../../../../platform/log/common/log.js';
22
import { localize } from '../../../../../nls.js';
23
import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js';
24
import { Schemas } from '../../../../../base/common/network.js';
25
import { MarkdownString } from '../../../../../base/common/htmlContent.js';
26
import { IHostService } from '../../../../services/host/browser/host.js';
27
import { mainWindow } from '../../../../../base/browser/window.js';
28
29
// example URL: code-oss:chat-prompt/install?url=https://gist.githubusercontent.com/aeschli/43fe78babd5635f062aef0195a476aad/raw/dfd71f60058a4dd25f584b55de3e20f5fd580e63/filterEvenNumbers.prompt.md
30
31
export class PromptUrlHandler extends Disposable implements IWorkbenchContribution, IURLHandler {
32
33
static readonly ID = 'workbench.contrib.promptUrlHandler';
34
35
constructor(
36
@IURLService urlService: IURLService,
37
@INotificationService private readonly notificationService: INotificationService,
38
@IRequestService private readonly requestService: IRequestService,
39
@IInstantiationService private readonly instantiationService: IInstantiationService,
40
@IFileService private readonly fileService: IFileService,
41
@IOpenerService private readonly openerService: IOpenerService,
42
@ILogService private readonly logService: ILogService,
43
@IDialogService private readonly dialogService: IDialogService,
44
45
@IHostService private readonly hostService: IHostService,
46
) {
47
super();
48
this._register(urlService.registerHandler(this));
49
}
50
51
async handleURL(uri: URI): Promise<boolean> {
52
let promptType: PromptsType | undefined;
53
switch (uri.path) {
54
case 'chat-prompt/install':
55
promptType = PromptsType.prompt;
56
break;
57
case 'chat-instructions/install':
58
promptType = PromptsType.instructions;
59
break;
60
case 'chat-mode/install':
61
promptType = PromptsType.mode;
62
break;
63
default:
64
return false;
65
}
66
67
try {
68
const query = decodeURIComponent(uri.query);
69
if (!query || !query.startsWith('url=')) {
70
return true;
71
}
72
73
const urlString = query.substring(4);
74
const url = URI.parse(urlString);
75
if (url.scheme !== Schemas.https && url.scheme !== Schemas.http) {
76
this.logService.error(`[PromptUrlHandler] Invalid URL: ${urlString}`);
77
return true;
78
}
79
80
await this.hostService.focus(mainWindow);
81
82
if (await this.shouldBlockInstall(promptType, url)) {
83
return true;
84
}
85
86
const result = await this.requestService.request({ type: 'GET', url: urlString }, CancellationToken.None);
87
if (result.res.statusCode !== 200) {
88
this.logService.error(`[PromptUrlHandler] Failed to fetch URL: ${urlString}`);
89
this.notificationService.error(localize('failed', 'Failed to fetch URL: {0}', urlString));
90
return true;
91
}
92
93
const responseData = (await streamToBuffer(result.stream)).toString();
94
95
const newFolder = await this.instantiationService.invokeFunction(askForPromptSourceFolder, promptType);
96
if (!newFolder) {
97
return true;
98
}
99
100
const newName = await this.instantiationService.invokeFunction(askForPromptFileName, promptType, newFolder.uri, getCleanPromptName(url));
101
if (!newName) {
102
return true;
103
}
104
105
const promptUri = URI.joinPath(newFolder.uri, newName);
106
107
await this.fileService.createFolder(newFolder.uri);
108
await this.fileService.createFile(promptUri, VSBuffer.fromString(responseData));
109
110
await this.openerService.open(promptUri);
111
return true;
112
113
} catch (error) {
114
this.logService.error(`Error handling prompt URL ${uri.toString()}`, error);
115
return true;
116
}
117
}
118
119
private async shouldBlockInstall(promptType: PromptsType, url: URI): Promise<boolean> {
120
let uriLabel = url.toString();
121
if (uriLabel.length > 50) {
122
uriLabel = `${uriLabel.substring(0, 35)}...${uriLabel.substring(uriLabel.length - 15)}`;
123
}
124
125
const detail = new MarkdownString('', { supportHtml: true });
126
detail.appendMarkdown(localize('confirmOpenDetail2', "This will access {0}.\n\n", `[${uriLabel}](${url.toString()})`));
127
detail.appendMarkdown(localize('confirmOpenDetail3', "If you did not initiate this request, it may represent an attempted attack on your system. Unless you took an explicit action to initiate this request, you should press 'No'"));
128
129
let message: string;
130
switch (promptType) {
131
case PromptsType.prompt:
132
message = localize('confirmInstallPrompt', "An external application wants to create a prompt file with content from a URL. Do you want to continue by selecting a destination folder and name?");
133
break;
134
case PromptsType.instructions:
135
message = localize('confirmInstallInstructions', "An external application wants to create an instructions file with content from a URL. Do you want to continue by selecting a destination folder and name?");
136
break;
137
default:
138
message = localize('confirmInstallMode', "An external application wants to create a chat mode with content from a URL. Do you want to continue by selecting a destination folder and name?");
139
break;
140
}
141
142
const { confirmed } = await this.dialogService.confirm({
143
type: 'warning',
144
primaryButton: localize({ key: 'yesButton', comment: ['&& denotes a mnemonic'] }, "&&Yes"),
145
cancelButton: localize('noButton', "No"),
146
message,
147
custom: {
148
markdownDetails: [{
149
markdown: detail
150
}]
151
}
152
});
153
154
return !confirmed;
155
156
}
157
}
158
159