Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationWidget.ts
3296 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 * as dom from '../../../../../base/browser/dom.js';
7
import { renderAsPlaintext } from '../../../../../base/browser/markdownRenderer.js';
8
import { Button, ButtonWithDropdown, IButton, IButtonOptions } from '../../../../../base/browser/ui/button/button.js';
9
import { Action, Separator } from '../../../../../base/common/actions.js';
10
import { Emitter, Event } from '../../../../../base/common/event.js';
11
import { IMarkdownString, MarkdownString } from '../../../../../base/common/htmlContent.js';
12
import { Disposable, DisposableStore, MutableDisposable } from '../../../../../base/common/lifecycle.js';
13
import type { ThemeIcon } from '../../../../../base/common/themables.js';
14
import { IMarkdownRenderResult, MarkdownRenderer } from '../../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js';
15
import { localize } from '../../../../../nls.js';
16
import { MenuWorkbenchToolBar } from '../../../../../platform/actions/browser/toolbar.js';
17
import { MenuId } from '../../../../../platform/actions/common/actions.js';
18
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
19
import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';
20
import { IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js';
21
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
22
import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js';
23
import { FocusMode } from '../../../../../platform/native/common/native.js';
24
import { defaultButtonStyles } from '../../../../../platform/theme/browser/defaultStyles.js';
25
import { IHostService } from '../../../../services/host/browser/host.js';
26
import { IViewsService } from '../../../../services/views/common/viewsService.js';
27
import { showChatView } from '../chat.js';
28
import './media/chatConfirmationWidget.css';
29
30
export interface IChatConfirmationButton<T> {
31
label: string;
32
isSecondary?: boolean;
33
tooltip?: string;
34
data: T;
35
disabled?: boolean;
36
onDidChangeDisablement?: Event<boolean>;
37
moreActions?: (IChatConfirmationButton<T> | Separator)[];
38
}
39
40
export interface IChatConfirmationWidgetOptions<T> {
41
title: string | IMarkdownString;
42
message: string | IMarkdownString;
43
subtitle?: string | IMarkdownString;
44
buttons: IChatConfirmationButton<T>[];
45
toolbarData?: { arg: any; partType: string; partSource?: string };
46
}
47
48
export class ChatQueryTitlePart extends Disposable {
49
private readonly _onDidChangeHeight = this._register(new Emitter<void>());
50
public readonly onDidChangeHeight = this._onDidChangeHeight.event;
51
private readonly _renderedTitle = this._register(new MutableDisposable<IMarkdownRenderResult>());
52
53
public get title() {
54
return this._title;
55
}
56
57
public set title(value: string | IMarkdownString) {
58
this._title = value;
59
60
const next = this._renderer.render(this.toMdString(value), {
61
asyncRenderCallback: () => this._onDidChangeHeight.fire(),
62
});
63
64
const previousEl = this._renderedTitle.value?.element;
65
if (previousEl?.parentElement) {
66
previousEl.parentElement.replaceChild(next.element, previousEl);
67
} else {
68
this.element.appendChild(next.element); // unreachable?
69
}
70
71
this._renderedTitle.value = next;
72
}
73
74
constructor(
75
private readonly element: HTMLElement,
76
private _title: IMarkdownString | string,
77
subtitle: string | IMarkdownString | undefined,
78
private readonly _renderer: MarkdownRenderer,
79
) {
80
super();
81
82
element.classList.add('chat-query-title-part');
83
84
this._renderedTitle.value = _renderer.render(this.toMdString(_title), {
85
asyncRenderCallback: () => this._onDidChangeHeight.fire(),
86
});
87
element.append(this._renderedTitle.value.element);
88
if (subtitle) {
89
const str = this.toMdString(subtitle);
90
const renderedTitle = this._register(_renderer.render(str, {
91
asyncRenderCallback: () => this._onDidChangeHeight.fire(),
92
}));
93
const wrapper = document.createElement('small');
94
wrapper.appendChild(renderedTitle.element);
95
element.append(wrapper);
96
}
97
}
98
99
private toMdString(value: string | IMarkdownString) {
100
if (typeof value === 'string') {
101
return new MarkdownString('', { supportThemeIcons: true }).appendText(value);
102
} else {
103
return new MarkdownString(value.value, { supportThemeIcons: true, isTrusted: value.isTrusted });
104
}
105
}
106
}
107
108
abstract class BaseSimpleChatConfirmationWidget<T> extends Disposable {
109
private _onDidClick = this._register(new Emitter<IChatConfirmationButton<T>>());
110
get onDidClick(): Event<IChatConfirmationButton<T>> { return this._onDidClick.event; }
111
112
protected _onDidChangeHeight = this._register(new Emitter<void>());
113
get onDidChangeHeight(): Event<void> { return this._onDidChangeHeight.event; }
114
115
private _domNode: HTMLElement;
116
get domNode(): HTMLElement {
117
return this._domNode;
118
}
119
120
private get showingButtons() {
121
return !this.domNode.classList.contains('hideButtons');
122
}
123
124
setShowButtons(showButton: boolean): void {
125
this.domNode.classList.toggle('hideButtons', !showButton);
126
}
127
128
private readonly messageElement: HTMLElement;
129
protected readonly markdownRenderer: MarkdownRenderer;
130
private readonly title: string | IMarkdownString;
131
132
private readonly notification = this._register(new MutableDisposable<DisposableStore>());
133
134
constructor(
135
options: IChatConfirmationWidgetOptions<T>,
136
@IInstantiationService protected readonly instantiationService: IInstantiationService,
137
@IContextMenuService contextMenuService: IContextMenuService,
138
@IConfigurationService private readonly _configurationService: IConfigurationService,
139
@IHostService private readonly _hostService: IHostService,
140
@IViewsService private readonly _viewsService: IViewsService,
141
@IContextKeyService contextKeyService: IContextKeyService,
142
) {
143
super();
144
145
const { title, subtitle, message, buttons } = options;
146
this.title = title;
147
148
149
const elements = dom.h('.chat-confirmation-widget-container@container', [
150
dom.h('.chat-confirmation-widget@root', [
151
dom.h('.chat-confirmation-widget-title@title'),
152
dom.h('.chat-confirmation-widget-message@message'),
153
dom.h('.chat-buttons-container@buttonsContainer', [
154
dom.h('.chat-buttons@buttons'),
155
dom.h('.chat-toolbar@toolbar'),
156
]),
157
]),
158
]);
159
configureAccessibilityContainer(elements.container, title, message);
160
this._domNode = elements.root;
161
this.markdownRenderer = this.instantiationService.createInstance(MarkdownRenderer, {});
162
163
const titlePart = this._register(instantiationService.createInstance(
164
ChatQueryTitlePart,
165
elements.title,
166
title,
167
subtitle,
168
this.markdownRenderer,
169
));
170
171
this._register(titlePart.onDidChangeHeight(() => this._onDidChangeHeight.fire()));
172
173
this.messageElement = elements.message;
174
175
// Create buttons
176
buttons.forEach(buttonData => {
177
const buttonOptions: IButtonOptions = { ...defaultButtonStyles, secondary: buttonData.isSecondary, title: buttonData.tooltip, disabled: buttonData.disabled };
178
179
let button: IButton;
180
if (buttonData.moreActions) {
181
button = new ButtonWithDropdown(elements.buttons, {
182
...buttonOptions,
183
contextMenuProvider: contextMenuService,
184
addPrimaryActionToDropdown: false,
185
actions: buttonData.moreActions.map(action => {
186
if (action instanceof Separator) {
187
return action;
188
}
189
return this._register(new Action(
190
action.label,
191
action.label,
192
undefined,
193
!action.disabled,
194
() => {
195
this._onDidClick.fire(action);
196
return Promise.resolve();
197
},
198
));
199
}),
200
});
201
} else {
202
button = new Button(elements.buttons, buttonOptions);
203
}
204
205
this._register(button);
206
button.label = buttonData.label;
207
this._register(button.onDidClick(() => this._onDidClick.fire(buttonData)));
208
if (buttonData.onDidChangeDisablement) {
209
this._register(buttonData.onDidChangeDisablement(disabled => button.enabled = !disabled));
210
}
211
});
212
213
// Create toolbar if actions are provided
214
if (options?.toolbarData) {
215
const overlay = contextKeyService.createOverlay([
216
['chatConfirmationPartType', options.toolbarData.partType],
217
['chatConfirmationPartSource', options.toolbarData.partSource],
218
]);
219
const nestedInsta = this._register(instantiationService.createChild(new ServiceCollection([IContextKeyService, overlay])));
220
this._register(nestedInsta.createInstance(
221
MenuWorkbenchToolBar,
222
elements.toolbar,
223
MenuId.ChatConfirmationMenu,
224
{
225
// buttonConfigProvider: () => ({ showLabel: false, showIcon: true }),
226
menuOptions: {
227
arg: options.toolbarData.arg,
228
shouldForwardArgs: true,
229
}
230
}
231
));
232
}
233
}
234
235
protected renderMessage(element: HTMLElement, listContainer: HTMLElement): void {
236
this.messageElement.append(element);
237
238
if (this.showingButtons && this._configurationService.getValue<boolean>('chat.notifyWindowOnConfirmation')) {
239
const targetWindow = dom.getWindow(listContainer);
240
if (!targetWindow.document.hasFocus()) {
241
this.notifyConfirmationNeeded(targetWindow);
242
}
243
}
244
}
245
246
private async notifyConfirmationNeeded(targetWindow: Window): Promise<void> {
247
248
// Focus Window
249
this._hostService.focus(targetWindow, { mode: FocusMode.Notify });
250
251
// Notify
252
const title = renderAsPlaintext(this.title);
253
const notification = await dom.triggerNotification(title ? localize('notificationTitle', "Chat: {0}", title) : localize('defaultTitle', "Chat: Confirmation Required"),
254
{
255
detail: localize('notificationDetail', "The current chat session requires your confirmation to proceed.")
256
}
257
);
258
if (notification) {
259
const disposables = this.notification.value = new DisposableStore();
260
disposables.add(notification);
261
262
disposables.add(Event.once(notification.onClick)(() => {
263
this._hostService.focus(targetWindow, { mode: FocusMode.Force });
264
showChatView(this._viewsService);
265
}));
266
267
disposables.add(this._hostService.onDidChangeFocus(focus => {
268
if (focus) {
269
disposables.dispose();
270
}
271
}));
272
}
273
}
274
}
275
276
/** @deprecated Use ChatConfirmationWidget instead */
277
export class SimpleChatConfirmationWidget<T> extends BaseSimpleChatConfirmationWidget<T> {
278
private _renderedMessage: HTMLElement | undefined;
279
280
constructor(
281
private readonly _container: HTMLElement,
282
options: IChatConfirmationWidgetOptions<T>,
283
@IInstantiationService instantiationService: IInstantiationService,
284
@IContextMenuService contextMenuService: IContextMenuService,
285
@IConfigurationService configurationService: IConfigurationService,
286
@IHostService hostService: IHostService,
287
@IViewsService viewsService: IViewsService,
288
@IContextKeyService contextKeyService: IContextKeyService,
289
) {
290
super(options, instantiationService, contextMenuService, configurationService, hostService, viewsService, contextKeyService);
291
this.updateMessage(options.message);
292
}
293
294
public updateMessage(message: string | IMarkdownString): void {
295
this._renderedMessage?.remove();
296
const renderedMessage = this._register(this.markdownRenderer.render(
297
typeof message === 'string' ? new MarkdownString(message) : message,
298
{ asyncRenderCallback: () => this._onDidChangeHeight.fire() }
299
));
300
this.renderMessage(renderedMessage.element, this._container);
301
this._renderedMessage = renderedMessage.element;
302
}
303
}
304
305
export interface IChatConfirmationWidget2Options<T> {
306
title: string | IMarkdownString;
307
message: string | IMarkdownString | HTMLElement;
308
icon?: ThemeIcon;
309
subtitle?: string | IMarkdownString;
310
buttons: IChatConfirmationButton<T>[];
311
toolbarData?: { arg: any; partType: string; partSource?: string };
312
}
313
314
abstract class BaseChatConfirmationWidget<T> extends Disposable {
315
private _onDidClick = this._register(new Emitter<IChatConfirmationButton<T>>());
316
get onDidClick(): Event<IChatConfirmationButton<T>> { return this._onDidClick.event; }
317
318
protected _onDidChangeHeight = this._register(new Emitter<void>());
319
get onDidChangeHeight(): Event<void> { return this._onDidChangeHeight.event; }
320
321
private _domNode: HTMLElement;
322
get domNode(): HTMLElement {
323
return this._domNode;
324
}
325
326
private _buttonsDomNode: HTMLElement;
327
328
private get showingButtons() {
329
return !this.domNode.classList.contains('hideButtons');
330
}
331
332
setShowButtons(showButton: boolean): void {
333
this.domNode.classList.toggle('hideButtons', !showButton);
334
}
335
336
private readonly messageElement: HTMLElement;
337
protected readonly markdownRenderer: MarkdownRenderer;
338
private readonly title: string | IMarkdownString;
339
340
private readonly notification = this._register(new MutableDisposable<DisposableStore>());
341
342
constructor(
343
options: IChatConfirmationWidget2Options<T>,
344
@IInstantiationService protected readonly instantiationService: IInstantiationService,
345
@IContextMenuService private readonly contextMenuService: IContextMenuService,
346
@IConfigurationService private readonly _configurationService: IConfigurationService,
347
@IHostService private readonly _hostService: IHostService,
348
@IViewsService private readonly _viewsService: IViewsService,
349
@IContextKeyService contextKeyService: IContextKeyService,
350
) {
351
super();
352
353
const { title, subtitle, message, buttons, icon } = options;
354
this.title = title;
355
356
const elements = dom.h('.chat-confirmation-widget-container@container', [
357
dom.h('.chat-confirmation-widget2@root', [
358
dom.h('.chat-confirmation-widget-title', [
359
dom.h('.chat-title@title'),
360
dom.h('.chat-toolbar-container@buttonsContainer', [
361
dom.h('.chat-toolbar@toolbar'),
362
]),
363
]),
364
dom.h('.chat-confirmation-widget-message@message'),
365
dom.h('.chat-confirmation-widget-buttons', [
366
dom.h('.chat-buttons@buttons'),
367
]),
368
]),]);
369
370
configureAccessibilityContainer(elements.container, title, message);
371
this._domNode = elements.root;
372
this._buttonsDomNode = elements.buttons;
373
374
this.markdownRenderer = this.instantiationService.createInstance(MarkdownRenderer, {});
375
376
const titlePart = this._register(instantiationService.createInstance(
377
ChatQueryTitlePart,
378
elements.title,
379
new MarkdownString(icon ? `$(${icon.id}) ${typeof title === 'string' ? title : title.value}` : typeof title === 'string' ? title : title.value),
380
subtitle,
381
this.markdownRenderer,
382
));
383
384
this._register(titlePart.onDidChangeHeight(() => this._onDidChangeHeight.fire()));
385
386
this.messageElement = elements.message;
387
388
this.updateButtons(buttons);
389
390
// Create toolbar if actions are provided
391
if (options?.toolbarData) {
392
const overlay = contextKeyService.createOverlay([
393
['chatConfirmationPartType', options.toolbarData.partType],
394
['chatConfirmationPartSource', options.toolbarData.partSource],
395
]);
396
const nestedInsta = this._register(instantiationService.createChild(new ServiceCollection([IContextKeyService, overlay])));
397
this._register(nestedInsta.createInstance(
398
MenuWorkbenchToolBar,
399
elements.toolbar,
400
MenuId.ChatConfirmationMenu,
401
{
402
// buttonConfigProvider: () => ({ showLabel: false, showIcon: true }),
403
menuOptions: {
404
arg: options.toolbarData.arg,
405
shouldForwardArgs: true,
406
}
407
}
408
));
409
}
410
}
411
412
updateButtons(buttons: IChatConfirmationButton<T>[]) {
413
while (this._buttonsDomNode.children.length > 0) {
414
this._buttonsDomNode.children[0].remove();
415
}
416
for (const buttonData of buttons) {
417
const buttonOptions: IButtonOptions = { ...defaultButtonStyles, secondary: buttonData.isSecondary, title: buttonData.tooltip, disabled: buttonData.disabled };
418
419
let button: IButton;
420
if (buttonData.moreActions) {
421
button = new ButtonWithDropdown(this._buttonsDomNode, {
422
...buttonOptions,
423
contextMenuProvider: this.contextMenuService,
424
addPrimaryActionToDropdown: false,
425
actions: buttonData.moreActions.map(action => {
426
if (action instanceof Separator) {
427
return action;
428
}
429
return this._register(new Action(
430
action.label,
431
action.label,
432
undefined,
433
!action.disabled,
434
() => {
435
this._onDidClick.fire(action);
436
return Promise.resolve();
437
},
438
));
439
}),
440
});
441
} else {
442
button = new Button(this._buttonsDomNode, buttonOptions);
443
}
444
445
this._register(button);
446
button.label = buttonData.label;
447
this._register(button.onDidClick(() => this._onDidClick.fire(buttonData)));
448
if (buttonData.onDidChangeDisablement) {
449
this._register(buttonData.onDidChangeDisablement(disabled => button.enabled = !disabled));
450
}
451
}
452
}
453
454
protected renderMessage(element: HTMLElement | IMarkdownString | string, listContainer: HTMLElement): void {
455
if (!dom.isHTMLElement(element)) {
456
const messageElement = this._register(this.markdownRenderer.render(
457
typeof element === 'string' ? new MarkdownString(element) : element,
458
{ asyncRenderCallback: () => this._onDidChangeHeight.fire() }
459
));
460
element = messageElement.element;
461
}
462
463
for (const child of this.messageElement.children) {
464
child.remove();
465
}
466
this.messageElement.append(element);
467
468
if (this.showingButtons && this._configurationService.getValue<boolean>('chat.notifyWindowOnConfirmation')) {
469
const targetWindow = dom.getWindow(listContainer);
470
if (!targetWindow.document.hasFocus()) {
471
this.notifyConfirmationNeeded(targetWindow);
472
}
473
}
474
}
475
476
private async notifyConfirmationNeeded(targetWindow: Window): Promise<void> {
477
478
// Focus Window
479
this._hostService.focus(targetWindow, { mode: FocusMode.Notify });
480
481
// Notify
482
const title = renderAsPlaintext(this.title);
483
const notification = await dom.triggerNotification(title ? localize('notificationTitle', "Chat: {0}", title) : localize('defaultTitle', "Chat: Confirmation Required"),
484
{
485
detail: localize('notificationDetail', "The current chat session requires your confirmation to proceed.")
486
}
487
);
488
if (notification) {
489
const disposables = this.notification.value = new DisposableStore();
490
disposables.add(notification);
491
492
disposables.add(Event.once(notification.onClick)(() => {
493
this._hostService.focus(targetWindow, { mode: FocusMode.Force });
494
showChatView(this._viewsService);
495
}));
496
497
disposables.add(this._hostService.onDidChangeFocus(focus => {
498
if (focus) {
499
disposables.dispose();
500
}
501
}));
502
}
503
}
504
}
505
export class ChatConfirmationWidget<T> extends BaseChatConfirmationWidget<T> {
506
private _renderedMessage: HTMLElement | undefined;
507
508
constructor(
509
private readonly _container: HTMLElement,
510
options: IChatConfirmationWidget2Options<T>,
511
@IInstantiationService instantiationService: IInstantiationService,
512
@IContextMenuService contextMenuService: IContextMenuService,
513
@IConfigurationService configurationService: IConfigurationService,
514
@IHostService hostService: IHostService,
515
@IViewsService viewsService: IViewsService,
516
@IContextKeyService contextKeyService: IContextKeyService,
517
) {
518
super(options, instantiationService, contextMenuService, configurationService, hostService, viewsService, contextKeyService);
519
this.renderMessage(options.message, this._container);
520
}
521
522
public updateMessage(message: string | IMarkdownString): void {
523
this._renderedMessage?.remove();
524
const renderedMessage = this._register(this.markdownRenderer.render(
525
typeof message === 'string' ? new MarkdownString(message) : message,
526
{ asyncRenderCallback: () => this._onDidChangeHeight.fire() }
527
));
528
this.renderMessage(renderedMessage.element, this._container);
529
this._renderedMessage = renderedMessage.element;
530
}
531
}
532
export class ChatCustomConfirmationWidget<T> extends BaseChatConfirmationWidget<T> {
533
constructor(
534
container: HTMLElement,
535
options: IChatConfirmationWidget2Options<T>,
536
@IInstantiationService instantiationService: IInstantiationService,
537
@IContextMenuService contextMenuService: IContextMenuService,
538
@IConfigurationService configurationService: IConfigurationService,
539
@IHostService hostService: IHostService,
540
@IViewsService viewsService: IViewsService,
541
@IContextKeyService contextKeyService: IContextKeyService,
542
) {
543
super(options, instantiationService, contextMenuService, configurationService, hostService, viewsService, contextKeyService);
544
this.renderMessage(options.message, container);
545
}
546
}
547
548
function configureAccessibilityContainer(container: HTMLElement, title: string | IMarkdownString, message?: string | IMarkdownString | HTMLElement): void {
549
container.tabIndex = 0;
550
const titleAsString = typeof title === 'string' ? title : title.value;
551
const messageAsString = typeof message === 'string' ? message : message && 'value' in message ? message.value : message && 'textContent' in message ? message.textContent : '';
552
container.setAttribute('aria-label', localize('chat.confirmationWidget.ariaLabel', "Chat Confirmation Dialog {0} {1}", titleAsString, messageAsString));
553
container.classList.add('chat-confirmation-widget-container');
554
}
555
556