Path: blob/main/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorOverlay.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/agentFeedbackEditorOverlay.css';6import { Disposable, DisposableMap, DisposableStore, combinedDisposable, toDisposable } from '../../../../base/common/lifecycle.js';7import { autorun, observableFromEvent, observableSignalFromEvent, observableValue } from '../../../../base/common/observable.js';8import { ActionViewItem, IBaseActionViewItemOptions } from '../../../../base/browser/ui/actionbar/actionViewItems.js';9import { IAction } from '../../../../base/common/actions.js';10import { Event } from '../../../../base/common/event.js';11import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js';12import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';13import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';14import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js';15import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';16import { IWorkbenchContribution } from '../../../../workbench/common/contributions.js';17import { EditorGroupView } from '../../../../workbench/browser/parts/editor/editorGroupView.js';18import { IEditorGroup, IEditorGroupsService } from '../../../../workbench/services/editor/common/editorGroupsService.js';19import { IAgentFeedbackService } from './agentFeedbackService.js';20import { hasSessionAgentFeedback, hasSessionEditorComments, navigateNextFeedbackActionId, navigatePreviousFeedbackActionId, navigationBearingFakeActionId, submitFeedbackActionId } from './agentFeedbackEditorActions.js';21import { assertType } from '../../../../base/common/types.js';22import { localize } from '../../../../nls.js';23import { getActiveResourceCandidates, getSessionForResource } from './agentFeedbackEditorUtils.js';24import { Menus } from '../../../browser/menus.js';25import { IChatEditingService } from '../../../../workbench/contrib/chat/common/editing/chatEditingService.js';26import { ICodeReviewService } from '../../codeReview/browser/codeReviewService.js';27import { getSessionEditorComments, hasAgentFeedbackComments } from './sessionEditorComments.js';28import { ISessionsManagementService } from '../../../services/sessions/common/sessionsManagement.js';2930class AgentFeedbackActionViewItem extends ActionViewItem {3132constructor(33action: IAction,34options: IBaseActionViewItemOptions,35private readonly _keybindingService: IKeybindingService,36private readonly _primaryActionIds: readonly string[] = [submitFeedbackActionId],37) {38const isIconOnly = action.id === navigatePreviousFeedbackActionId || action.id === navigateNextFeedbackActionId;39super(undefined, action, { ...options, icon: isIconOnly, label: !isIconOnly, keybindingNotRenderedWithLabel: true });40}4142override render(container: HTMLElement): void {43super.render(container);44if (this._primaryActionIds.includes(this._action.id)) {45this.element?.classList.add('primary');46}47}4849protected override getTooltip(): string | undefined {50const value = super.getTooltip();51if (!value || this.options.keybinding) {52return value;53}54return this._keybindingService.appendKeybinding(value, this._action.id);55}56}5758export class AgentFeedbackOverlayWidget extends Disposable {5960private readonly _domNode: HTMLElement;61private readonly _toolbarNode: HTMLElement;62private readonly _showStore = this._store.add(new DisposableStore());63private readonly _navigationBearings = observableValue<{ activeIdx: number; totalCount: number }>(this, { activeIdx: -1, totalCount: 0 });6465constructor(66@IInstantiationService private readonly _instaService: IInstantiationService,67@IKeybindingService private readonly _keybindingService: IKeybindingService,68) {69super();7071this._domNode = document.createElement('div');72this._domNode.classList.add('agent-feedback-editor-overlay-widget');7374this._toolbarNode = document.createElement('div');75this._toolbarNode.classList.add('agent-feedback-editor-overlay-toolbar');76}7778getDomNode(): HTMLElement {79return this._domNode;80}8182show(navigationBearings: { activeIdx: number; totalCount: number }): void {83this._showStore.clear();84this._navigationBearings.set(navigationBearings, undefined);8586if (!this._domNode.contains(this._toolbarNode)) {87this._domNode.appendChild(this._toolbarNode);88}8990this._showStore.add(this._instaService.createInstance(MenuWorkbenchToolBar, this._toolbarNode, Menus.AgentFeedbackEditorContent, {91telemetrySource: 'agentFeedback.overlayToolbar',92hiddenItemStrategy: HiddenItemStrategy.Ignore,93toolbarOptions: {94primaryGroup: () => true,95useSeparatorsInPrimaryActions: true96},97menuOptions: { renderShortTitle: true },98actionViewItemProvider: (action, options) => {99if (action.id === navigationBearingFakeActionId) {100const that = this;101return new class extends ActionViewItem {102constructor() {103super(undefined, action, { ...options, icon: false, label: true, keybindingNotRenderedWithLabel: true });104}105106override render(container: HTMLElement): void {107super.render(container);108container.classList.add('label-item');109110this._store.add(autorun(r => {111assertType(this.label);112const { activeIdx, totalCount } = that._navigationBearings.read(r);113if (totalCount > 0) {114const current = activeIdx === -1 ? 1 : activeIdx + 1;115this.label.innerText = localize('nOfM', '{0}/{1}', current, totalCount);116} else {117this.label.innerText = localize('zero', '0/0');118}119}));120}121};122}123124return new AgentFeedbackActionViewItem(action, options, this._keybindingService);125},126}));127this._showStore.add(toDisposable(() => this._toolbarNode.remove()));128}129130hide(): void {131this._showStore.clear();132this._navigationBearings.set({ activeIdx: -1, totalCount: 0 }, undefined);133this._toolbarNode.remove();134}135}136137class AgentFeedbackOverlayController {138139private readonly _store = new DisposableStore();140private readonly _domNode = document.createElement('div');141142constructor(143container: HTMLElement,144group: IEditorGroup,145@IAgentFeedbackService agentFeedbackService: IAgentFeedbackService,146@ISessionsManagementService sessionsManagementService: ISessionsManagementService,147@IInstantiationService instaService: IInstantiationService,148@IChatEditingService chatEditingService: IChatEditingService,149@IContextKeyService contextKeyService: IContextKeyService,150@ICodeReviewService codeReviewService: ICodeReviewService,151) {152this._domNode.classList.add('agent-feedback-editor-overlay');153this._domNode.style.position = 'absolute';154this._domNode.style.bottom = '24px';155this._domNode.style.right = '24px';156this._domNode.style.zIndex = '100';157158const widget = this._store.add(instaService.createInstance(AgentFeedbackOverlayWidget));159this._domNode.appendChild(widget.getDomNode());160this._store.add(toDisposable(() => this._domNode.remove()));161const hasCommentsContext = hasSessionEditorComments.bindTo(contextKeyService);162const hasAgentFeedbackContext = hasSessionAgentFeedback.bindTo(contextKeyService);163164const show = () => {165if (!container.contains(this._domNode)) {166container.appendChild(this._domNode);167}168};169170const hide = () => {171if (container.contains(this._domNode)) {172widget.hide();173this._domNode.remove();174}175};176177const activeSignal = observableSignalFromEvent(this, Event.any(178group.onDidActiveEditorChange,179group.onDidModelChange,180agentFeedbackService.onDidChangeFeedback,181agentFeedbackService.onDidChangeNavigation,182));183184this._store.add(autorun(r => {185activeSignal.read(r);186187const candidates = getActiveResourceCandidates(group.activeEditorPane?.input);188let navigationBearings = undefined;189let hasAgentFeedback = false;190for (const candidate of candidates) {191const sessionResource = getSessionForResource(candidate, chatEditingService, sessionsManagementService);192if (!sessionResource) {193continue;194}195196const comments = getSessionEditorComments(197sessionResource,198agentFeedbackService.getFeedback(sessionResource),199codeReviewService.getReviewState(sessionResource).read(r),200codeReviewService.getPRReviewState(sessionResource).read(r),201);202if (comments.length > 0) {203navigationBearings = agentFeedbackService.getNavigationBearing(sessionResource, comments);204hasAgentFeedback = hasAgentFeedbackComments(comments);205break;206}207}208209if (!navigationBearings) {210hasCommentsContext.set(false);211hasAgentFeedbackContext.set(false);212hide();213return;214}215216hasCommentsContext.set(true);217hasAgentFeedbackContext.set(hasAgentFeedback);218widget.show(navigationBearings);219show();220}));221}222223dispose(): void {224this._store.dispose();225}226}227228export class AgentFeedbackEditorOverlay implements IWorkbenchContribution {229230static readonly ID = 'chat.agentFeedback.editorOverlay';231232private readonly _store = new DisposableStore();233234constructor(235@IEditorGroupsService editorGroupsService: IEditorGroupsService,236@IInstantiationService instantiationService: IInstantiationService,237) {238const editorGroups = observableFromEvent(239this,240Event.any(editorGroupsService.onDidAddGroup, editorGroupsService.onDidRemoveGroup),241() => editorGroupsService.groups242);243244const overlayWidgets = this._store.add(new DisposableMap<IEditorGroup>());245246this._store.add(autorun(r => {247const groups = editorGroups.read(r);248const toDelete = new Set(overlayWidgets.keys());249250for (const group of groups) {251if (!(group instanceof EditorGroupView)) {252continue;253}254255toDelete.delete(group);256257if (!overlayWidgets.has(group)) {258const scopedInstaService = instantiationService.createChild(259new ServiceCollection([IContextKeyService, group.scopedContextKeyService])260);261262const ctrl = scopedInstaService.createInstance(AgentFeedbackOverlayController, group.element, group);263overlayWidgets.set(group, combinedDisposable(ctrl, scopedInstaService));264}265}266267for (const group of toDelete) {268overlayWidgets.deleteAndDispose(group);269}270}));271}272273dispose(): void {274this._store.dispose();275}276}277278279