diff --git a/extensions/copilot/src/extension/agents/vscode-node/editModeAgentProvider.ts b/extensions/copilot/src/extension/agents/vscode-node/editModeAgentProvider.ts new file mode 100644 index 00000000000..1500ead2066 --- /dev/null +++ b/extensions/copilot/src/extension/agents/vscode-node/editModeAgentProvider.ts @@ -0,0 +1,89 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { AGENT_FILE_EXTENSION } from '../../../platform/customInstructions/common/promptTypes'; +import { IVSCodeExtensionContext } from '../../../platform/extContext/common/extensionContext'; +import { IFileSystemService } from '../../../platform/filesystem/common/fileSystemService'; +import { ILogService } from '../../../platform/log/common/logService'; +import { Disposable } from '../../../util/vs/base/common/lifecycle'; +import { AgentConfig, buildAgentMarkdown } from './agentTypes'; + +const BASE_EDIT_MODE_AGENT_CONFIG: AgentConfig = { + name: 'Edit', + description: 'Edit-only mode restricted to the currently active file and any files explicitly attached in the request context.', + argumentHint: 'Describe the edit to apply in the active or attached files', + target: 'vscode', + disableModelInvocation: true, + userInvokable: true, + tools: ['read', 'edit'], + agents: [], + handoffs: [ + { + label: 'Continue with Agent Mode', + agent: 'agent', + prompt: 'You are now switching to Agent Mode, where you can read and edit any file in the codebase. Continue with the task without the previous restrictions of Edit Mode.', + send: true, + }, + ], + body: `You are a focused allowlist editing agent. + +## Rules +- Allowed files are strictly: (1) the currently active file and (2) files explicitly attached in the request context. +- Only read and edit files in that allowlist. +- Create a new file only when the user explicitly asks to create that file. +- Never create, delete, rename, or modify any file outside that allowlist. +- Never propose or use terminal commands. +- If a request requires touching files outside the allowlist, stop and explain that Edit Mode is restricted to the active file plus attached files. + +## Workflow +1. Build the allowed-file set from context: active file + attached files. +2. Confirm every requested edit target is in that allowed-file set before editing, unless it is an explicitly user-requested new file creation. +3. Make the minimum required edits only within allowed files. +4. Summarize exactly what changed and list touched files. +5. If further changes are needed outside the allowlist, suggest switching to Agent Mode to complete the task without restrictions.` +}; + +export class EditModeAgentProvider extends Disposable implements vscode.ChatCustomAgentProvider { + readonly label = vscode.l10n.t('Edit Mode Agent'); + + private static readonly CACHE_DIR = 'edit-mode-agent'; + private static readonly AGENT_FILENAME = `EditMode${AGENT_FILE_EXTENSION}`; + + constructor( + @IVSCodeExtensionContext private readonly _extensionContext: IVSCodeExtensionContext, + @IFileSystemService private readonly _fileSystemService: IFileSystemService, + @ILogService private readonly _logService: ILogService, + ) { + super(); + } + + async provideCustomAgents( + _context: unknown, + _token: vscode.CancellationToken + ): Promise { + const content = buildAgentMarkdown(BASE_EDIT_MODE_AGENT_CONFIG); + const fileUri = await this._writeCacheFile(content); + return [{ uri: fileUri }]; + } + + private async _writeCacheFile(content: string): Promise { + const cacheDir = vscode.Uri.joinPath( + this._extensionContext.globalStorageUri, + EditModeAgentProvider.CACHE_DIR + ); + + try { + await this._fileSystemService.stat(cacheDir); + } catch { + await this._fileSystemService.createDirectory(cacheDir); + } + + const fileUri = vscode.Uri.joinPath(cacheDir, EditModeAgentProvider.AGENT_FILENAME); + await this._fileSystemService.writeFile(fileUri, new TextEncoder().encode(content)); + this._logService.trace(`[EditModeAgentProvider] Wrote agent file: ${fileUri.toString()}`); + return fileUri; + } +} diff --git a/extensions/copilot/src/extension/agents/vscode-node/promptFileContrib.ts b/extensions/copilot/src/extension/agents/vscode-node/promptFileContrib.ts index e1669a660c1..838c981d1c8 100644 --- a/extensions/copilot/src/extension/agents/vscode-node/promptFileContrib.ts +++ b/extensions/copilot/src/extension/agents/vscode-node/promptFileContrib.ts @@ -5,12 +5,13 @@ import * as vscode from 'vscode'; import { ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService'; -import { Disposable } from '../../../util/vs/base/common/lifecycle'; +import { Disposable, MutableDisposable } from '../../../util/vs/base/common/lifecycle'; import { SyncDescriptor } from '../../../util/vs/platform/instantiation/common/descriptors'; import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation'; import { IExtensionContribution } from '../../common/contributions'; import { AgentCustomizationSkillProvider } from './agentCustomizationSkillProvider'; import { AskAgentProvider } from './askAgentProvider'; +import { EditModeAgentProvider } from './editModeAgentProvider'; import { ExploreAgentProvider } from './exploreAgentProvider'; import { GitHubOrgCustomAgentProvider } from './githubOrgCustomAgentProvider'; import { GitHubOrgInstructionsProvider } from './githubOrgInstructionsProvider'; @@ -27,6 +28,26 @@ export class PromptFileContribution extends Disposable implements IExtensionCont // Register custom agent provider if ('registerCustomAgentProvider' in vscode.chat) { + const editModeProviderRegistration = this._register(new MutableDisposable()); + const editModeHiddenSetting = 'chat.editMode.hidden'; + const updateEditModeProvider = () => { + const isEditModeHidden = configurationService.getNonExtensionConfig(editModeHiddenSetting); + if (!isEditModeHidden) { + if (!editModeProviderRegistration.value) { + editModeProviderRegistration.value = vscode.chat.registerCustomAgentProvider(instantiationService.createInstance(EditModeAgentProvider)); + } + } else { + editModeProviderRegistration.clear(); + } + }; + + updateEditModeProvider(); + this._register(configurationService.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(editModeHiddenSetting)) { + updateEditModeProvider(); + } + })); + // Only register the provider if the setting is enabled if (configurationService.getConfig(ConfigKey.EnableOrganizationCustomAgents)) { const githubOrgAgentProvider: vscode.ChatCustomAgentProvider = instantiationService.createInstance(new SyncDescriptor(GitHubOrgCustomAgentProvider));