Path: blob/main/src/vs/sessions/contrib/codeReview/browser/codeReview.contributions.ts
13401 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { Codicon } from '../../../../base/common/codicons.js';6import { Disposable, MutableDisposable } from '../../../../base/common/lifecycle.js';7import { autorun } from '../../../../base/common/observable.js';8import { URI } from '../../../../base/common/uri.js';9import { localize } from '../../../../nls.js';10import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js';11import { ContextKeyExpr, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js';12import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';13import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';14import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../../workbench/common/contributions.js';15import { IsSessionsWindowContext } from '../../../../workbench/common/contextkeys.js';16import { IsPhoneLayoutContext } from '../../../common/contextkeys.js';17import { ChatContextKeys } from '../../../../workbench/contrib/chat/common/actions/chatContextKeys.js';18import { CHAT_CATEGORY } from '../../../../workbench/contrib/chat/browser/actions/chatActions.js';19import { ISessionsManagementService } from '../../../services/sessions/common/sessionsManagement.js';20import { ThemeIcon } from '../../../../base/common/themables.js';21import { IAgentFeedbackService } from '../../agentFeedback/browser/agentFeedbackService.js';22import { getSessionEditorComments } from '../../agentFeedback/browser/sessionEditorComments.js';23import { CodeReviewService, CodeReviewStateKind, getCodeReviewFilesFromSessionChanges, getCodeReviewVersion, ICodeReviewService, MAX_CODE_REVIEWS_PER_SESSION_VERSION, PRReviewStateKind } from './codeReviewService.js';24import { CopilotCloudSessionType } from '../../../services/sessions/common/session.js';2526registerSingleton(ICodeReviewService, CodeReviewService, InstantiationType.Delayed);2728const canRunSessionCodeReviewContextKey = new RawContextKey<boolean>('sessions.canRunCodeReview', true, {29type: 'boolean',30description: localize('sessions.canRunCodeReview', "True when a new code review can be started for the active session version."),31});3233function registerSessionCodeReviewAction(tooltip: string, icon: ThemeIcon): Disposable {34class RunSessionCodeReviewAction extends Action2 {35static readonly ID = 'sessions.codeReview.run';3637constructor() {38super({39id: RunSessionCodeReviewAction.ID,40title: localize('sessions.runCodeReview', "Run Code Review"),41tooltip,42category: CHAT_CATEGORY,43icon,44precondition: ContextKeyExpr.and(45ChatContextKeys.hasAgentSessionChanges,46canRunSessionCodeReviewContextKey),47menu: [48{49id: MenuId.ChatEditingSessionChangesToolbar,50group: 'navigation',51order: 7,52when: ContextKeyExpr.and(53IsSessionsWindowContext,54ChatContextKeys.agentSessionType.notEqualsTo(CopilotCloudSessionType.id),55IsPhoneLayoutContext.negate(),56),57},58],59});60}6162override async run(accessor: ServicesAccessor, sessionResource?: URI): Promise<void> {63const sessionManagementService = accessor.get(ISessionsManagementService);64const codeReviewService = accessor.get(ICodeReviewService);65const agentFeedbackService = accessor.get(IAgentFeedbackService);6667const resource = URI.isUri(sessionResource)68? sessionResource69: sessionManagementService.activeSession.get()?.resource;70if (!resource) {71return;72}7374// Get changes from ISession75const sessionData = sessionManagementService.getSession(resource);76const changes = sessionData?.changes.get();77if (!changes || changes.length === 0) {78return;79}8081const files = getCodeReviewFilesFromSessionChanges(changes);82const version = getCodeReviewVersion(files);8384// If there are existing comments (code review or PR review), navigate to the first one85const reviewState = codeReviewService.getReviewState(resource).get();86const prReviewState = codeReviewService.getPRReviewState(resource).get();87const reviewCount = reviewState.kind !== CodeReviewStateKind.Idle && reviewState.version === version ? reviewState.reviewCount : 0;88const codeReviewCount = reviewState.kind === CodeReviewStateKind.Result && reviewState.version === version ? reviewState.comments.length : 0;89const prReviewCount = prReviewState.kind === PRReviewStateKind.Loaded ? prReviewState.comments.length : 0;9091if (codeReviewCount > 0 || prReviewCount > 0) {92const comments = getSessionEditorComments(93resource,94agentFeedbackService.getFeedback(resource),95reviewState,96prReviewState,97);98const first = agentFeedbackService.getNextNavigableItem(resource, comments, true);99if (first) {100await agentFeedbackService.revealSessionComment(resource, first.id, first.resourceUri, first.range);101}102return;103}104105if (reviewCount >= MAX_CODE_REVIEWS_PER_SESSION_VERSION) {106return;107}108109110codeReviewService.requestReview(resource, version, files);111}112}113114return registerAction2(RunSessionCodeReviewAction) as Disposable;115}116117class CodeReviewToolbarContribution extends Disposable implements IWorkbenchContribution {118119static readonly ID = 'sessions.contrib.codeReviewToolbar';120121private readonly _actionRegistration = this._register(new MutableDisposable<Disposable>());122123constructor(124@IContextKeyService contextKeyService: IContextKeyService,125@ISessionsManagementService private readonly _sessionManagementService: ISessionsManagementService,126@ICodeReviewService private readonly _codeReviewService: ICodeReviewService,127) {128super();129130const canRunCodeReviewContext = canRunSessionCodeReviewContextKey.bindTo(contextKeyService);131132this._register(autorun(reader => {133const activeSession = this._sessionManagementService.activeSession.read(reader);134this._actionRegistration.clear();135136const sessionResource = activeSession?.resource;137if (!sessionResource) {138canRunCodeReviewContext.set(false);139this._actionRegistration.value = registerSessionCodeReviewAction(localize('sessions.runCodeReview.noSession', "No active session available for code review."), Codicon.codeReview);140return;141}142143const changes = activeSession.changes.read(reader);144if (changes.length === 0) {145canRunCodeReviewContext.set(false);146this._actionRegistration.value = registerSessionCodeReviewAction(localize('sessions.runCodeReview.noChanges', "No changes available for code review."), Codicon.codeReview);147return;148}149150const files = getCodeReviewFilesFromSessionChanges(changes);151const version = getCodeReviewVersion(files);152const reviewState = this._codeReviewService.getReviewState(sessionResource).read(reader);153const prReviewState = this._codeReviewService.getPRReviewState(sessionResource).read(reader);154const reviewCount = reviewState.kind !== CodeReviewStateKind.Idle && reviewState.version === version ? reviewState.reviewCount : 0;155156const codeReviewCount = reviewState.kind === CodeReviewStateKind.Result && reviewState.version === version ? reviewState.comments.length : 0;157const prReviewCount = prReviewState.kind === PRReviewStateKind.Loaded ? prReviewState.comments.length : 0;158const totalCommentCount = codeReviewCount + prReviewCount;159160let canRunCodeReview = true;161let tooltip = localize('sessions.runCodeReview.tooltip.default', "Run Code Review");162let icon = Codicon.codeReview;163164if (reviewState.kind === CodeReviewStateKind.Loading && reviewState.version === version) {165canRunCodeReview = false;166tooltip = localize('sessions.runCodeReview.tooltip.loading', "Creating code review...");167icon = Codicon.commentDraft;168} else if (totalCommentCount > 0) {169canRunCodeReview = true;170icon = Codicon.commentUnresolved;171tooltip = totalCommentCount === 1172? localize('sessions.runCodeReview.tooltip.oneUnresolved', "1 review comment unresolved.")173: localize('sessions.runCodeReview.tooltip.manyUnresolved', "{0} review comments unresolved.", totalCommentCount);174} else if (reviewCount >= MAX_CODE_REVIEWS_PER_SESSION_VERSION) {175canRunCodeReview = false;176tooltip = localize('sessions.runCodeReview.tooltip.limitReached', "Maximum of {0} code reviews reached for this session version.", MAX_CODE_REVIEWS_PER_SESSION_VERSION);177icon = Codicon.codeReview;178} else if (reviewState.kind === CodeReviewStateKind.Result && reviewState.version === version) {179canRunCodeReview = true;180tooltip = reviewState.didProduceComments181? localize('sessions.runCodeReview.tooltip.runAgain', "Run another code review.")182: localize('sessions.runCodeReview.tooltip.noCommentsRunAgain', "Previous code review produced no comments. Run code review again.");183icon = reviewState.didProduceComments ? Codicon.comment : Codicon.codeReview;184}185186canRunCodeReviewContext.set(canRunCodeReview);187this._actionRegistration.value = registerSessionCodeReviewAction(tooltip, icon);188}));189}190}191192registerWorkbenchContribution2(CodeReviewToolbarContribution.ID, CodeReviewToolbarContribution, WorkbenchPhase.AfterRestored);193194195