Skip to content

feat(copilot/chat): make /review a first-class chat command#320029

Draft
andysharman wants to merge 1 commit into
microsoft:mainfrom
andysharman:andysharman/ccr-progress-ux
Draft

feat(copilot/chat): make /review a first-class chat command#320029
andysharman wants to merge 1 commit into
microsoft:mainfrom
andysharman:andysharman/ccr-progress-ux

Conversation

@andysharman
Copy link
Copy Markdown
Contributor

Unhides the /review slash command and adds a chat-driven entry point from the existing SCM gutter buttons. The chat path:

  • accepts scope arguments: staged, unstaged, all, selection, file
  • streams findings into the Comments panel with a task-bound spinner
  • renders a per-file findings summary with clickable links
  • provides a 'Next Comment' button for keyboard-free navigation
  • routes the SCM gutter buttons through /review so the chat thread is the canonical entry point and preserves the user's current chat mode (agent/ask)

Existing non-chat flows (selection review via Ctrl+I, per-file SCM context menu, codeReview.run) continue to use the legacy direct-to- Comments-panel path with their original auto-jump-to-first-finding behaviour, gated by an opts.suppressAutoReveal flag on IReviewService.addReviewComments.

Copilot AI review requested due to automatic review settings June 5, 2026 00:45
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

This PR reroutes the /review intent for chat (Panel) to a new narrative-driven flow that calls githubReview() directly, integrates with the VS Code Comments panel, and adds chat controls to navigate/reveal review findings.

Changes:

  • Extended IReviewService to support suppressing auto-reveal when adding comments and to expose the currently active review thread.
  • Implemented Panel chat /review execution in reviewIntent.ts (scope parsing, pre-clear behavior, streaming comment adds, summary rendering, cancellation/rollback behavior).
  • Added new chat commands for “Next Comment” and “Reveal Comment” plus updated command wiring to open chat with /review ... queries.
Show a summary per file
File Description
extensions/copilot/src/platform/test/node/simulationWorkspaceServices.ts Updates test double to match IReviewService API additions.
extensions/copilot/src/platform/review/vscode/reviewServiceImpl.ts Adds suppressible auto-reveal and exposes active thread for chat navigation.
extensions/copilot/src/platform/review/common/reviewService.ts Extends IReviewService API (opts + getActiveThread()).
extensions/copilot/src/extension/review/node/test/doReview.spec.ts Aligns tests with updated review service API.
extensions/copilot/src/extension/review/node/githubReviewAgent.ts Minor signature formatting change (trailing comma).
extensions/copilot/src/extension/intents/node/test/reviewIntent.spec.ts Adds coverage for Panel /review narrative + Comments-panel integration behaviors.
extensions/copilot/src/extension/intents/node/reviewIntent.ts Implements Panel /review flow, summary markdown, and scope parsing.
extensions/copilot/src/extension/inlineChat/vscode-node/inlineChatCommands.ts Rewires review commands to open chat queries; adds chat navigation/reveal commands.
extensions/copilot/package.json Removes when gating for review slash-command contribution.

Copilot's findings

  • Files reviewed: 9/9 changed files
  • Comments generated: 3

Comment on lines +342 to +391
function buildFindingsByFileSummary(included: readonly ReviewComment[], excluded: readonly ReviewComment[], workspaceService: IWorkspaceService): MarkdownString {
type GroupEntry = { comment: ReviewComment; isExcluded: boolean };
const groups = new Map<string, { relPath: string; entries: GroupEntry[] }>();
const addEntry = (comment: ReviewComment, isExcluded: boolean): void => {
const key = comment.uri.toString();
let group = groups.get(key);
if (!group) {
group = { relPath: workspaceService.asRelativePath(comment.uri), entries: [] };
groups.set(key, group);
}
group.entries.push({ comment, isExcluded });
};
for (const c of included) {
addEntry(c, false);
}
for (const c of excluded) {
addEntry(c, true);
}
const orderedGroups = [...groups.values()].sort((a, b) =>
a.relPath.localeCompare(b.relPath, undefined, { sensitivity: 'base' })
);
const lines: string[] = [];
lines.push(`**${l10n.t('Findings by file')}**`);
lines.push('');
for (const group of orderedGroups) {
const entries = group.entries.slice().sort((a, b) => {
const lineDiff = a.comment.range.start.line - b.comment.range.start.line;
if (lineDiff !== 0) {
return lineDiff;
}
return a.comment.range.start.character - b.comment.range.start.character;
});
const headingLabel = entries.length === 1
? l10n.t('{0} (1 finding)', group.relPath)
: l10n.t('{0} ({1} findings)', group.relPath, entries.length);
lines.push(`### ${headingLabel}`);
lines.push('');
for (const entry of entries) {
const { line, excerpt } = buildFindingExcerpt(entry.comment, workspaceService);
const basename = path.basename(entry.comment.uri.fsPath);
const suffix = entry.isExcluded ? ' ' + l10n.t('(filtered)') : '';
const ch = entry.comment.range.start.character;
const args = encodeURIComponent(JSON.stringify([entry.comment.uri.toString(), line, ch]));
lines.push(`- [${basename}:${line}](command:github.copilot.chat.review.revealComment?${args}): ${excerpt}${suffix}`);
}
lines.push('');
}
const md = new MarkdownString(lines.join('\n'));
md.isTrusted = { enabledCommands: ['github.copilot.chat.review.revealComment'] };
return md;
Comment on lines +221 to +276
let result: FeedbackResult;
try {
result = await githubReview(
this.logService, this.gitExtensionService, this.authService,
this.capiClientService, this.domainService, this.fetcherService,
this.envService, this.ignoreService, this.workspaceService,
this.customInstructionsService, group, editor, progress, token
);
if (result.type === 'cancelled' || token.isCancellationRequested) {
if (!reviewComplete.isSettled) {
reviewComplete.complete(l10n.t('Review cancelled'));
}
if (addedComments.length) {
this.reviewService.removeReviewComments(addedComments);
}
outputStream.markdown(l10n.t('Review cancelled.'));
return;
}
if (result.type === 'error') {
if (!reviewComplete.isSettled) {
reviewComplete.complete(l10n.t('Review failed'));
}
outputStream.markdown(l10n.t('Review failed: {0}', result.reason));
return;
}
const excluded = result.excludedComments ?? [];
if (excluded.length) {
safeAdd(excluded);
}
const filesWithComments = new Set([...result.comments, ...excluded].map(c => c.uri.toString())).size;
if (result.comments.length === 0) {
if (!reviewComplete.isSettled) {
reviewComplete.complete(l10n.t('No issues found'));
}
outputStream.markdown(l10n.t('No issues found.'));
return;
}
if (!reviewComplete.isSettled) {
reviewComplete.complete(l10n.t('Review complete: {0} findings across {1} files', result.comments.length, filesWithComments));
}
outputStream.markdown(l10n.t('Review complete: {0} findings across {1} files.', result.comments.length, filesWithComments));
if (excluded.length) {
outputStream.markdown('\n\n' + l10n.t('{0} comments filtered by policy.', excluded.length));
}
if (result.comments.length + excluded.length >= 5) {
outputStream.markdown(buildFindingsByFileSummary(result.comments, excluded, this.workspaceService));
}
outputStream.button({ command: 'github.copilot.chat.review.nextFromChat', title: l10n.t('Next Comment') });
} finally {
// Defensive: always settle the task-bound progress so the spinner stops,
// even if `githubReview` threw before any explicit branch ran.
if (!reviewComplete.isSettled) {
reviewComplete.complete(l10n.t('Review failed'));
}
reviewStore.dispose();
}
Comment on lines +294 to +296
disposables.add(vscode.commands.registerCommand('github.copilot.chat.review.nextFromChat', () =>
goToNextReview(reviewService.getActiveThread(), +1)
));
Unhides the /review slash command and adds a chat-driven entry point
that complements the existing SCM gutter buttons. The chat path:

* accepts scope arguments: staged, unstaged, all, selection, file
* streams findings into the Comments panel with a task-bound spinner
* renders a per-file findings summary with clickable links
* provides a 'Next Comment' button for keyboard-free navigation
* routes the SCM gutter buttons through /review so the chat thread
  is the canonical entry point and preserves the user's current
  chat mode (agent/ask)

Existing non-chat flows (selection review via Ctrl+I, per-file SCM
context menu, codeReview.run) continue to use the legacy direct-to-
Comments-panel path with their original auto-jump-to-first-finding
behaviour, gated by an opts.suppressAutoReveal flag on
IReviewService.addReviewComments.
@andysharman andysharman force-pushed the andysharman/ccr-progress-ux branch from 6c8b3fd to ab1a8aa Compare June 5, 2026 21:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants