mirror of
https://github.com/microsoft/vscode.git
synced 2025-12-11 10:13:36 -06:00
Linkable chat prompts/instructions/modes using vscode:// links (#252441)
* Linkable chat prompts/instructions/modes using vscode:// links * update * update * update * update * add confirmation dialog * always show folder selection * improve wording * update
This commit is contained in:
parent
1a9be6ebcf
commit
be0a02753f
@ -111,6 +111,7 @@ import { runSaveToPromptAction, SAVE_TO_PROMPT_SLASH_COMMAND_NAME } from './prom
|
||||
import { ChatDynamicVariableModel } from './contrib/chatDynamicVariables.js';
|
||||
import { ChatAttachmentResolveService, IChatAttachmentResolveService } from './chatAttachmentResolveService.js';
|
||||
import { registerLanguageModelActions } from './actions/chatLanguageModelActions.js';
|
||||
import { PromptUrlHandler } from './promptSyntax/promptUrlHandler.js';
|
||||
|
||||
// Register configuration
|
||||
const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);
|
||||
@ -711,6 +712,7 @@ registerWorkbenchContribution2(ChatEditingEditorContextKeys.ID, ChatEditingEdito
|
||||
registerWorkbenchContribution2(ChatTransferContribution.ID, ChatTransferContribution, WorkbenchPhase.BlockRestore);
|
||||
registerWorkbenchContribution2(ChatContextContributions.ID, ChatContextContributions, WorkbenchPhase.AfterRestored);
|
||||
registerWorkbenchContribution2(ChatResponseResourceFileSystemProvider.ID, ChatResponseResourceFileSystemProvider, WorkbenchPhase.AfterRestored);
|
||||
registerWorkbenchContribution2(PromptUrlHandler.ID, PromptUrlHandler, WorkbenchPhase.BlockRestore);
|
||||
|
||||
registerChatActions();
|
||||
registerChatCopyActions();
|
||||
|
||||
@ -44,11 +44,6 @@ export async function askForPromptSourceFolder(
|
||||
return;
|
||||
}
|
||||
|
||||
// if there is only one folder and it's for new, no need to ask
|
||||
if (!existingFolder && folders.length === 1) {
|
||||
return folders[0];
|
||||
}
|
||||
|
||||
const pickOptions: IPickOptions<IFolderQuickPickItem> = {
|
||||
placeHolder: existingFolder ? getPlaceholderStringforMove(type, isMove) : getPlaceholderStringforNew(type),
|
||||
canPickMany: false,
|
||||
|
||||
@ -0,0 +1,166 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { streamToBuffer, VSBuffer } from '../../../../../base/common/buffer.js';
|
||||
import { CancellationToken } from '../../../../../base/common/cancellation.js';
|
||||
import { Disposable } from '../../../../../base/common/lifecycle.js';
|
||||
import { URI } from '../../../../../base/common/uri.js';
|
||||
import { IFileService } from '../../../../../platform/files/common/files.js';
|
||||
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
|
||||
import { INotificationService } from '../../../../../platform/notification/common/notification.js';
|
||||
import { IOpenerService } from '../../../../../platform/opener/common/opener.js';
|
||||
import { IRequestService } from '../../../../../platform/request/common/request.js';
|
||||
import { IURLHandler, IURLService } from '../../../../../platform/url/common/url.js';
|
||||
import { IWorkbenchContribution } from '../../../../common/contributions.js';
|
||||
import { askForPromptFileName } from './pickers/askForPromptName.js';
|
||||
import { askForPromptSourceFolder } from './pickers/askForPromptSourceFolder.js';
|
||||
import { getCleanPromptName } from '../../common/promptSyntax/config/promptFileLocations.js';
|
||||
import { PromptsType } from '../../common/promptSyntax/promptTypes.js';
|
||||
import { ILogService } from '../../../../../platform/log/common/log.js';
|
||||
import { localize } from '../../../../../nls.js';
|
||||
import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js';
|
||||
import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js';
|
||||
import { Schemas } from '../../../../../base/common/network.js';
|
||||
import { MarkdownString } from '../../../../../base/common/htmlContent.js';
|
||||
|
||||
// example URL: code-oss:chat-prompt/install?url=https://gist.githubusercontent.com/aeschli/43fe78babd5635f062aef0195a476aad/raw/dfd71f60058a4dd25f584b55de3e20f5fd580e63/filterEvenNumbers.prompt.md
|
||||
|
||||
export class PromptUrlHandler extends Disposable implements IWorkbenchContribution, IURLHandler {
|
||||
|
||||
static readonly ID = 'workbench.contrib.promptUrlHandler';
|
||||
|
||||
static readonly CONFIRM_INSTALL_STORAGE_KEY = 'security.promptForPromptProtocolHandling';
|
||||
|
||||
constructor(
|
||||
@IURLService urlService: IURLService,
|
||||
@INotificationService private readonly notificationService: INotificationService,
|
||||
@IRequestService private readonly requestService: IRequestService,
|
||||
@IInstantiationService private readonly instantiationService: IInstantiationService,
|
||||
@IFileService private readonly fileService: IFileService,
|
||||
@IOpenerService private readonly openerService: IOpenerService,
|
||||
@ILogService private readonly logService: ILogService,
|
||||
@IDialogService private readonly dialogService: IDialogService,
|
||||
@IStorageService private readonly storageService: IStorageService,
|
||||
) {
|
||||
super();
|
||||
this._register(urlService.registerHandler(this));
|
||||
}
|
||||
|
||||
async handleURL(uri: URI): Promise<boolean> {
|
||||
let promptType: PromptsType | undefined;
|
||||
switch (uri.path) {
|
||||
case 'chat-prompt/install':
|
||||
promptType = PromptsType.prompt;
|
||||
break;
|
||||
case 'chat-instructions/install':
|
||||
promptType = PromptsType.instructions;
|
||||
break;
|
||||
case 'chat-mode/install':
|
||||
promptType = PromptsType.mode;
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
const query = decodeURIComponent(uri.query);
|
||||
if (!query || !query.startsWith('url=')) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const urlString = query.substring(4);
|
||||
const url = URI.parse(urlString);
|
||||
if (url.scheme !== Schemas.https && url.scheme !== Schemas.http) {
|
||||
this.logService.error(`[PromptUrlHandler] Invalid URL: ${urlString}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (await this.shouldBlockInstall(promptType, url)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const result = await this.requestService.request({ type: 'GET', url: urlString }, CancellationToken.None);
|
||||
if (result.res.statusCode !== 200) {
|
||||
this.logService.error(`[PromptUrlHandler] Failed to fetch URL: ${urlString}`);
|
||||
this.notificationService.error(localize('failed', 'Failed to fetch URL: {0}', urlString));
|
||||
return false;
|
||||
}
|
||||
|
||||
const responseData = (await streamToBuffer(result.stream)).toString();
|
||||
|
||||
const newFolder = await this.instantiationService.invokeFunction(askForPromptSourceFolder, promptType);
|
||||
if (!newFolder) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const newName = await this.instantiationService.invokeFunction(askForPromptFileName, promptType, newFolder.uri, getCleanPromptName(url));
|
||||
if (!newName) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const promptUri = URI.joinPath(newFolder.uri, newName);
|
||||
|
||||
await this.fileService.createFolder(newFolder.uri);
|
||||
await this.fileService.createFile(promptUri, VSBuffer.fromString(responseData));
|
||||
|
||||
await this.openerService.open(promptUri);
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
this.logService.error(`Error handling prompt URL ${uri.toString()}`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async shouldBlockInstall(promptType: PromptsType, url: URI): Promise<boolean> {
|
||||
const location = url.with({ path: url.path.substring(0, url.path.indexOf('/', 1) + 1), query: undefined, fragment: undefined }).toString();
|
||||
const key = PromptUrlHandler.CONFIRM_INSTALL_STORAGE_KEY + '-' + location;
|
||||
|
||||
if (this.storageService.getBoolean(key, StorageScope.APPLICATION, false)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let uriLabel = url.toString();
|
||||
if (uriLabel.length > 50) {
|
||||
uriLabel = `${uriLabel.substring(0, 35)}...${uriLabel.substring(uriLabel.length - 15)}`;
|
||||
}
|
||||
|
||||
const detail = new MarkdownString('', { supportHtml: true });
|
||||
detail.appendMarkdown(localize('confirmOpenDetail2', "This will access {0}.\n\n", `[${uriLabel}](${url.toString()})`));
|
||||
detail.appendMarkdown(localize('confirmOpenDetail1', "Do you want to continue by selecting a destination folder and name?\n\n"));
|
||||
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 'Cancel'"));
|
||||
|
||||
let message: string;
|
||||
switch (promptType) {
|
||||
case PromptsType.prompt:
|
||||
message = localize('confirmInstallPrompt', "An external application wants to create a prompt file with content from a URL.");
|
||||
break;
|
||||
case PromptsType.instructions:
|
||||
message = localize('confirmInstallInstructions', "An external application wants to create an instructions file with content from a URL.");
|
||||
break;
|
||||
default:
|
||||
message = localize('confirmInstallMode', "An external application wants to create a chat mode with content from a URL.");
|
||||
break;
|
||||
}
|
||||
|
||||
const { confirmed, checkboxChecked } = await this.dialogService.confirm({
|
||||
type: 'warning',
|
||||
primaryButton: localize({ key: 'confirmOpenButton', comment: ['&& denotes a mnemonic'] }, "&&Continue"),
|
||||
message,
|
||||
checkbox: { label: localize('confirmOpenDoNotAskAgain', "Do not show this message again for files from '{0}'", location) },
|
||||
custom: {
|
||||
markdownDetails: [{
|
||||
markdown: detail
|
||||
}]
|
||||
}
|
||||
});
|
||||
|
||||
if (checkboxChecked) {
|
||||
this.storageService.store(key, true, StorageScope.APPLICATION, StorageTarget.MACHINE);
|
||||
}
|
||||
return !confirmed;
|
||||
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user