Path: blob/main/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorWidgetContribution.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 './media/agentFeedbackEditorWidget.css';67import { Action } from '../../../../base/common/actions.js';8import { Codicon } from '../../../../base/common/codicons.js';9import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js';10import { Event } from '../../../../base/common/event.js';11import { autorun, observableSignalFromEvent } from '../../../../base/common/observable.js';12import { ICodeEditor, IOverlayWidget, IOverlayWidgetPosition } from '../../../../editor/browser/editorBrowser.js';13import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';14import { IEditorContribution, IEditorDecorationsCollection, ScrollType } from '../../../../editor/common/editorCommon.js';15import { EditorContributionInstantiation, registerEditorContribution } from '../../../../editor/browser/editorExtensions.js';16import { EditorOption } from '../../../../editor/common/config/editorOptions.js';17import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js';18import { $, addDisposableListener, addStandardDisposableListener, clearNode, getTotalWidth } from '../../../../base/browser/dom.js';19import { URI } from '../../../../base/common/uri.js';20import { Range } from '../../../../editor/common/core/range.js';21import { overviewRulerRangeHighlight } from '../../../../editor/common/core/editorColorRegistry.js';22import { OverviewRulerLane } from '../../../../editor/common/model.js';23import { themeColorFromId } from '../../../../platform/theme/common/themeService.js';24import { ThemeIcon } from '../../../../base/common/themables.js';25import * as nls from '../../../../nls.js';26import { IAgentFeedbackService } from './agentFeedbackService.js';27import { IChatEditingService } from '../../../../workbench/contrib/chat/common/editing/chatEditingService.js';28import { isIChatSessionFileChange2 } from '../../../../workbench/contrib/chat/common/chatSessionsService.js';29import { ISessionsManagementService } from '../../../services/sessions/common/sessionsManagement.js';30import { createAgentFeedbackContext, getSessionForResource } from './agentFeedbackEditorUtils.js';31import { ICodeReviewService, IPRReviewState } from '../../codeReview/browser/codeReviewService.js';32import { getSessionEditorComments, groupNearbySessionEditorComments, ISessionEditorComment, SessionEditorCommentSource, toSessionEditorCommentId } from './sessionEditorComments.js';33import { ActionBar } from '../../../../base/browser/ui/actionbar/actionbar.js';34import { isEqual } from '../../../../base/common/resources.js';35import { IMarkdownRendererService } from '../../../../platform/markdown/browser/markdownRenderer.js';36import { MarkdownString } from '../../../../base/common/htmlContent.js';37import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';38import { KeyCode } from '../../../../base/common/keyCodes.js';39import { ISessionFileChange } from '../../../services/sessions/common/session.js';4041interface ICommentItemActions {42editAction: Action;43convertAction: Action | undefined;44removeAction: Action;45}4647/**48* Widget that displays agent feedback comments for a group of nearby feedback items.49* Positioned on the right side of the editor like a speech bubble.50*/51export class AgentFeedbackEditorWidget extends Disposable implements IOverlayWidget {5253private static _idPool = 0;54private readonly _id: string = `agent-feedback-widget-${AgentFeedbackEditorWidget._idPool++}`;5556private readonly _domNode: HTMLElement;57private readonly _headerNode: HTMLElement;58private readonly _titleNode: HTMLElement;59private readonly _toggleButton: HTMLElement;60private readonly _bodyNode: HTMLElement;61private readonly _itemElements = new Map<string, HTMLElement>();6263private _position: IOverlayWidgetPosition | null = null;64private _isExpanded: boolean = false;65private _disposed: boolean = false;66private _startLineNumber: number = 1;67private readonly _rangeHighlightDecoration: IEditorDecorationsCollection;6869private readonly _eventStore = this._register(new DisposableStore());7071constructor(72private readonly _editor: ICodeEditor,73private readonly _commentItems: readonly ISessionEditorComment[],74private readonly _sessionResource: URI,75@IAgentFeedbackService private readonly _agentFeedbackService: IAgentFeedbackService,76@ICodeReviewService private readonly _codeReviewService: ICodeReviewService,77@IMarkdownRendererService private readonly _markdownRendererService: IMarkdownRendererService,78@ICodeEditorService private readonly _codeEditorService: ICodeEditorService,79) {80super();8182this._rangeHighlightDecoration = this._editor.createDecorationsCollection();8384// Create DOM structure85this._domNode = $('div.agent-feedback-widget');86this._domNode.classList.add('collapsed');8788// Header89this._headerNode = $('div.agent-feedback-widget-header');9091// Comment icon (decorative, hidden from screen readers)92const commentIcon = renderIcon(Codicon.comment);93commentIcon.setAttribute('aria-hidden', 'true');94this._headerNode.appendChild(commentIcon);9596// Title showing feedback count97this._titleNode = $('span.agent-feedback-widget-title');98this._updateTitle();99this._headerNode.appendChild(this._titleNode);100101// Spacer102this._headerNode.appendChild($('span.agent-feedback-widget-spacer'));103104// Toggle expand/collapse button105this._toggleButton = $('div.agent-feedback-widget-toggle');106this._updateToggleButton();107this._headerNode.appendChild(this._toggleButton);108109this._domNode.appendChild(this._headerNode);110111// Body (collapsible) — starts collapsed112this._bodyNode = $('div.agent-feedback-widget-body');113this._bodyNode.classList.add('collapsed');114this._buildFeedbackItems();115this._domNode.appendChild(this._bodyNode);116117// Arrow pointer118const arrow = $('div.agent-feedback-widget-arrow');119this._domNode.appendChild(arrow);120121// Event handlers122this._setupEventHandlers();123124// Add visible class for initial display125this._domNode.classList.add('visible');126127// Add to editor128this._editor.addOverlayWidget(this);129}130131private _setupEventHandlers(): void {132// Toggle button click - expand/collapse133this._eventStore.add(addDisposableListener(this._toggleButton, 'click', (e) => {134e.stopPropagation();135this._toggleExpanded();136}));137138// Header click - also toggles expand/collapse139this._eventStore.add(addDisposableListener(this._headerNode, 'click', () => {140this._toggleExpanded();141}));142143}144145private _toggleExpanded(): void {146if (this._isExpanded) {147this.collapse();148} else {149this.expand();150}151}152153private _updateTitle(): void {154const count = this._commentItems.length;155if (count === 1) {156this._titleNode.textContent = this._commentItems[0].text;157} else {158this._titleNode.textContent = nls.localize('nComments', "{0} comments", count);159}160}161162private _updateToggleButton(): void {163clearNode(this._toggleButton);164if (this._isExpanded) {165this._toggleButton.appendChild(renderIcon(Codicon.chevronUp));166this._toggleButton.title = nls.localize('collapse', "Collapse");167} else {168this._toggleButton.appendChild(renderIcon(Codicon.chevronDown));169this._toggleButton.title = nls.localize('expand', "Expand");170}171}172173private _buildFeedbackItems(): void {174clearNode(this._bodyNode);175this._itemElements.clear();176177for (const comment of this._commentItems) {178const item = $('div.agent-feedback-widget-item');179item.classList.add(`agent-feedback-widget-item-${comment.source}`);180if (comment.suggestion) {181item.classList.add('agent-feedback-widget-item-suggestion');182}183this._itemElements.set(comment.id, item);184185const itemHeader = $('div.agent-feedback-widget-item-header');186const itemMeta = $('div.agent-feedback-widget-item-meta');187188const lineInfo = $('span.agent-feedback-widget-line-info');189if (comment.range.startLineNumber === comment.range.endLineNumber) {190lineInfo.textContent = nls.localize('lineNumber', "Line {0}", comment.range.startLineNumber);191} else {192lineInfo.textContent = nls.localize('lineRange', "Lines {0}-{1}", comment.range.startLineNumber, comment.range.endLineNumber);193}194itemMeta.appendChild(lineInfo);195196if (comment.source !== SessionEditorCommentSource.AgentFeedback) {197const typeBadge = $('span.agent-feedback-widget-item-type');198typeBadge.textContent = this._getTypeLabel(comment);199itemMeta.appendChild(typeBadge);200}201202itemHeader.appendChild(itemMeta);203204const actionBarContainer = $('div.agent-feedback-widget-item-actions');205const actionBar = this._eventStore.add(new ActionBar(actionBarContainer));206207const itemActions: ICommentItemActions = { editAction: undefined!, convertAction: undefined, removeAction: undefined! };208209itemActions.editAction = new Action(210'agentFeedback.widget.edit',211nls.localize('editComment', "Edit"),212ThemeIcon.asClassName(Codicon.edit),213true,214(): void => { this._startEditing(comment, text, itemActions); },215);216actionBar.push(itemActions.editAction, { icon: true, label: false });217218if (comment.canConvertToAgentFeedback) {219itemActions.convertAction = new Action(220'agentFeedback.widget.convert',221nls.localize('convertComment', "Convert to Agent Feedback"),222ThemeIcon.asClassName(Codicon.check),223true,224() => this._convertToAgentFeedback(comment),225);226actionBar.push(itemActions.convertAction, { icon: true, label: false });227}228itemActions.removeAction = new Action(229'agentFeedback.widget.remove',230nls.localize('removeComment', "Remove"),231ThemeIcon.asClassName(Codicon.close),232true,233() => this._removeComment(comment),234);235actionBar.push(itemActions.removeAction, { icon: true, label: false });236237itemHeader.appendChild(actionBarContainer);238item.appendChild(itemHeader);239240const text = $('div.agent-feedback-widget-text');241const rendered = this._markdownRendererService.render(new MarkdownString(comment.text));242this._eventStore.add(rendered);243text.appendChild(rendered.element);244item.appendChild(text);245246if (comment.suggestion?.edits.length) {247item.appendChild(this._renderSuggestion(comment));248}249250this._eventStore.add(addDisposableListener(item, 'mouseenter', () => {251this._highlightRange(comment);252}));253254this._eventStore.add(addDisposableListener(item, 'mouseleave', () => {255this._rangeHighlightDecoration.clear();256}));257258this._eventStore.add(addDisposableListener(item, 'click', e => {259if ((e.target as HTMLElement | null)?.closest('.action-bar')) {260return;261}262this.focusFeedback(comment.id);263this._agentFeedbackService.setNavigationAnchor(this._sessionResource, comment.id);264this._revealComment(comment);265}));266267this._bodyNode.appendChild(item);268}269}270271private _getTypeLabel(comment: ISessionEditorComment): string {272if (comment.source === SessionEditorCommentSource.PRReview) {273return nls.localize('prReviewComment', "PR Review");274}275276if (comment.source === SessionEditorCommentSource.CodeReview) {277return comment.suggestion278? nls.localize('reviewSuggestion', "Review Suggestion")279: nls.localize('reviewComment', "Review");280}281282return comment.suggestion283? nls.localize('feedbackSuggestion', "Feedback Suggestion")284: nls.localize('feedbackComment', "Feedback");285}286287private _renderSuggestion(comment: ISessionEditorComment): HTMLElement {288const suggestionNode = $('div.agent-feedback-widget-suggestion');289290for (const edit of comment.suggestion?.edits ?? []) {291const editNode = $('div.agent-feedback-widget-suggestion-edit');292293const header = $('div.agent-feedback-widget-suggestion-header');294if (edit.range.startLineNumber === edit.range.endLineNumber) {295header.textContent = nls.localize('suggestedChangeLine', "Suggested Change \u2022 Line {0}", edit.range.startLineNumber);296} else {297header.textContent = nls.localize('suggestedChangeLines', "Suggested Change \u2022 Lines {0}-{1}", edit.range.startLineNumber, edit.range.endLineNumber);298}299editNode.appendChild(header);300301const newText = $('pre.agent-feedback-widget-suggestion-text');302newText.textContent = edit.newText;303editNode.appendChild(newText);304suggestionNode.appendChild(editNode);305}306307return suggestionNode;308}309310private _removeComment(comment: ISessionEditorComment): void {311if (comment.source === SessionEditorCommentSource.PRReview) {312this._codeReviewService.resolvePRReviewThread(this._sessionResource!, comment.sourceId);313return;314}315if (comment.source === SessionEditorCommentSource.CodeReview) {316this._codeReviewService.removeComment(this._sessionResource, comment.sourceId);317return;318}319320this._agentFeedbackService.removeFeedback(this._sessionResource, comment.sourceId);321}322323private _startEditing(comment: ISessionEditorComment, textContainer: HTMLElement, actions: ICommentItemActions): void {324// Disable all actions while editing325actions.editAction.enabled = false;326if (actions.convertAction) {327actions.convertAction.enabled = false;328}329actions.removeAction.enabled = false;330331const editStore = new DisposableStore();332this._eventStore.add(editStore);333334clearNode(textContainer);335textContainer.classList.add('editing');336337const textarea = $('textarea.agent-feedback-widget-edit-textarea') as HTMLTextAreaElement;338textarea.value = comment.text;339textarea.rows = 1;340textContainer.appendChild(textarea);341342// Auto-size the textarea343const autoSize = () => {344textarea.style.height = 'auto';345textarea.style.height = `${textarea.scrollHeight}px`;346this._editor.layoutOverlayWidget(this);347};348autoSize();349350editStore.add(addDisposableListener(textarea, 'input', autoSize));351352editStore.add(addStandardDisposableListener(textarea, 'keydown', (e) => {353if (e.keyCode === KeyCode.Enter && !e.shiftKey) {354e.preventDefault();355e.stopPropagation();356const newText = textarea.value.trim();357if (newText) {358this._saveEdit(comment, newText);359}360// Widget will be rebuilt by the change event361} else if (e.keyCode === KeyCode.Escape) {362e.preventDefault();363e.stopPropagation();364this._stopEditing(comment, textContainer, editStore, actions);365}366}));367368// Stop editing when focus is lost369editStore.add(addDisposableListener(textarea, 'blur', () => {370this._stopEditing(comment, textContainer, editStore, actions);371}));372373textarea.focus();374}375376private _saveEdit(comment: ISessionEditorComment, newText: string): void {377if (comment.source === SessionEditorCommentSource.AgentFeedback) {378this._agentFeedbackService.updateFeedback(this._sessionResource, comment.sourceId, newText);379} else {380// PR review and code review comments are converted to agent feedback on edit381this._convertToAgentFeedbackWithText(comment, newText);382}383}384385private _stopEditing(comment: ISessionEditorComment, textContainer: HTMLElement, editStore: DisposableStore, actions: ICommentItemActions): void {386editStore.dispose();387388// Re-enable actions389actions.editAction.enabled = true;390if (actions.convertAction) {391actions.convertAction.enabled = true;392}393actions.removeAction.enabled = true;394395textContainer.classList.remove('editing');396clearNode(textContainer);397const rendered = this._markdownRendererService.render(new MarkdownString(comment.text));398this._eventStore.add(rendered);399textContainer.appendChild(rendered.element);400this._editor.layoutOverlayWidget(this);401}402403private _convertToAgentFeedback(comment: ISessionEditorComment): void {404this._convertToAgentFeedbackWithText(comment, comment.text);405}406407/**408* Converts a non-agent-feedback comment into an agent feedback item, optionally with edited text.409*/410private _convertToAgentFeedbackWithText(comment: ISessionEditorComment, text: string): void {411if (!comment.canConvertToAgentFeedback) {412return;413}414415const sourcePRReviewCommentId = comment.source === SessionEditorCommentSource.PRReview416? comment.sourceId417: undefined;418419const feedback = this._agentFeedbackService.addFeedback(420this._sessionResource,421comment.resourceUri,422comment.range,423text,424comment.suggestion,425createAgentFeedbackContext(this._editor, this._codeEditorService, comment.resourceUri, comment.range),426sourcePRReviewCommentId,427);428this._agentFeedbackService.setNavigationAnchor(this._sessionResource, toSessionEditorCommentId(SessionEditorCommentSource.AgentFeedback, feedback.id));429if (comment.source === SessionEditorCommentSource.CodeReview) {430this._codeReviewService.removeComment(this._sessionResource, comment.sourceId);431} else if (comment.source === SessionEditorCommentSource.PRReview) {432this._codeReviewService.markPRReviewCommentConverted(this._sessionResource, comment.sourceId);433}434}435436/**437* Expand the widget body.438*/439expand(): void {440this._isExpanded = true;441this._domNode.classList.remove('collapsed');442this._bodyNode.classList.remove('collapsed');443this._updateToggleButton();444this._editor.layoutOverlayWidget(this);445}446447/**448* Collapse the widget body.449*/450collapse(): void {451this._isExpanded = false;452this._domNode.classList.add('collapsed');453this._bodyNode.classList.add('collapsed');454this._updateToggleButton();455this.clearFocus();456this._editor.layoutOverlayWidget(this);457}458459/**460* Focus a specific feedback item within this widget.461* Highlights its range in the editor and marks it as focused.462*/463focusFeedback(feedbackId: string): void {464// Clear previous focus465for (const el of this._itemElements.values()) {466el.classList.remove('focused');467}468469const feedback = this._commentItems.find(f => f.id === feedbackId);470if (!feedback) {471return;472}473474// Add focused class to the item475const itemEl = this._itemElements.get(feedbackId);476itemEl?.classList.add('focused');477478// Show range highlighting479this._highlightRange(feedback);480}481482/**483* Clear focus state and range highlighting.484*/485clearFocus(): void {486for (const el of this._itemElements.values()) {487el.classList.remove('focused');488}489this._rangeHighlightDecoration.clear();490}491492private _highlightRange(feedback: ISessionEditorComment): void {493const endLineNumber = feedback.range.endLineNumber;494const range = new Range(495feedback.range.startLineNumber, 1,496endLineNumber, this._editor.getModel()?.getLineMaxColumn(endLineNumber) ?? 1497);498this._rangeHighlightDecoration.set([499{500range,501options: {502description: 'agent-feedback-range-highlight',503className: 'rangeHighlight',504isWholeLine: true,505linesDecorationsClassName: 'agent-feedback-widget-range-glyph',506}507},508{509range,510options: {511description: 'agent-feedback-range-highlight-overview',512overviewRuler: {513color: themeColorFromId(overviewRulerRangeHighlight),514position: OverviewRulerLane.Full,515}516}517}518]);519}520521/**522* Returns true if this widget contains the given feedback item (by id).523*/524containsFeedback(feedbackId: string): boolean {525return this._commentItems.some(f => f.id === feedbackId);526}527528/**529* Updates the widget position and layout.530*/531layout(startLineNumber: number): void {532if (this._disposed) {533return;534}535536this._startLineNumber = startLineNumber;537538const lineHeight = this._editor.getOption(EditorOption.lineHeight);539const { contentLeft, contentWidth, verticalScrollbarWidth } = this._editor.getLayoutInfo();540const scrollTop = this._editor.getScrollTop();541542const widgetWidth = getTotalWidth(this._domNode) || 280;543const widgetHeight = this._domNode.offsetHeight || 0;544const headerHeight = this._headerNode.offsetHeight || lineHeight;545546// Align the header center with the start line center before clamping within the editor content area.547const contentRelativeTop = this._editor.getTopForLineNumber(startLineNumber) + (lineHeight - headerHeight) / 2;548const scrollHeight = this._editor.getScrollHeight();549const clampedContentTop = Math.min(Math.max(0, contentRelativeTop), Math.max(0, scrollHeight - widgetHeight));550551this._position = {552stackOrdinal: 2,553preference: {554top: clampedContentTop - scrollTop,555left: contentLeft + contentWidth - (2 * verticalScrollbarWidth + widgetWidth)556}557};558559this._editor.layoutOverlayWidget(this);560}561562/**563* Shows or hides the widget.564*/565toggle(show: boolean): void {566this._domNode.classList.toggle('visible', show);567if (show && this._commentItems.length > 0) {568this.layout(this._commentItems[0].range.startLineNumber);569}570}571572/**573* Relayouts the widget at its current line number.574*/575relayout(): void {576if (this._startLineNumber) {577this.layout(this._startLineNumber);578}579}580581// IOverlayWidget implementation582583getId(): string {584return this._id;585}586587getDomNode(): HTMLElement {588return this._domNode;589}590591getPosition(): IOverlayWidgetPosition | null {592return this._position;593}594595override dispose(): void {596if (this._disposed) {597return;598}599this._disposed = true;600this._rangeHighlightDecoration.clear();601this._editor.removeOverlayWidget(this);602super.dispose();603}604605private _revealComment(comment: ISessionEditorComment): void {606const range = new Range(607comment.range.startLineNumber,6081,609comment.range.endLineNumber,610this._editor.getModel()?.getLineMaxColumn(comment.range.endLineNumber) ?? 1,611);612this._editor.revealRangeInCenterIfOutsideViewport(range, ScrollType.Smooth);613}614}615616/**617* Editor contribution that manages agent feedback widgets.618* Groups feedback items and creates combined widgets for nearby items.619* Widgets start collapsed and expand when navigated to.620*/621class AgentFeedbackEditorWidgetContribution extends Disposable implements IEditorContribution {622623static readonly ID = 'agentFeedback.editorWidgetContribution';624625private readonly _widgets: AgentFeedbackEditorWidget[] = [];626private _sessionResource: URI | undefined;627628constructor(629private readonly _editor: ICodeEditor,630@IAgentFeedbackService private readonly _agentFeedbackService: IAgentFeedbackService,631@IChatEditingService private readonly _chatEditingService: IChatEditingService,632@ISessionsManagementService private readonly _sessionsManagementService: ISessionsManagementService,633@ICodeReviewService private readonly _codeReviewService: ICodeReviewService,634@IInstantiationService private readonly _instantiationService: IInstantiationService,635) {636super();637638this._store.add(this._agentFeedbackService.onDidChangeNavigation(sessionResource => {639if (this._sessionResource && sessionResource.toString() === this._sessionResource.toString()) {640this._handleNavigation();641}642}));643644const rebuildSignal = observableSignalFromEvent(this, Event.any(645this._agentFeedbackService.onDidChangeFeedback,646this._editor.onDidChangeModel,647));648649this._store.add(Event.any(this._editor.onDidScrollChange, this._editor.onDidLayoutChange)(() => {650for (const widget of this._widgets) {651widget.relayout();652}653}));654655this._store.add(autorun(reader => {656rebuildSignal.read(reader);657this._resolveSession();658if (!this._sessionResource) {659this._clearWidgets();660return;661}662663this._rebuildWidgets(664this._codeReviewService.getReviewState(this._sessionResource).read(reader),665this._codeReviewService.getPRReviewState(this._sessionResource).read(reader),666);667this._handleNavigation();668}));669}670671private _resolveSession(): void {672const model = this._editor.getModel();673if (!model) {674this._sessionResource = undefined;675return;676}677this._sessionResource = getSessionForResource(model.uri, this._chatEditingService, this._sessionsManagementService);678}679680private _rebuildWidgets(681reviewState = this._sessionResource ? this._codeReviewService.getReviewState(this._sessionResource).get() : undefined,682prReviewState: IPRReviewState | undefined = this._sessionResource ? this._codeReviewService.getPRReviewState(this._sessionResource).get() : undefined,683): void {684this._clearWidgets();685686if (!this._sessionResource || !reviewState) {687return;688}689690const model = this._editor.getModel();691if (!model) {692return;693}694695const comments = getSessionEditorComments(696this._sessionResource,697this._agentFeedbackService.getFeedback(this._sessionResource),698reviewState,699prReviewState,700);701const fileComments = this._getCommentsForModel(model.uri, comments);702if (fileComments.length === 0) {703return;704}705706const groups = groupNearbySessionEditorComments(fileComments, 5);707708// Create widgets in reverse file order so that widgets further up in the709// file are added to the DOM last and therefore render on top of widgets710// further down.711for (let i = groups.length - 1; i >= 0; i--) {712const group = groups[i];713const widget = this._instantiationService.createInstance(AgentFeedbackEditorWidget, this._editor, group, this._sessionResource);714this._widgets.push(widget);715716widget.layout(group[0].range.startLineNumber);717}718}719720private _getCommentsForModel(resourceUri: URI, comments: readonly ISessionEditorComment[]): readonly ISessionEditorComment[] {721const change = this._getSessionChangeForResource(resourceUri);722if (!change) {723return comments.filter(comment => isEqual(comment.resourceUri, resourceUri));724}725726if (!this._isCurrentOrModifiedResource(change, resourceUri)) {727return [];728}729730return comments.filter(comment => comment.resourceUri.fsPath === resourceUri.fsPath);731}732733private _getSessionChangeForResource(resourceUri: URI): ISessionFileChange | undefined {734if (!this._sessionResource) {735return undefined;736}737738const changes = this._sessionsManagementService.getSession(this._sessionResource)?.changes.get();739if (!changes) {740return undefined;741}742743return changes.find(change => this._changeMatchesFsPath(change, resourceUri));744}745746private _changeMatchesFsPath(change: ISessionFileChange, resourceUri: URI): boolean {747if (isIChatSessionFileChange2(change)) {748return change.uri.fsPath === resourceUri.fsPath749|| change.modifiedUri?.fsPath === resourceUri.fsPath750|| change.originalUri?.fsPath === resourceUri.fsPath;751}752753return change.modifiedUri.fsPath === resourceUri.fsPath754|| change.originalUri?.fsPath === resourceUri.fsPath;755}756757private _isCurrentOrModifiedResource(change: ISessionFileChange, resourceUri: URI): boolean {758if (isIChatSessionFileChange2(change)) {759return isEqual(change.uri, resourceUri) || (change.modifiedUri ? isEqual(change.modifiedUri, resourceUri) : false);760}761762return isEqual(change.modifiedUri, resourceUri);763}764765private _handleNavigation(): void {766if (!this._sessionResource) {767return;768}769770const model = this._editor.getModel();771if (!model) {772return;773}774775const comments = getSessionEditorComments(776this._sessionResource,777this._agentFeedbackService.getFeedback(this._sessionResource),778this._codeReviewService.getReviewState(this._sessionResource).get(),779this._codeReviewService.getPRReviewState(this._sessionResource).get(),780);781const bearing = this._agentFeedbackService.getNavigationBearing(this._sessionResource, comments);782if (bearing.activeIdx < 0) {783return;784}785786const activeFeedback = comments[bearing.activeIdx];787if (!activeFeedback) {788return;789}790791if (this._getCommentsForModel(model.uri, [activeFeedback]).length === 0) {792for (const widget of this._widgets) {793widget.collapse();794}795return;796}797798// Expand the widget containing the active feedback, collapse all others799for (const widget of this._widgets) {800if (widget.containsFeedback(activeFeedback.id)) {801widget.expand();802widget.focusFeedback(activeFeedback.id);803} else {804widget.collapse();805}806}807808// Reveal the feedback range in the editor809const range = new Range(810activeFeedback.range.startLineNumber, 1,811activeFeedback.range.endLineNumber, 1812);813this._editor.revealRangeInCenterIfOutsideViewport(range, ScrollType.Smooth);814}815816private _clearWidgets(): void {817for (const widget of this._widgets) {818widget.dispose();819}820this._widgets.length = 0;821}822823override dispose(): void {824this._clearWidgets();825super.dispose();826}827}828829registerEditorContribution(AgentFeedbackEditorWidgetContribution.ID, AgentFeedbackEditorWidgetContribution, EditorContributionInstantiation.Eventually);830831832