Path: blob/main/src/vs/sessions/contrib/codeReview/browser/codeReviewService.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 { Disposable, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js';6import { autorun, derivedOpts, IObservable, observableValue, transaction } from '../../../../base/common/observable.js';7import { isEqual } from '../../../../base/common/resources.js';8import { URI, UriComponents } from '../../../../base/common/uri.js';9import { IRange, Range } from '../../../../editor/common/core/range.js';10import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';11import { ICommandService } from '../../../../platform/commands/common/commands.js';12import { ILogService } from '../../../../platform/log/common/log.js';13import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';14import { generateUuid } from '../../../../base/common/uuid.js';15import { hash } from '../../../../base/common/hash.js';16import { hasKey } from '../../../../base/common/types.js';17import { isIChatSessionFileChange2 } from '../../../../workbench/contrib/chat/common/chatSessionsService.js';18import { IGitHubService } from '../../github/browser/githubService.js';19import { ISessionsManagementService } from '../../../services/sessions/common/sessionsManagement.js';20import { ISessionFileChange } from '../../../services/sessions/common/session.js';21import { structuralEquals } from '../../../../base/common/equals.js';2223// --- Types -------------------------------------------------------------------2425export interface ICodeReviewComment {26readonly id: string;27readonly uri: URI;28readonly range: IRange;29readonly body: string;30readonly kind: string;31readonly severity: string;32readonly suggestion?: ICodeReviewSuggestion;33}3435export interface ICodeReviewSuggestion {36readonly edits: readonly ICodeReviewSuggestionChange[];37}3839export interface ICodeReviewSuggestionChange {40readonly range: IRange;41readonly newText: string;42readonly oldText: string;43}4445export interface ICodeReviewFile {46readonly currentUri: URI;47readonly baseUri?: URI;48}4950export function getCodeReviewFilesFromSessionChanges(changes: readonly ISessionFileChange[]): readonly ICodeReviewFile[] {51return changes.map(change => {52if (isIChatSessionFileChange2(change)) {53return {54currentUri: change.modifiedUri ?? change.uri,55baseUri: change.originalUri,56};57}5859return {60currentUri: change.modifiedUri,61baseUri: change.originalUri,62};63});64}6566export function getCodeReviewVersion(files: readonly ICodeReviewFile[]): string {67const stableFileList = files68.map(file => `${file.currentUri.toString()}|${file.baseUri?.toString() ?? ''}`)69.sort();7071return `v1:${stableFileList.length}:${hash(stableFileList)}`;72}7374export const MAX_CODE_REVIEWS_PER_SESSION_VERSION = 5;7576export const enum CodeReviewStateKind {77Idle = 'idle',78Loading = 'loading',79Result = 'result',80Error = 'error',81}8283export type ICodeReviewState =84| { readonly kind: CodeReviewStateKind.Idle }85| { readonly kind: CodeReviewStateKind.Loading; readonly version: string; readonly reviewCount: number }86| { readonly kind: CodeReviewStateKind.Result; readonly version: string; readonly reviewCount: number; readonly comments: readonly ICodeReviewComment[]; readonly didProduceComments: boolean }87| { readonly kind: CodeReviewStateKind.Error; readonly version: string; readonly reviewCount: number; readonly reason: string };8889// --- PR Review Types ---------------------------------------------------------9091export const enum PRReviewStateKind {92None = 'none',93Loading = 'loading',94Loaded = 'loaded',95Error = 'error',96}9798export type IPRReviewState =99| { readonly kind: PRReviewStateKind.None }100| { readonly kind: PRReviewStateKind.Loading }101| { readonly kind: PRReviewStateKind.Loaded; readonly comments: readonly IPRReviewComment[] }102| { readonly kind: PRReviewStateKind.Error; readonly reason: string };103104export interface IPRReviewComment {105readonly id: string;106readonly uri: URI;107readonly range: IRange;108readonly body: string;109readonly author: string;110}111112/** Shape of a single comment as returned by the code review command. */113interface IRawCodeReviewComment {114readonly uri: IRawCodeReviewUri;115readonly range: IRawCodeReviewRange;116readonly body?: string;117readonly kind?: string;118readonly severity?: string;119readonly suggestion?: IRawCodeReviewSuggestion;120}121122type IRawCodeReviewUri = URI | UriComponents | string;123124interface IRawCodeReviewPosition {125readonly line?: number;126readonly character?: number;127}128129interface IRawCodeReviewRangeWithPositions {130readonly start?: IRawCodeReviewPosition;131readonly end?: IRawCodeReviewPosition;132}133134interface IRawCodeReviewRangeWithLines {135readonly startLine?: number;136readonly startColumn?: number;137readonly endLine?: number;138readonly endColumn?: number;139}140141type IRawCodeReviewRangeTuple = readonly [IRawCodeReviewPosition, IRawCodeReviewPosition];142143type IRawCodeReviewRange = IRange | IRawCodeReviewRangeWithPositions | IRawCodeReviewRangeWithLines | IRawCodeReviewRangeTuple;144145interface IRawCodeReviewSuggestion {146readonly edits: readonly IRawCodeReviewSuggestionChange[];147}148149interface IRawCodeReviewSuggestionChange {150readonly range: IRawCodeReviewRange;151readonly newText: string;152readonly oldText: string;153}154155// --- Service Interface -------------------------------------------------------156157export const ICodeReviewService = createDecorator<ICodeReviewService>('codeReviewService');158159export interface ICodeReviewService {160readonly _serviceBrand: undefined;161162/**163* Get the observable review state for a session.164*/165getReviewState(sessionResource: URI): IObservable<ICodeReviewState>;166167/**168* Synchronously check if a completed review exists for the given session+version.169*/170hasReview(sessionResource: URI, version: string): boolean;171172/**173* Request a code review for the given session. The review is associated with174* a version string (fingerprint of changed files). If a review is already in175* progress or there are still unresolved review comments for this version,176* this is a no-op.177*/178requestReview(sessionResource: URI, version: string, files: readonly { readonly currentUri: URI; readonly baseUri?: URI }[]): void;179180/**181* Remove a single comment from the review results.182*/183removeComment(sessionResource: URI, commentId: string): void;184185/**186* Update the body text of a single code review comment.187*/188updateComment(sessionResource: URI, commentId: string, newBody: string): void;189190/**191* Dismiss/clear the review for a session entirely.192*/193dismissReview(sessionResource: URI): void;194195/**196* Get the observable PR review state for a session.197* Returns unresolved review comments from the PR associated with the session.198*/199getPRReviewState(sessionResource: URI): IObservable<IPRReviewState>;200201/**202* Resolve a PR review thread on GitHub and remove it from local state.203*/204resolvePRReviewThread(sessionResource: URI, threadId: string): Promise<void>;205206/**207* Mark a PR review comment as locally converted to agent feedback.208* The comment is hidden from the PR review state until the session is209* cleaned up.210*/211markPRReviewCommentConverted(sessionResource: URI, commentId: string): void;212}213214// --- Storage Types -----------------------------------------------------------215216interface IStoredCodeReview {217readonly version: string;218readonly reviewCount?: number;219readonly didProduceComments?: boolean;220readonly comments: readonly IStoredCodeReviewComment[];221}222223interface IStoredCodeReviewComment {224readonly id: string;225readonly uri: UriComponents;226readonly range: IRange;227readonly body: string;228readonly kind: string;229readonly severity: string;230readonly suggestion?: ICodeReviewSuggestion;231}232233// --- Implementation ----------------------------------------------------------234235interface ISessionReviewData {236readonly state: ReturnType<typeof observableValue<ICodeReviewState>>;237}238239type IPullRequestReviewThreadsModel = ReturnType<IGitHubService['getPullRequestReviewThreads']>;240241interface IPRSessionReviewData {242readonly state: ReturnType<typeof observableValue<IPRReviewState>>;243readonly disposables: DisposableStore;244readonly pollingDisposable: DisposableStore;245reviewThreadsModel?: IPullRequestReviewThreadsModel;246initialized: boolean;247}248249function isRawCodeReviewRangeWithPositions(range: IRawCodeReviewRange): range is IRawCodeReviewRangeWithPositions {250return typeof range === 'object' && range !== null && hasKey(range, { start: true, end: true });251}252253function isRawCodeReviewRangeTuple(range: IRawCodeReviewRange): range is IRawCodeReviewRangeTuple {254return Array.isArray(range) && range.length >= 2;255}256257function normalizeCodeReviewUri(uri: IRawCodeReviewUri): URI {258return typeof uri === 'string' ? URI.parse(uri) : URI.revive(uri);259}260261function normalizeCodeReviewRange(range: IRawCodeReviewRange): IRange {262if (Range.isIRange(range)) {263return Range.lift(range);264}265266if (isRawCodeReviewRangeTuple(range)) {267const [start, end] = range;268return new Range(269(start.line ?? 0) + 1,270(start.character ?? 0) + 1,271(end.line ?? start.line ?? 0) + 1,272(end.character ?? start.character ?? 0) + 1,273);274}275276if (isRawCodeReviewRangeWithPositions(range) && range.start && range.end) {277return new Range(278(range.start.line ?? 0) + 1,279(range.start.character ?? 0) + 1,280(range.end.line ?? range.start.line ?? 0) + 1,281(range.end.character ?? range.start.character ?? 0) + 1,282);283}284285const lineRange = range as IRawCodeReviewRangeWithLines;286return new Range(287(lineRange.startLine ?? 0) + 1,288(lineRange.startColumn ?? 0) + 1,289(lineRange.endLine ?? lineRange.startLine ?? 0) + 1,290(lineRange.endColumn ?? lineRange.startColumn ?? 0) + 1,291);292}293294function normalizeCodeReviewSuggestion(suggestion: IRawCodeReviewSuggestion | undefined): ICodeReviewSuggestion | undefined {295if (!suggestion) {296return undefined;297}298299return {300edits: suggestion.edits.map(edit => ({301range: normalizeCodeReviewRange(edit.range),302newText: edit.newText,303oldText: edit.oldText,304})),305};306}307308export class CodeReviewService extends Disposable implements ICodeReviewService {309310declare readonly _serviceBrand: undefined;311312private static readonly _STORAGE_KEY = 'codeReview.reviews';313314private readonly _reviewsBySession = new Map<string, ISessionReviewData>();315private readonly _prReviewBySession = new Map<string, IPRSessionReviewData>();316/** PR review comment IDs that have been converted to agent feedback (per session). */317private readonly _convertedPRCommentsBySession = new Map<string, Set<string>>();318319constructor(320@ICommandService private readonly _commandService: ICommandService,321@ILogService private readonly _logService: ILogService,322@IStorageService private readonly _storageService: IStorageService,323@IGitHubService private readonly _gitHubService: IGitHubService,324@ISessionsManagementService private readonly _sessionsManagementService: ISessionsManagementService,325) {326super();327this._loadFromStorage();328this._registerSessionListeners();329330const activeSessionResourceObs = derivedOpts({ equalsFn: isEqual }, reader => {331return this._sessionsManagementService.activeSession.read(reader)?.resource;332});333334const gitHubInfoObs = derivedOpts<{ owner: string; repo: string; pullRequestNumber: number } | undefined>({ equalsFn: structuralEquals }, reader => {335const gitHubInfo = this._sessionsManagementService.activeSession.read(reader)?.gitHubInfo.read(reader);336if (!gitHubInfo?.pullRequest) {337return undefined;338}339340return {341owner: gitHubInfo.owner,342repo: gitHubInfo.repo,343pullRequestNumber: gitHubInfo.pullRequest.number,344};345});346347this._register(autorun(reader => {348const activeSessionResource = activeSessionResourceObs.read(reader);349if (!activeSessionResource) {350return;351}352353const gitHubInfo = gitHubInfoObs.read(reader);354const data = this._ensurePRReviewInitialized(activeSessionResource, gitHubInfo);355356if (!data.reviewThreadsModel) {357return;358}359360// Initial fetch of review threads361data.reviewThreadsModel.refresh().catch(err => {362this._logService.error('[CodeReviewService] Failed to fetch PR review threads:', err);363data.state.set({ kind: PRReviewStateKind.Error, reason: String(err) }, undefined);364});365366// Start polling of review threads367data.pollingDisposable.add(data.reviewThreadsModel.startPolling());368369reader.store.add(toDisposable(() => {370data.pollingDisposable.clear();371}));372}));373}374375getReviewState(sessionResource: URI): IObservable<ICodeReviewState> {376return this._getOrCreateData(sessionResource).state;377}378379hasReview(sessionResource: URI, version: string): boolean {380const data = this._reviewsBySession.get(sessionResource.toString());381if (!data) {382return false;383}384const state = data.state.get();385return state.kind === CodeReviewStateKind.Result && state.version === version;386}387388requestReview(sessionResource: URI, version: string, files: readonly { readonly currentUri: URI; readonly baseUri?: URI }[]): void {389const data = this._getOrCreateData(sessionResource);390const currentState = data.state.get();391const currentReviewCount = currentState.kind !== CodeReviewStateKind.Idle && currentState.version === version ? currentState.reviewCount : 0;392393// Don't re-request if already loading or unresolved comments remain for this version.394if (currentState.kind === CodeReviewStateKind.Loading && currentState.version === version) {395return;396}397if (currentReviewCount >= MAX_CODE_REVIEWS_PER_SESSION_VERSION) {398return;399}400if (currentState.kind === CodeReviewStateKind.Result && currentState.version === version && currentState.comments.length > 0) {401return;402}403404data.state.set({ kind: CodeReviewStateKind.Loading, version, reviewCount: currentReviewCount + 1 }, undefined);405406this._executeReview(sessionResource, version, files, data);407}408409removeComment(sessionResource: URI, commentId: string): void {410const data = this._reviewsBySession.get(sessionResource.toString());411if (!data) {412return;413}414415const state = data.state.get();416if (state.kind !== CodeReviewStateKind.Result) {417return;418}419420const filtered = state.comments.filter(c => c.id !== commentId);421data.state.set({ kind: CodeReviewStateKind.Result, version: state.version, reviewCount: state.reviewCount, comments: filtered, didProduceComments: state.didProduceComments }, undefined);422this._saveToStorage();423}424425updateComment(sessionResource: URI, commentId: string, newBody: string): void {426const data = this._reviewsBySession.get(sessionResource.toString());427if (!data) {428return;429}430431const state = data.state.get();432if (state.kind !== CodeReviewStateKind.Result) {433return;434}435436const updated = state.comments.map(c => c.id === commentId ? { ...c, body: newBody } : c);437data.state.set({ kind: CodeReviewStateKind.Result, version: state.version, reviewCount: state.reviewCount, comments: updated, didProduceComments: state.didProduceComments }, undefined);438this._saveToStorage();439}440441dismissReview(sessionResource: URI): void {442const data = this._reviewsBySession.get(sessionResource.toString());443if (data) {444data.state.set({ kind: CodeReviewStateKind.Idle }, undefined);445this._saveToStorage();446}447}448449private _getOrCreateData(sessionResource: URI): ISessionReviewData {450const key = sessionResource.toString();451let data = this._reviewsBySession.get(key);452if (!data) {453data = {454state: observableValue<ICodeReviewState>(`codeReview.state.${key}`, { kind: CodeReviewStateKind.Idle }),455};456this._reviewsBySession.set(key, data);457}458return data;459}460461private async _executeReview(462sessionResource: URI,463version: string,464files: readonly { readonly currentUri: URI; readonly baseUri?: URI }[],465data: ISessionReviewData,466): Promise<void> {467try {468const result: { type: string; comments?: IRawCodeReviewComment[]; reason?: string } | undefined =469await this._commandService.executeCommand('chat.internal.codeReview.run', {470files: files.map(f => ({471currentUri: f.currentUri,472baseUri: f.baseUri,473})),474});475476// Check if version is still current (hasn't been dismissed or replaced)477const currentState = data.state.get();478if (currentState.kind !== CodeReviewStateKind.Loading || currentState.version !== version) {479return;480}481482if (!result || result.type === 'cancelled') {483data.state.set({ kind: CodeReviewStateKind.Idle }, undefined);484return;485}486487if (result.type === 'error') {488data.state.set({ kind: CodeReviewStateKind.Error, version, reviewCount: currentState.reviewCount, reason: result.reason ?? 'Unknown error' }, undefined);489return;490}491492if (result.type === 'success') {493const comments: ICodeReviewComment[] = (result.comments ?? []).map((raw) => ({494id: generateUuid(),495uri: normalizeCodeReviewUri(raw.uri),496range: normalizeCodeReviewRange(raw.range),497body: raw.body ?? '',498kind: raw.kind ?? '',499severity: raw.severity ?? '',500suggestion: normalizeCodeReviewSuggestion(raw.suggestion),501}));502503transaction(tx => {504data.state.set({ kind: CodeReviewStateKind.Result, version, reviewCount: currentState.reviewCount, comments, didProduceComments: comments.length > 0 }, tx);505});506this._saveToStorage();507}508} catch (err) {509const currentState = data.state.get();510if (currentState.kind === CodeReviewStateKind.Loading && currentState.version === version) {511data.state.set({ kind: CodeReviewStateKind.Error, version, reviewCount: currentState.reviewCount, reason: String(err) }, undefined);512}513}514}515516private _loadFromStorage(): void {517const raw = this._storageService.get(CodeReviewService._STORAGE_KEY, StorageScope.WORKSPACE);518if (!raw) {519return;520}521522try {523const stored: Record<string, IStoredCodeReview> = JSON.parse(raw);524for (const [key, review] of Object.entries(stored)) {525const comments: ICodeReviewComment[] = review.comments.map(c => ({526id: c.id,527uri: URI.revive(c.uri),528range: c.range,529body: c.body,530kind: c.kind,531severity: c.severity,532suggestion: c.suggestion,533}));534const data = this._getOrCreateData(URI.parse(key));535data.state.set({ kind: CodeReviewStateKind.Result, version: review.version, reviewCount: review.reviewCount ?? 1, comments, didProduceComments: review.didProduceComments ?? comments.length > 0 }, undefined);536}537} catch {538// Corrupted storage data - ignore539}540}541542private _saveToStorage(): void {543const stored: Record<string, IStoredCodeReview> = {};544for (const [key, data] of this._reviewsBySession) {545const state = data.state.get();546if (state.kind === CodeReviewStateKind.Result) {547stored[key] = {548version: state.version,549reviewCount: state.reviewCount,550didProduceComments: state.didProduceComments,551comments: state.comments.map(c => ({552id: c.id,553uri: c.uri.toJSON(),554range: c.range,555body: c.body,556kind: c.kind,557severity: c.severity,558suggestion: c.suggestion,559})),560};561}562}563564if (Object.keys(stored).length === 0) {565this._storageService.remove(CodeReviewService._STORAGE_KEY, StorageScope.WORKSPACE);566} else {567this._storageService.store(CodeReviewService._STORAGE_KEY, JSON.stringify(stored), StorageScope.WORKSPACE, StorageTarget.MACHINE);568}569}570571private _registerSessionListeners(): void {572this._register(this._sessionsManagementService.onDidChangeSessions(e => {573let changed = false;574575// Clean up reviews for removed/archived sessions576for (const session of [...e.removed, ...e.changed.filter(s => s.isArchived.get())]) {577this._disposePRReview(session.resource);578579const key = session.resource.toString();580const data = this._reviewsBySession.get(key);581if (data) {582data.state.set({ kind: CodeReviewStateKind.Idle }, undefined);583changed = true;584}585}586587// Check for stale review versions when sessions change588for (const [key, data] of this._reviewsBySession) {589const state = data.state.get();590if (state.kind !== CodeReviewStateKind.Result) {591continue;592}593594const session = this._sessionsManagementService.getSession(URI.parse(key));595if (!session) {596// Session no longer exists - clean up597data.state.set({ kind: CodeReviewStateKind.Idle }, undefined);598changed = true;599continue;600}601602const changes = session.changes.get();603if (changes.length === 0) {604// Session has no file-level changes - clean up605data.state.set({ kind: CodeReviewStateKind.Idle }, undefined);606changed = true;607continue;608}609610const files = getCodeReviewFilesFromSessionChanges(changes);611const currentVersion = getCodeReviewVersion(files);612if (state.version !== currentVersion) {613// Version mismatch - review is stale614data.state.set({ kind: CodeReviewStateKind.Idle }, undefined);615changed = true;616}617}618619if (changed) {620this._saveToStorage();621}622}));623}624625getPRReviewState(sessionResource: URI): IObservable<IPRReviewState> {626return this._getOrCreatePRReviewData(sessionResource).state;627}628629async resolvePRReviewThread(sessionResource: URI, threadId: string): Promise<void> {630const session = this._sessionsManagementService.getSession(sessionResource);631const gitHubInfo = session?.gitHubInfo.get();632if (gitHubInfo?.pullRequest) {633const reviewThreadsModel = this._gitHubService.getPullRequestReviewThreads(gitHubInfo.owner, gitHubInfo.repo, gitHubInfo.pullRequest.number);634try {635await reviewThreadsModel.resolveThread(threadId);636} catch (err) {637this._logService.warn('[CodeReviewService] Failed to resolve PR thread on GitHub:', err);638}639}640641// Remove from local state regardless of GitHub success642const data = this._prReviewBySession.get(sessionResource.toString());643if (data) {644const currentState = data.state.get();645if (currentState.kind === PRReviewStateKind.Loaded) {646const filtered = currentState.comments.filter(c => c.id !== threadId);647data.state.set({ kind: PRReviewStateKind.Loaded, comments: filtered }, undefined);648}649}650}651652markPRReviewCommentConverted(sessionResource: URI, commentId: string): void {653const key = sessionResource.toString();654let converted = this._convertedPRCommentsBySession.get(key);655if (!converted) {656converted = new Set();657this._convertedPRCommentsBySession.set(key, converted);658}659converted.add(commentId);660661// Immediately filter the comment from the observable PR review state662const data = this._prReviewBySession.get(key);663if (data) {664const currentState = data.state.get();665if (currentState.kind === PRReviewStateKind.Loaded) {666const filtered = currentState.comments.filter(c => c.id !== commentId);667data.state.set({ kind: PRReviewStateKind.Loaded, comments: filtered }, undefined);668}669}670}671672private _getOrCreatePRReviewData(sessionResource: URI): IPRSessionReviewData {673const key = sessionResource.toString();674let data = this._prReviewBySession.get(key);675if (!data) {676data = {677state: observableValue<IPRReviewState>(`prReview.state.${key}`, { kind: PRReviewStateKind.None }),678disposables: new DisposableStore(),679pollingDisposable: new DisposableStore(),680initialized: false,681};682data.disposables.add(data.pollingDisposable);683this._prReviewBySession.set(key, data);684}685return data;686}687688private _ensurePRReviewInitialized(sessionResource: URI, gitHubInfo: { owner: string; repo: string; pullRequestNumber: number } | undefined): IPRSessionReviewData {689const data = this._getOrCreatePRReviewData(sessionResource);690if (data.initialized) {691return data;692}693694const session = this._sessionsManagementService.getSession(sessionResource);695if (!session || !gitHubInfo) {696return data;697}698699data.initialized = true;700data.state.set({ kind: PRReviewStateKind.Loading }, undefined);701702const reviewThreadsModel = this._gitHubService.getPullRequestReviewThreads(gitHubInfo.owner, gitHubInfo.repo, gitHubInfo.pullRequestNumber);703const workspace = session.workspace.get();704data.reviewThreadsModel = reviewThreadsModel;705706// Watch the PR review threads model and map to local state707data.disposables.add(autorun(reader => {708const threads = reviewThreadsModel.reviewThreads.read(reader);709const converted = this._convertedPRCommentsBySession.get(sessionResource.toString());710const comments: IPRReviewComment[] = [];711712for (const thread of threads) {713if (thread.isResolved) {714continue;715}716const threadId = String(thread.id);717if (converted?.has(threadId)) {718continue;719}720const baseUri = workspace?.repositories[0]?.workingDirectory ?? workspace?.repositories[0]?.uri;721if (!baseUri) {722continue;723}724const fileUri = URI.joinPath(baseUri, thread.path);725const line = thread.line ?? 1;726const firstComment = thread.comments[0];727comments.push({728id: String(thread.id),729uri: fileUri,730range: new Range(line, 1, line, 1),731body: firstComment?.body ?? '',732author: firstComment?.author.login ?? '',733});734}735736data.state.set({ kind: PRReviewStateKind.Loaded, comments }, undefined);737}));738739return data;740}741742private _disposePRReview(sessionResource: URI): void {743const key = sessionResource.toString();744this._convertedPRCommentsBySession.delete(key);745const data = this._prReviewBySession.get(key);746if (data) {747data.disposables.dispose();748749this._prReviewBySession.delete(key);750}751}752753override dispose(): void {754for (const data of this._prReviewBySession.values()) {755data.disposables.dispose();756}757this._prReviewBySession.clear();758759super.dispose();760}761}762763764