Skip to content

Commit 7c8f422

Browse files
authored
Merge pull request microsoft#260770 from microsoft/tyriar/257468
Update confirmations to use new proposed terminal style
2 parents ef41842 + 19e3d7f commit 7c8f422

17 files changed

+418
-61
lines changed

src/vs/base/common/lifecycle.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -818,3 +818,18 @@ export function thenIfNotDisposed<T>(promise: Promise<T>, then: (result: T) => v
818818
disposed = true;
819819
});
820820
}
821+
822+
/**
823+
* Call `then` on a promise that resolves to a {@link IDisposable}, then either register the
824+
* disposable or register it to the {@link DisposableStore}, depending on whether the store is
825+
* disposed or not.
826+
*/
827+
export function thenRegisterOrDispose<T extends IDisposable>(promise: Promise<T>, store: DisposableStore): void {
828+
promise.then(ref => {
829+
if (store.isDisposed) {
830+
ref.dispose();
831+
} else {
832+
store.add(ref);
833+
}
834+
});
835+
}

src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationContentPart.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { IChatProgressRenderableResponseContent } from '../../common/chatModel.j
1111
import { IChatConfirmation, IChatSendRequestOptions, IChatService } from '../../common/chatService.js';
1212
import { isResponseVM } from '../../common/chatViewModel.js';
1313
import { IChatWidgetService } from '../chat.js';
14-
import { ChatConfirmationWidget } from './chatConfirmationWidget.js';
14+
import { SimpleChatConfirmationWidget } from './chatConfirmationWidget.js';
1515
import { IChatContentPart, IChatContentPartRenderContext } from './chatContentParts.js';
1616

1717
export class ChatConfirmationContentPart extends Disposable implements IChatContentPart {
@@ -39,7 +39,7 @@ export class ChatConfirmationContentPart extends Disposable implements IChatCont
3939
{ label: localize('accept', "Accept"), data: confirmation.data },
4040
{ label: localize('dismiss', "Dismiss"), data: confirmation.data, isSecondary: true },
4141
];
42-
const confirmationWidget = this._register(this.instantiationService.createInstance(ChatConfirmationWidget, context.container, { title: confirmation.title, buttons, message: confirmation.message }));
42+
const confirmationWidget = this._register(this.instantiationService.createInstance(SimpleChatConfirmationWidget, context.container, { title: confirmation.title, buttons, message: confirmation.message }));
4343
confirmationWidget.setShowButtons(!confirmation.isUsed);
4444

4545
this._register(confirmationWidget.onDidChangeHeight(() => this._onDidChangeHeight.fire()));

src/vs/workbench/contrib/chat/browser/chatContentParts/chatConfirmationWidget.ts

Lines changed: 227 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { Action, Separator } from '../../../../../base/common/actions.js';
1010
import { Emitter, Event } from '../../../../../base/common/event.js';
1111
import { IMarkdownString, MarkdownString } from '../../../../../base/common/htmlContent.js';
1212
import { Disposable, DisposableStore, MutableDisposable } from '../../../../../base/common/lifecycle.js';
13+
import type { ThemeIcon } from '../../../../../base/common/themables.js';
1314
import { IMarkdownRenderResult, MarkdownRenderer, openLinkFromMarkdown } from '../../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js';
1415
import { localize } from '../../../../../nls.js';
1516
import { MenuWorkbenchToolBar } from '../../../../../platform/actions/browser/toolbar.js';
@@ -106,7 +107,7 @@ export class ChatQueryTitlePart extends Disposable {
106107
}
107108
}
108109

109-
abstract class BaseChatConfirmationWidget extends Disposable {
110+
abstract class BaseSimpleChatConfirmationWidget extends Disposable {
110111
private _onDidClick = this._register(new Emitter<IChatConfirmationButton>());
111112
get onDidClick(): Event<IChatConfirmationButton> { return this._onDidClick.event; }
112113

@@ -269,7 +270,9 @@ abstract class BaseChatConfirmationWidget extends Disposable {
269270
}
270271
}
271272
}
272-
export class ChatConfirmationWidget extends BaseChatConfirmationWidget {
273+
274+
/** @deprecated Use ChatConfirmationWidget instead */
275+
export class SimpleChatConfirmationWidget extends BaseSimpleChatConfirmationWidget {
273276
private _renderedMessage: HTMLElement | undefined;
274277

275278
constructor(
@@ -297,10 +300,231 @@ export class ChatConfirmationWidget extends BaseChatConfirmationWidget {
297300
}
298301
}
299302

303+
export interface IChatConfirmationWidget2Options {
304+
title: string | IMarkdownString;
305+
icon?: ThemeIcon;
306+
subtitle?: string | IMarkdownString;
307+
buttons: IChatConfirmationButton[];
308+
toolbarData?: { arg: any; partType: string; partSource?: string };
309+
}
310+
311+
abstract class BaseChatConfirmationWidget extends Disposable {
312+
private _onDidClick = this._register(new Emitter<IChatConfirmationButton>());
313+
get onDidClick(): Event<IChatConfirmationButton> { return this._onDidClick.event; }
314+
315+
protected _onDidChangeHeight = this._register(new Emitter<void>());
316+
get onDidChangeHeight(): Event<void> { return this._onDidChangeHeight.event; }
317+
318+
private _domNode: HTMLElement;
319+
get domNode(): HTMLElement {
320+
return this._domNode;
321+
}
322+
323+
private get showingButtons() {
324+
return !this.domNode.classList.contains('hideButtons');
325+
}
326+
327+
setShowButtons(showButton: boolean): void {
328+
this.domNode.classList.toggle('hideButtons', !showButton);
329+
}
330+
331+
private readonly messageElement: HTMLElement;
332+
protected readonly markdownRenderer: MarkdownRenderer;
333+
private readonly title: string | IMarkdownString;
334+
335+
private readonly notification = this._register(new MutableDisposable<DisposableStore>());
336+
337+
constructor(
338+
options: IChatConfirmationWidget2Options,
339+
@IInstantiationService protected readonly instantiationService: IInstantiationService,
340+
@IContextMenuService contextMenuService: IContextMenuService,
341+
@IConfigurationService private readonly _configurationService: IConfigurationService,
342+
@IHostService private readonly _hostService: IHostService,
343+
@IViewsService private readonly _viewsService: IViewsService,
344+
@IContextKeyService contextKeyService: IContextKeyService,
345+
) {
346+
super();
347+
348+
const { title, subtitle, buttons, icon } = options;
349+
this.title = title;
350+
351+
const elements = dom.h('.chat-confirmation-widget2@root', [
352+
dom.h('.chat-confirmation-widget-title', [
353+
dom.h('.chat-title@title'),
354+
dom.h('.chat-buttons@buttons'),
355+
]),
356+
dom.h('.chat-confirmation-widget-message@message'),
357+
dom.h('.chat-buttons-container@buttonsContainer', [
358+
dom.h('.chat-toolbar@toolbar'),
359+
]),
360+
]);
361+
this._domNode = elements.root;
362+
this.markdownRenderer = this.instantiationService.createInstance(MarkdownRenderer, {});
363+
364+
const titlePart = this._register(instantiationService.createInstance(
365+
ChatQueryTitlePart,
366+
elements.title,
367+
new MarkdownString(icon ? `$(${icon.id}) ${typeof title === 'string' ? title : title.value}` : typeof title === 'string' ? title : title.value),
368+
subtitle,
369+
this.markdownRenderer,
370+
));
371+
372+
this._register(titlePart.onDidChangeHeight(() => this._onDidChangeHeight.fire()));
373+
374+
this.messageElement = elements.message;
375+
376+
for (const buttonData of buttons) {
377+
const buttonOptions: IButtonOptions = { ...defaultButtonStyles, secondary: buttonData.isSecondary, title: buttonData.tooltip, disabled: buttonData.disabled };
378+
379+
let button: IButton;
380+
if (buttonData.moreActions) {
381+
button = new ButtonWithDropdown(elements.buttons, {
382+
...buttonOptions,
383+
contextMenuProvider: contextMenuService,
384+
addPrimaryActionToDropdown: false,
385+
actions: buttonData.moreActions.map(action => {
386+
if (action instanceof Separator) {
387+
return action;
388+
}
389+
return this._register(new Action(
390+
action.label,
391+
action.label,
392+
undefined,
393+
!action.disabled,
394+
() => {
395+
this._onDidClick.fire(action);
396+
return Promise.resolve();
397+
},
398+
));
399+
}),
400+
});
401+
} else {
402+
button = new Button(elements.buttons, buttonOptions);
403+
}
404+
405+
this._register(button);
406+
button.label = buttonData.label;
407+
this._register(button.onDidClick(() => this._onDidClick.fire(buttonData)));
408+
if (buttonData.onDidChangeDisablement) {
409+
this._register(buttonData.onDidChangeDisablement(disabled => button.enabled = !disabled));
410+
}
411+
}
412+
413+
// Create toolbar if actions are provided
414+
if (options?.toolbarData) {
415+
const overlay = contextKeyService.createOverlay([
416+
['chatConfirmationPartType', options.toolbarData.partType],
417+
['chatConfirmationPartSource', options.toolbarData.partSource],
418+
]);
419+
const nestedInsta = this._register(instantiationService.createChild(new ServiceCollection([IContextKeyService, overlay])));
420+
this._register(nestedInsta.createInstance(
421+
MenuWorkbenchToolBar,
422+
elements.toolbar,
423+
MenuId.ChatConfirmationMenu,
424+
{
425+
// buttonConfigProvider: () => ({ showLabel: false, showIcon: true }),
426+
menuOptions: {
427+
arg: options.toolbarData.arg,
428+
shouldForwardArgs: true,
429+
}
430+
}
431+
));
432+
}
433+
}
434+
435+
436+
// protected renderMessage(element: HTMLElement, listContainer: HTMLElement): void {
437+
// this.messageElement.append(element);
438+
439+
// if (this.showingButtons && this._configurationService.getValue<boolean>('chat.notifyWindowOnConfirmation')) {
440+
// const targetWindow = dom.getWindow(listContainer);
441+
// if (!targetWindow.document.hasFocus()) {
442+
// this.notifyConfirmationNeeded(targetWindow);
443+
// }
444+
// }
445+
// }
446+
protected renderMessage(element: HTMLElement | IMarkdownString | string, listContainer: HTMLElement): void {
447+
if (!dom.isHTMLElement(element)) {
448+
const messageElement = this._register(this.markdownRenderer.render(
449+
typeof element === 'string' ? new MarkdownString(element) : element,
450+
{ asyncRenderCallback: () => this._onDidChangeHeight.fire() }
451+
));
452+
element = messageElement.element;
453+
}
454+
455+
for (const child of this.messageElement.children) {
456+
child.remove();
457+
}
458+
this.messageElement.append(element);
459+
460+
if (this.showingButtons && this._configurationService.getValue<boolean>('chat.notifyWindowOnConfirmation')) {
461+
const targetWindow = dom.getWindow(listContainer);
462+
if (!targetWindow.document.hasFocus()) {
463+
this.notifyConfirmationNeeded(targetWindow);
464+
}
465+
}
466+
}
467+
468+
private async notifyConfirmationNeeded(targetWindow: Window): Promise<void> {
469+
470+
// Focus Window
471+
this._hostService.focus(targetWindow, { mode: FocusMode.Notify });
472+
473+
// Notify
474+
const title = renderAsPlaintext(this.title);
475+
const notification = await dom.triggerNotification(title ? localize('notificationTitle', "Chat: {0}", title) : localize('defaultTitle', "Chat: Confirmation Required"),
476+
{
477+
detail: localize('notificationDetail', "The current chat session requires your confirmation to proceed.")
478+
}
479+
);
480+
if (notification) {
481+
const disposables = this.notification.value = new DisposableStore();
482+
disposables.add(notification);
483+
484+
disposables.add(Event.once(notification.onClick)(() => {
485+
this._hostService.focus(targetWindow, { mode: FocusMode.Force });
486+
showChatView(this._viewsService);
487+
}));
488+
489+
disposables.add(this._hostService.onDidChangeFocus(focus => {
490+
if (focus) {
491+
disposables.dispose();
492+
}
493+
}));
494+
}
495+
}
496+
}
497+
export class ChatConfirmationWidget extends BaseChatConfirmationWidget {
498+
private _renderedMessage: HTMLElement | undefined;
499+
500+
constructor(
501+
private readonly _container: HTMLElement,
502+
options: IChatConfirmationWidget2Options & { message: HTMLElement | IMarkdownString | string },
503+
@IInstantiationService instantiationService: IInstantiationService,
504+
@IContextMenuService contextMenuService: IContextMenuService,
505+
@IConfigurationService configurationService: IConfigurationService,
506+
@IHostService hostService: IHostService,
507+
@IViewsService viewsService: IViewsService,
508+
@IContextKeyService contextKeyService: IContextKeyService,
509+
) {
510+
super(options, instantiationService, contextMenuService, configurationService, hostService, viewsService, contextKeyService);
511+
this.renderMessage(options.message, this._container);
512+
}
513+
514+
public updateMessage(message: string | IMarkdownString): void {
515+
this._renderedMessage?.remove();
516+
const renderedMessage = this._register(this.markdownRenderer.render(
517+
typeof message === 'string' ? new MarkdownString(message) : message,
518+
{ asyncRenderCallback: () => this._onDidChangeHeight.fire() }
519+
));
520+
this.renderMessage(renderedMessage.element, this._container);
521+
this._renderedMessage = renderedMessage.element;
522+
}
523+
}
300524
export class ChatCustomConfirmationWidget extends BaseChatConfirmationWidget {
301525
constructor(
302526
container: HTMLElement,
303-
options: IChatConfirmationWidgetOptions & { message: HTMLElement },
527+
options: IChatConfirmationWidget2Options & { message: HTMLElement | IMarkdownString | string },
304528
@IInstantiationService instantiationService: IInstantiationService,
305529
@IContextMenuService contextMenuService: IContextMenuService,
306530
@IConfigurationService configurationService: IConfigurationService,

src/vs/workbench/contrib/chat/browser/chatContentParts/chatElicitationContentPart.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,12 @@ export class ChatElicitationContentPart extends Disposable implements IChatConte
3434
const confirmationWidget = this._register(this.instantiationService.createInstance(ChatConfirmationWidget, context.container, { title: elicitation.title, subtitle: elicitation.subtitle, buttons, message: this.getMessageToRender(elicitation), toolbarData: { partType: 'elicitation', partSource: elicitation.source?.type, arg: elicitation } }));
3535
confirmationWidget.setShowButtons(elicitation.state === 'pending');
3636

37-
this._register(elicitation.onDidRequestHide(() => this.domNode.remove()));
37+
if (elicitation.onDidRequestHide) {
38+
this._register(elicitation.onDidRequestHide(() => this.domNode.remove()));
39+
}
3840

3941
this._register(confirmationWidget.onDidChangeHeight(() => this._onDidChangeHeight.fire()));
4042

41-
const messageToRender = this.getMessageToRender(elicitation);
42-
4343
this._register(confirmationWidget.onDidClick(async e => {
4444
if (e.data) {
4545
await elicitation.accept();
@@ -48,15 +48,15 @@ export class ChatElicitationContentPart extends Disposable implements IChatConte
4848
}
4949

5050
confirmationWidget.setShowButtons(false);
51-
confirmationWidget.updateMessage(messageToRender);
51+
confirmationWidget.updateMessage(this.getMessageToRender(elicitation));
5252

5353
this._onDidChangeHeight.fire();
5454
}));
5555

56-
5756
this.chatAccessibilityService.acceptElicitation(elicitation);
5857
this.domNode = confirmationWidget.domNode;
5958
this.domNode.tabIndex = 0;
59+
const messageToRender = this.getMessageToRender(elicitation);
6060
this.domNode.ariaLabel = elicitation.title + ' ' + (typeof messageToRender === 'string' ? messageToRender : messageToRender.value || '');
6161
}
6262

0 commit comments

Comments
 (0)