Skip to content
This repository was archived by the owner on May 20, 2026. It is now read-only.

autopilot copilot code review - #5013

Draft
Justin Chen (justschen) wants to merge 1 commit into
mainfrom
justin/diggersby
Draft

Justin Chen (justschen) wants to merge 1 commit into
mainfrom
justin/diggersby

Conversation

@justschen

Copy link
Copy Markdown
Contributor
  • automatically starts a code review session if we detect edits

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an “autopilot” flow that can automatically run Copilot code review after the agent has made edits, so the model can address review comments before final completion.

Changes:

  • Extend autopilot ToolCallingLoop to detect edit-tool usage, snapshot pre-edit file contents, and trigger reviewFileChanges on edited files when the task completes.
  • Extend code review inputs to accept baseContent (inline) as an alternative to baseUri, and plumb this through reviewFileChanges.
  • Thread IPromptPathRepresentationService into tool-calling loops to resolve tool-provided file paths into URIs.
Show a summary per file
File Description
src/extension/intents/node/toolCallingLoop.ts Implements autopilot edit detection, pre-edit snapshotting, and automatic code review continuation logic.
src/extension/intents/test/node/toolCallingLoopAutopilot.spec.ts Exposes additional protected members for autopilot-related unit testing.
src/extension/review/node/doReview.ts Allows reviewFileChanges to use provided baseContent and only falls back to baseUri reads when needed.
src/platform/review/common/reviewCommand.ts Extends CodeReviewFileInput to allow passing baseContent directly.
src/platform/configuration/common/configurationService.ts Introduces config key to enable/disable autopilot code review.
src/extension/prompt/node/defaultIntentRequestHandler.ts Updates ToolCallingLoop construction to pass IPromptPathRepresentationService.
src/extension/prompt/node/codebaseToolCalling.ts Updates ToolCallingLoop construction to pass IPromptPathRepresentationService.
src/extension/prompt/node/searchSubagentToolCallingLoop.ts Updates ToolCallingLoop construction to pass IPromptPathRepresentationService.
src/extension/prompt/node/executionSubagentToolCallingLoop.ts Updates ToolCallingLoop construction to pass IPromptPathRepresentationService.
src/extension/mcp/vscode-node/mcpToolCallingLoop.tsx Updates ToolCallingLoop construction to pass IPromptPathRepresentationService.

Copilot's findings

  • Files reviewed: 10/10 changed files
  • Comments generated: 5

Comment on lines +501 to +505
const headerRe = /\*\*\*\s+(?:Add|Update|Delete)\s+File:\s+(.+)/g;
let m: RegExpExecArray | null;
while ((m = headerRe.exec(patchText)) !== null) {
paths.add(m[1].trim());
}

Copilot AI Apr 6, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

apply_patch supports renames via *** Move to: <new path> headers, but extractEditedFilePathsFromRound only captures the *** (Add|Update|Delete) File: path. In rename cases the edited file list (and pre-edit snapshot lookup) can miss the new path, causing the review to either skip the file or diff it against an empty base. Consider also extracting *** Move to: targets (and/or mapping snapshots from old->new path) so renamed files are reviewed correctly.

Suggested change
const headerRe = /\*\*\*\s+(?:Add|Update|Delete)\s+File:\s+(.+)/g;
let m: RegExpExecArray | null;
while ((m = headerRe.exec(patchText)) !== null) {
paths.add(m[1].trim());
}
const fileHeaderRe = /\*\*\*\s+(?:Add|Update|Delete)\s+File:\s+(.+)/g;
const moveToHeaderRe = /\*\*\*\s+Move to:\s+(.+)/g;
let m: RegExpExecArray | null;
while ((m = fileHeaderRe.exec(patchText)) !== null) {
paths.add(m[1].trim());
}
while ((m = moveToHeaderRe.exec(patchText)) !== null) {
paths.add(m[1].trim());
}

Copilot uses AI. Check for mistakes.
Comment on lines +623 to +625
// Mark review as completed so we don't re-run after the fix cycle.
this.autopilotCodeReviewCompleted = true;

Copilot AI Apr 6, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

autopilotCodeReviewCompleted is set to true before the review actually runs. If reviewFileChanges throws or the token is cancelled mid-review, this prevents any subsequent attempt to run the review, effectively skipping it permanently for the session. Consider only setting this flag after a successful review attempt (or resetting it in the catch path) so transient failures don’t disable code review.

Suggested change
// Mark review as completed so we don't re-run after the fix cycle.
this.autopilotCodeReviewCompleted = true;

Copilot uses AI. Check for mistakes.
Comment on lines 9 to 16
/**
* A single file to review, specified by URI pairs.
*/
export interface CodeReviewFileInput {
readonly currentUri: vscode.Uri;
readonly baseUri?: vscode.Uri;
readonly baseContent?: string;
}

Copilot AI Apr 6, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The file header comment says inputs are “specified by URI pairs”, but the interface now also supports baseContent (which can replace baseUri). Update the doc comment to reflect that callers may provide either baseUri or inline baseContent for the base side of the diff.

Copilot uses AI. Check for mistakes.
export const WorkspacePrototypeAdoCodeSearchEndpointOverride = defineAndMigrateSetting<string>('chat.advanced.workspace.prototypeAdoCodeSearchEndpointOverride', 'chat.workspace.prototypeAdoCodeSearchEndpointOverride', '');
export const FeedbackOnChange = defineAndMigrateSetting('chat.advanced.feedback.onChange', 'chat.feedback.onChange', false);
export const ReviewIntent = defineAndMigrateSetting('chat.advanced.review.intent', 'chat.review.intent', false);
export const AutopilotCodeReviewEnabled = defineSetting<boolean>('chat.advanced.autopilotCodeReview.enabled', ConfigType.Simple, false);

Copilot AI Apr 6, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ConfigKey.Advanced.AutopilotCodeReviewEnabled is defined in code, but there is no corresponding contributed setting in package.json (so it won’t show up in Settings UI and will be treated as non-public by defineSetting). If this is intended to be user-configurable/experiment-togglable, add a github.copilot.chat... configuration entry (and consider aligning the key naming with other chat settings that don’t include a .advanced. segment).

Suggested change
export const AutopilotCodeReviewEnabled = defineSetting<boolean>('chat.advanced.autopilotCodeReview.enabled', ConfigType.Simple, false);
export const AutopilotCodeReviewEnabled = defineAndMigrateSetting<boolean>('chat.advanced.autopilotCodeReview.enabled', 'chat.autopilotCodeReview.enabled', false);

Copilot uses AI. Check for mistakes.
Comment on lines +472 to +610
/**
* Returns `true` if any tool call round contains a code-editing tool invocation.
*/
protected hadCodeEdits(): boolean {
return this.toolCallRounds.some(
round => round.toolCalls.some(tc => ToolCallingLoop.EDIT_TOOL_NAMES.has(tc.name))
);
}

/**
* Extracts deduplicated file paths from a single round's edit tool calls.
*/
private static extractEditedFilePathsFromRound(round: IToolCallRound): string[] {
const paths = new Set<string>();
for (const tc of round.toolCalls) {
if (!ToolCallingLoop.EDIT_TOOL_NAMES.has(tc.name)) {
continue;
}
try {
const args = JSON.parse(tc.arguments);
if (tc.name === ToolName.MultiReplaceString) {
const replacements: { filePath?: string }[] = args.replacements ?? [];
for (const r of replacements) {
if (r.filePath) {
paths.add(r.filePath);
}
}
} else if (tc.name === ToolName.ApplyPatch) {
const patchText: string = args.input ?? '';
const headerRe = /\*\*\*\s+(?:Add|Update|Delete)\s+File:\s+(.+)/g;
let m: RegExpExecArray | null;
while ((m = headerRe.exec(patchText)) !== null) {
paths.add(m[1].trim());
}
} else if (args.filePath) {
paths.add(args.filePath);
}
} catch {
// malformed arguments — skip
}
}
return [...paths];
}

/**
* Scans tool call rounds for code-editing tools and extracts the deduplicated
* set of file paths that were edited.
*/
protected getEditedFilePaths(): URI[] {
const paths = new Set<string>();
for (const round of this.toolCallRounds) {
for (const p of ToolCallingLoop.extractEditedFilePathsFromRound(round)) {
paths.add(p);
}
}
return [...paths].flatMap(p => {
try {
return [resolveToolInputPath(p, this._promptPathRepresentationService)];
} catch {
return [];
}
});
}

/**
* After a round's tool calls are known but before they execute (next iteration),
* capture the current file content so we have a pre-edit snapshot for code review.
* Only captures each file once — the first snapshot represents the state before
* autopilot's first edit to that file.
*/
private async capturePreEditSnapshots(round: IToolCallRound): Promise<void> {
if (this.options.request.permissionLevel !== 'autopilot') {
return;
}
const filePaths = ToolCallingLoop.extractEditedFilePathsFromRound(round);
const newEntries: { key: string; uri: URI }[] = [];
for (const p of filePaths) {
try {
const uri = resolveToolInputPath(p, this._promptPathRepresentationService);
const key = uri.fsPath;
if (!this.preEditSnapshots.has(key)) {
newEntries.push({ key, uri });
}
} catch {
// invalid path — skip
}
}
if (!newEntries.length) {
return;
}
await Promise.all(newEntries.map(async ({ key, uri }) => {
try {
const bytes = await this._fileSystemService.readFile(uri);
if (!this.preEditSnapshots.has(key)) {
this.preEditSnapshots.set(key, new TextDecoder().decode(bytes));
}
} catch {
// File doesn't exist yet (e.g. create_file) — record empty string
if (!this.preEditSnapshots.has(key)) {
this.preEditSnapshots.set(key, '');
}
}
}));
}

/**
* Formats code review comments into a continuation message for the model.
*/
private formatReviewCommentsForModel(comments: readonly CodeReviewComment[]): string {
const lines: string[] = [
'Code review found the following issues with your changes. Please address each one:\n',
];
for (const comment of comments) {
const file = comment.uri.fsPath;
const startLine = comment.range.start.line + 1;
const endLine = comment.range.end.line + 1;
const loc = startLine === endLine ? `line ${startLine}` : `lines ${startLine}-${endLine}`;
lines.push(`- **${file}** (${loc}) [${comment.severity}]: ${comment.body}`);
}
lines.push(
'',
'After addressing all review comments, call task_complete with a brief summary of what was fixed.',
);
return lines.join('\n');
}

/**
* Runs code review on files edited during autopilot, if applicable.
* Returns `true` if review comments were found and the loop should continue
* so the model can address them.
*/
protected async performAutopilotCodeReview(
outputStream: ChatResponseStream | undefined,
token: CancellationToken,
): Promise<boolean> {
if (this.options.request.permissionLevel !== 'autopilot') {
return false;
}
if (!this._configurationService.getConfig(ConfigKey.Advanced.AutopilotCodeReviewEnabled)) {

Copilot AI Apr 6, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Autopilot code review introduces non-trivial new behavior (detecting edit tools, snapshotting pre-edit file content, invoking reviewFileChanges, and continuing the loop with formatted comments), but there are no new assertions in the existing ToolCallingLoop Vitest suites to cover these paths. Add unit tests (e.g. in toolCallingLoopAutopilot.spec.ts) covering: edit detection (hadCodeEdits), file path extraction (including apply_patch parsing), snapshot usage, and the "review found comments -> continue" flow.

Copilot uses AI. Check for mistakes.
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants