Files
vscode/extensions/copilot/src/extension/tools/node/insertEditTool.tsx
T
Alex Dima 27ecfb15cf Thread session workingDirectory through tools, prompts, and confirmations
In the agents window, each chat session has its own working directory
that may differ from the current workspace folders (which change when
switching between sessions). This caused tools to search the wrong
folder, show spurious 'Allow reading external files?' prompts, and
render incorrect workspace_info in the system prompt.

Core plumbing:
- Add workingDirectory to IToolInvocationContext, IToolInvocationPreparationContext,
  ILanguageModelToolConfirmationRef, and IChatAgentRequest
- Enrich tool invocation context from model.workingDirectory in invokeTool()
- Include workingDirectory in toolInvocationToken built in extHostTypeConverters
- Pass workingDirectory through LanguageModelToolInvocationOptions and
  LanguageModelToolInvocationPrepareOptions (proposed API)
- Revive workingDirectory URI in extHostLanguageModelTools

Tool fixes (when workingDirectory is set, use it exclusively):
- chatExternalPathConfirmation: auto-approve paths within workingDirectory
- isFileExternalAndNeedsConfirmation / isDirExternalAndNeedsConfirmation /
  assertFileOkForTool: treat workingDirectory as workspace-internal
- createEditConfirmation: use workingDirectory for edit trust checks
- All edit tools (create_file, replace_string, multi_replace, apply_patch,
  insert_edit, edit_notebook, create_directory): pass workingDirectory
- resolveToolUri: resolve relative paths against workingDirectory
- inputGlobToPattern: scope unscoped globs to workingDirectory
- file_search / grep_search: scope searches to workingDirectory
- semantic_search: prefer workingDirectory for cwd
- run_in_terminal: prefer workingDirectory for terminal cwd
- fetchPageTool: check workingDirectory for file URI trust
- readFileTool / listDirTool / viewImageTool: pass workingDirectory

Prompt fixes:
- WorkspaceFoldersHint: show workingDirectory instead of workspace folders
- AgentMultirootWorkspaceStructure: generate file tree from workingDirectory
2026-05-08 16:15:11 +02:00

141 lines
6.8 KiB
TypeScript

/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import type * as vscode from 'vscode';
import { NotebookDocumentSnapshot } from '../../../platform/editing/common/notebookDocumentSnapshot';
import { IEndpointProvider } from '../../../platform/endpoint/common/endpointProvider';
import { ILanguageDiagnosticsService } from '../../../platform/languages/common/languageDiagnosticsService';
import { ILogService } from '../../../platform/log/common/logService';
import { IAlternativeNotebookContentService } from '../../../platform/notebook/common/alternativeContent';
import { INotebookService } from '../../../platform/notebook/common/notebookService';
import { IPromptPathRepresentationService } from '../../../platform/prompts/common/promptPathRepresentationService';
import { ITelemetryService } from '../../../platform/telemetry/common/telemetry';
import { IWorkspaceService } from '../../../platform/workspace/common/workspaceService';
import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';
import { LanguageModelPromptTsxPart, LanguageModelToolResult } from '../../../vscodeTypes';
import { IBuildPromptContext } from '../../prompt/common/intents';
import { renderPromptElementJSON } from '../../prompts/node/base/promptRenderer';
import { IEditToolLearningService } from '../common/editToolLearningService';
import { ToolName } from '../common/toolNames';
import { ICopilotTool, ToolRegistry } from '../common/toolsRegistry';
import { IToolsService } from '../common/toolsService';
import { ActionType } from './applyPatch/parser';
import { EditFileResult } from './editFileToolResult';
import { createEditConfirmation, getDisallowedEditUriError, logEditToolResult } from './editFileToolUtils';
import { sendEditNotebookTelemetry } from './editNotebookTool';
import { assertFileNotContentExcluded } from './toolUtils';
export interface IEditFileParams {
explanation: string;
filePath: string;
code: string;
}
export const InternalEditToolId = 'vscode_editFile_internal';
export class EditFileTool implements ICopilotTool<IEditFileParams> {
public static toolName = ToolName.EditFile;
public static readonly nonDeferred = true;
private promptContext?: IBuildPromptContext;
constructor(
@IPromptPathRepresentationService protected readonly promptPathRepresentationService: IPromptPathRepresentationService,
@IInstantiationService protected readonly instantiationService: IInstantiationService,
@IWorkspaceService protected readonly workspaceService: IWorkspaceService,
@IToolsService protected readonly toolsService: IToolsService,
@INotebookService protected readonly notebookService: INotebookService,
@ILanguageDiagnosticsService protected readonly languageDiagnosticsService: ILanguageDiagnosticsService,
@IAlternativeNotebookContentService private readonly alternativeNotebookContentService: IAlternativeNotebookContentService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
@IEndpointProvider private readonly endpointProvider: IEndpointProvider,
@IEditToolLearningService private readonly editToolLearningService: IEditToolLearningService,
@ILogService private readonly logService: ILogService,
) { }
async invoke(options: vscode.LanguageModelToolInvocationOptions<IEditFileParams>, token: vscode.CancellationToken) {
const uri = this.promptPathRepresentationService.resolveFilePath(options.input.filePath);
if (!uri) {
throw new Error(`Invalid file path`);
}
const disallowedUriError = getDisallowedEditUriError(uri, this.promptContext?.allowedEditUris, this.promptPathRepresentationService);
if (disallowedUriError) {
throw new Error(disallowedUriError);
}
await this.instantiationService.invokeFunction(accessor => assertFileNotContentExcluded(accessor, uri));
const existingDiagnostics = this.languageDiagnosticsService.getDiagnostics(uri);
// Wait for vscode to do the edit, call the codemapper service, wait for textedits to be applied
const internalOptions = {
...options,
input: {
...options.input,
uri
}
};
try {
await this.toolsService.invokeTool(InternalEditToolId, internalOptions, token);
void this.recordEditSuccess(options, true);
} catch (error) {
void this.recordEditSuccess(options, false);
throw error;
}
const isNotebook = this.notebookService.hasSupportedNotebooks(uri);
const document = isNotebook ?
await this.workspaceService.openNotebookDocumentAndSnapshot(uri, this.alternativeNotebookContentService.getFormat(this.promptContext?.request?.model)) :
await this.workspaceService.openTextDocumentAndSnapshot(uri);
if (document instanceof NotebookDocumentSnapshot) {
sendEditNotebookTelemetry(this.telemetryService, this.endpointProvider, 'insertEdit', uri, this.promptContext?.requestId, options.model ?? this.promptContext?.request?.model);
}
// Then fill in the tool result
return new LanguageModelToolResult([
new LanguageModelPromptTsxPart(
await renderPromptElementJSON(
this.instantiationService,
EditFileResult,
{ files: [{ operation: ActionType.UPDATE, uri, isNotebook, existingDiagnostics }], toolName: ToolName.EditFile, requestId: options.chatRequestId, model: options.model },
// If we are not called with tokenization options, have _some_ fake tokenizer
// otherwise we end up returning the entire document
options.tokenizationOptions ?? {
tokenBudget: 1000,
countTokens: (t) => Promise.resolve(t.length * 3 / 4)
},
token,
),
)
]);
}
prepareInvocation(options: vscode.LanguageModelToolInvocationPrepareOptions<IEditFileParams>, token: vscode.CancellationToken): vscode.ProviderResult<vscode.PreparedToolInvocation> {
const uri = this.promptPathRepresentationService.resolveFilePath(options.input.filePath);
return this.instantiationService.invokeFunction(
createEditConfirmation,
uri ? [uri] : [],
this.promptContext?.allowedEditUris,
async () => '```\n' + options.input.code + '\n```',
options.forceConfirmationReason,
undefined,
options.workingDirectory,
);
}
async resolveInput(input: IEditFileParams, promptContext: IBuildPromptContext): Promise<IEditFileParams> {
this.promptContext = promptContext;
return input;
}
private recordEditSuccess(options: vscode.LanguageModelToolInvocationOptions<IEditFileParams>, success: boolean) {
if (options.model) {
this.editToolLearningService.didMakeEdit(options.model, ToolName.EditFile, success);
}
logEditToolResult(this.logService, options.chatRequestId, { input: options.input, success });
}
}
ToolRegistry.registerTool(EditFileTool);