Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/sessions/contrib/agentFeedback/browser/agentFeedbackEditorOverlay.ts
13401 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import './media/agentFeedbackEditorOverlay.css';
7
import { Disposable, DisposableMap, DisposableStore, combinedDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
8
import { autorun, observableFromEvent, observableSignalFromEvent, observableValue } from '../../../../base/common/observable.js';
9
import { ActionViewItem, IBaseActionViewItemOptions } from '../../../../base/browser/ui/actionbar/actionViewItems.js';
10
import { IAction } from '../../../../base/common/actions.js';
11
import { Event } from '../../../../base/common/event.js';
12
import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js';
13
import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
14
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
15
import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js';
16
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
17
import { IWorkbenchContribution } from '../../../../workbench/common/contributions.js';
18
import { EditorGroupView } from '../../../../workbench/browser/parts/editor/editorGroupView.js';
19
import { IEditorGroup, IEditorGroupsService } from '../../../../workbench/services/editor/common/editorGroupsService.js';
20
import { IAgentFeedbackService } from './agentFeedbackService.js';
21
import { hasSessionAgentFeedback, hasSessionEditorComments, navigateNextFeedbackActionId, navigatePreviousFeedbackActionId, navigationBearingFakeActionId, submitFeedbackActionId } from './agentFeedbackEditorActions.js';
22
import { assertType } from '../../../../base/common/types.js';
23
import { localize } from '../../../../nls.js';
24
import { getActiveResourceCandidates, getSessionForResource } from './agentFeedbackEditorUtils.js';
25
import { Menus } from '../../../browser/menus.js';
26
import { IChatEditingService } from '../../../../workbench/contrib/chat/common/editing/chatEditingService.js';
27
import { ICodeReviewService } from '../../codeReview/browser/codeReviewService.js';
28
import { getSessionEditorComments, hasAgentFeedbackComments } from './sessionEditorComments.js';
29
import { ISessionsManagementService } from '../../../services/sessions/common/sessionsManagement.js';
30
31
class AgentFeedbackActionViewItem extends ActionViewItem {
32
33
constructor(
34
action: IAction,
35
options: IBaseActionViewItemOptions,
36
private readonly _keybindingService: IKeybindingService,
37
private readonly _primaryActionIds: readonly string[] = [submitFeedbackActionId],
38
) {
39
const isIconOnly = action.id === navigatePreviousFeedbackActionId || action.id === navigateNextFeedbackActionId;
40
super(undefined, action, { ...options, icon: isIconOnly, label: !isIconOnly, keybindingNotRenderedWithLabel: true });
41
}
42
43
override render(container: HTMLElement): void {
44
super.render(container);
45
if (this._primaryActionIds.includes(this._action.id)) {
46
this.element?.classList.add('primary');
47
}
48
}
49
50
protected override getTooltip(): string | undefined {
51
const value = super.getTooltip();
52
if (!value || this.options.keybinding) {
53
return value;
54
}
55
return this._keybindingService.appendKeybinding(value, this._action.id);
56
}
57
}
58
59
export class AgentFeedbackOverlayWidget extends Disposable {
60
61
private readonly _domNode: HTMLElement;
62
private readonly _toolbarNode: HTMLElement;
63
private readonly _showStore = this._store.add(new DisposableStore());
64
private readonly _navigationBearings = observableValue<{ activeIdx: number; totalCount: number }>(this, { activeIdx: -1, totalCount: 0 });
65
66
constructor(
67
@IInstantiationService private readonly _instaService: IInstantiationService,
68
@IKeybindingService private readonly _keybindingService: IKeybindingService,
69
) {
70
super();
71
72
this._domNode = document.createElement('div');
73
this._domNode.classList.add('agent-feedback-editor-overlay-widget');
74
75
this._toolbarNode = document.createElement('div');
76
this._toolbarNode.classList.add('agent-feedback-editor-overlay-toolbar');
77
}
78
79
getDomNode(): HTMLElement {
80
return this._domNode;
81
}
82
83
show(navigationBearings: { activeIdx: number; totalCount: number }): void {
84
this._showStore.clear();
85
this._navigationBearings.set(navigationBearings, undefined);
86
87
if (!this._domNode.contains(this._toolbarNode)) {
88
this._domNode.appendChild(this._toolbarNode);
89
}
90
91
this._showStore.add(this._instaService.createInstance(MenuWorkbenchToolBar, this._toolbarNode, Menus.AgentFeedbackEditorContent, {
92
telemetrySource: 'agentFeedback.overlayToolbar',
93
hiddenItemStrategy: HiddenItemStrategy.Ignore,
94
toolbarOptions: {
95
primaryGroup: () => true,
96
useSeparatorsInPrimaryActions: true
97
},
98
menuOptions: { renderShortTitle: true },
99
actionViewItemProvider: (action, options) => {
100
if (action.id === navigationBearingFakeActionId) {
101
const that = this;
102
return new class extends ActionViewItem {
103
constructor() {
104
super(undefined, action, { ...options, icon: false, label: true, keybindingNotRenderedWithLabel: true });
105
}
106
107
override render(container: HTMLElement): void {
108
super.render(container);
109
container.classList.add('label-item');
110
111
this._store.add(autorun(r => {
112
assertType(this.label);
113
const { activeIdx, totalCount } = that._navigationBearings.read(r);
114
if (totalCount > 0) {
115
const current = activeIdx === -1 ? 1 : activeIdx + 1;
116
this.label.innerText = localize('nOfM', '{0}/{1}', current, totalCount);
117
} else {
118
this.label.innerText = localize('zero', '0/0');
119
}
120
}));
121
}
122
};
123
}
124
125
return new AgentFeedbackActionViewItem(action, options, this._keybindingService);
126
},
127
}));
128
this._showStore.add(toDisposable(() => this._toolbarNode.remove()));
129
}
130
131
hide(): void {
132
this._showStore.clear();
133
this._navigationBearings.set({ activeIdx: -1, totalCount: 0 }, undefined);
134
this._toolbarNode.remove();
135
}
136
}
137
138
class AgentFeedbackOverlayController {
139
140
private readonly _store = new DisposableStore();
141
private readonly _domNode = document.createElement('div');
142
143
constructor(
144
container: HTMLElement,
145
group: IEditorGroup,
146
@IAgentFeedbackService agentFeedbackService: IAgentFeedbackService,
147
@ISessionsManagementService sessionsManagementService: ISessionsManagementService,
148
@IInstantiationService instaService: IInstantiationService,
149
@IChatEditingService chatEditingService: IChatEditingService,
150
@IContextKeyService contextKeyService: IContextKeyService,
151
@ICodeReviewService codeReviewService: ICodeReviewService,
152
) {
153
this._domNode.classList.add('agent-feedback-editor-overlay');
154
this._domNode.style.position = 'absolute';
155
this._domNode.style.bottom = '24px';
156
this._domNode.style.right = '24px';
157
this._domNode.style.zIndex = '100';
158
159
const widget = this._store.add(instaService.createInstance(AgentFeedbackOverlayWidget));
160
this._domNode.appendChild(widget.getDomNode());
161
this._store.add(toDisposable(() => this._domNode.remove()));
162
const hasCommentsContext = hasSessionEditorComments.bindTo(contextKeyService);
163
const hasAgentFeedbackContext = hasSessionAgentFeedback.bindTo(contextKeyService);
164
165
const show = () => {
166
if (!container.contains(this._domNode)) {
167
container.appendChild(this._domNode);
168
}
169
};
170
171
const hide = () => {
172
if (container.contains(this._domNode)) {
173
widget.hide();
174
this._domNode.remove();
175
}
176
};
177
178
const activeSignal = observableSignalFromEvent(this, Event.any(
179
group.onDidActiveEditorChange,
180
group.onDidModelChange,
181
agentFeedbackService.onDidChangeFeedback,
182
agentFeedbackService.onDidChangeNavigation,
183
));
184
185
this._store.add(autorun(r => {
186
activeSignal.read(r);
187
188
const candidates = getActiveResourceCandidates(group.activeEditorPane?.input);
189
let navigationBearings = undefined;
190
let hasAgentFeedback = false;
191
for (const candidate of candidates) {
192
const sessionResource = getSessionForResource(candidate, chatEditingService, sessionsManagementService);
193
if (!sessionResource) {
194
continue;
195
}
196
197
const comments = getSessionEditorComments(
198
sessionResource,
199
agentFeedbackService.getFeedback(sessionResource),
200
codeReviewService.getReviewState(sessionResource).read(r),
201
codeReviewService.getPRReviewState(sessionResource).read(r),
202
);
203
if (comments.length > 0) {
204
navigationBearings = agentFeedbackService.getNavigationBearing(sessionResource, comments);
205
hasAgentFeedback = hasAgentFeedbackComments(comments);
206
break;
207
}
208
}
209
210
if (!navigationBearings) {
211
hasCommentsContext.set(false);
212
hasAgentFeedbackContext.set(false);
213
hide();
214
return;
215
}
216
217
hasCommentsContext.set(true);
218
hasAgentFeedbackContext.set(hasAgentFeedback);
219
widget.show(navigationBearings);
220
show();
221
}));
222
}
223
224
dispose(): void {
225
this._store.dispose();
226
}
227
}
228
229
export class AgentFeedbackEditorOverlay implements IWorkbenchContribution {
230
231
static readonly ID = 'chat.agentFeedback.editorOverlay';
232
233
private readonly _store = new DisposableStore();
234
235
constructor(
236
@IEditorGroupsService editorGroupsService: IEditorGroupsService,
237
@IInstantiationService instantiationService: IInstantiationService,
238
) {
239
const editorGroups = observableFromEvent(
240
this,
241
Event.any(editorGroupsService.onDidAddGroup, editorGroupsService.onDidRemoveGroup),
242
() => editorGroupsService.groups
243
);
244
245
const overlayWidgets = this._store.add(new DisposableMap<IEditorGroup>());
246
247
this._store.add(autorun(r => {
248
const groups = editorGroups.read(r);
249
const toDelete = new Set(overlayWidgets.keys());
250
251
for (const group of groups) {
252
if (!(group instanceof EditorGroupView)) {
253
continue;
254
}
255
256
toDelete.delete(group);
257
258
if (!overlayWidgets.has(group)) {
259
const scopedInstaService = instantiationService.createChild(
260
new ServiceCollection([IContextKeyService, group.scopedContextKeyService])
261
);
262
263
const ctrl = scopedInstaService.createInstance(AgentFeedbackOverlayController, group.element, group);
264
overlayWidgets.set(group, combinedDisposable(ctrl, scopedInstaService));
265
}
266
}
267
268
for (const group of toDelete) {
269
overlayWidgets.deleteAndDispose(group);
270
}
271
}));
272
}
273
274
dispose(): void {
275
this._store.dispose();
276
}
277
}
278
279