Path: blob/main/src/vs/workbench/contrib/chat/browser/promptSyntax/promptUrlHandler.ts
3296 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*--------------------------------------------------------------------------------------------*/45import { streamToBuffer, VSBuffer } from '../../../../../base/common/buffer.js';6import { CancellationToken } from '../../../../../base/common/cancellation.js';7import { Disposable } from '../../../../../base/common/lifecycle.js';8import { URI } from '../../../../../base/common/uri.js';9import { IFileService } from '../../../../../platform/files/common/files.js';10import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';11import { INotificationService } from '../../../../../platform/notification/common/notification.js';12import { IOpenerService } from '../../../../../platform/opener/common/opener.js';13import { IRequestService } from '../../../../../platform/request/common/request.js';14import { IURLHandler, IURLService } from '../../../../../platform/url/common/url.js';15import { IWorkbenchContribution } from '../../../../common/contributions.js';16import { askForPromptFileName } from './pickers/askForPromptName.js';17import { askForPromptSourceFolder } from './pickers/askForPromptSourceFolder.js';18import { getCleanPromptName } from '../../common/promptSyntax/config/promptFileLocations.js';19import { PromptsType } from '../../common/promptSyntax/promptTypes.js';20import { ILogService } from '../../../../../platform/log/common/log.js';21import { localize } from '../../../../../nls.js';22import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js';23import { Schemas } from '../../../../../base/common/network.js';24import { MarkdownString } from '../../../../../base/common/htmlContent.js';25import { IHostService } from '../../../../services/host/browser/host.js';26import { mainWindow } from '../../../../../base/browser/window.js';2728// example URL: code-oss:chat-prompt/install?url=https://gist.githubusercontent.com/aeschli/43fe78babd5635f062aef0195a476aad/raw/dfd71f60058a4dd25f584b55de3e20f5fd580e63/filterEvenNumbers.prompt.md2930export class PromptUrlHandler extends Disposable implements IWorkbenchContribution, IURLHandler {3132static readonly ID = 'workbench.contrib.promptUrlHandler';3334constructor(35@IURLService urlService: IURLService,36@INotificationService private readonly notificationService: INotificationService,37@IRequestService private readonly requestService: IRequestService,38@IInstantiationService private readonly instantiationService: IInstantiationService,39@IFileService private readonly fileService: IFileService,40@IOpenerService private readonly openerService: IOpenerService,41@ILogService private readonly logService: ILogService,42@IDialogService private readonly dialogService: IDialogService,4344@IHostService private readonly hostService: IHostService,45) {46super();47this._register(urlService.registerHandler(this));48}4950async handleURL(uri: URI): Promise<boolean> {51let promptType: PromptsType | undefined;52switch (uri.path) {53case 'chat-prompt/install':54promptType = PromptsType.prompt;55break;56case 'chat-instructions/install':57promptType = PromptsType.instructions;58break;59case 'chat-mode/install':60promptType = PromptsType.mode;61break;62default:63return false;64}6566try {67const query = decodeURIComponent(uri.query);68if (!query || !query.startsWith('url=')) {69return true;70}7172const urlString = query.substring(4);73const url = URI.parse(urlString);74if (url.scheme !== Schemas.https && url.scheme !== Schemas.http) {75this.logService.error(`[PromptUrlHandler] Invalid URL: ${urlString}`);76return true;77}7879await this.hostService.focus(mainWindow);8081if (await this.shouldBlockInstall(promptType, url)) {82return true;83}8485const result = await this.requestService.request({ type: 'GET', url: urlString }, CancellationToken.None);86if (result.res.statusCode !== 200) {87this.logService.error(`[PromptUrlHandler] Failed to fetch URL: ${urlString}`);88this.notificationService.error(localize('failed', 'Failed to fetch URL: {0}', urlString));89return true;90}9192const responseData = (await streamToBuffer(result.stream)).toString();9394const newFolder = await this.instantiationService.invokeFunction(askForPromptSourceFolder, promptType);95if (!newFolder) {96return true;97}9899const newName = await this.instantiationService.invokeFunction(askForPromptFileName, promptType, newFolder.uri, getCleanPromptName(url));100if (!newName) {101return true;102}103104const promptUri = URI.joinPath(newFolder.uri, newName);105106await this.fileService.createFolder(newFolder.uri);107await this.fileService.createFile(promptUri, VSBuffer.fromString(responseData));108109await this.openerService.open(promptUri);110return true;111112} catch (error) {113this.logService.error(`Error handling prompt URL ${uri.toString()}`, error);114return true;115}116}117118private async shouldBlockInstall(promptType: PromptsType, url: URI): Promise<boolean> {119let uriLabel = url.toString();120if (uriLabel.length > 50) {121uriLabel = `${uriLabel.substring(0, 35)}...${uriLabel.substring(uriLabel.length - 15)}`;122}123124const detail = new MarkdownString('', { supportHtml: true });125detail.appendMarkdown(localize('confirmOpenDetail2', "This will access {0}.\n\n", `[${uriLabel}](${url.toString()})`));126detail.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'"));127128let message: string;129switch (promptType) {130case PromptsType.prompt:131message = 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?");132break;133case PromptsType.instructions:134message = 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?");135break;136default:137message = 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?");138break;139}140141const { confirmed } = await this.dialogService.confirm({142type: 'warning',143primaryButton: localize({ key: 'yesButton', comment: ['&& denotes a mnemonic'] }, "&&Yes"),144cancelButton: localize('noButton', "No"),145message,146custom: {147markdownDetails: [{148markdown: detail149}]150}151});152153return !confirmed;154155}156}157158159