Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { alert } from '../../../../../base/browser/ui/aria/aria.js';
import { localize } from '../../../../../nls.js';
import { Action2, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js';
import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js';
import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js';
import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js';
import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js';
import { IChatWidgetService } from '../chat.js';
import { ChatContextKeys } from '../../common/chatContextKeys.js';
import { ChatAgentLocation } from '../../common/constants.js';
import { isResponseVM } from '../../common/chatViewModel.js';
import { CONTEXT_ACCESSIBILITY_MODE_ENABLED } from '../../../../../platform/accessibility/common/accessibility.js';

export const ACTION_ID_FOCUS_CHAT_CONFIRMATION = 'workbench.action.chat.focusConfirmation';

class AnnounceChatConfirmationAction extends Action2 {
constructor() {
super({
id: ACTION_ID_FOCUS_CHAT_CONFIRMATION,
title: { value: localize('focusChatConfirmation', 'Focus Chat Confirmation'), original: 'Focus Chat Confirmation' },
category: { value: localize('chat.category', 'Chat'), original: 'Chat' },
f1: true,
keybinding: {
weight: KeybindingWeight.WorkbenchContrib,
primary: KeyMod.Alt | KeyCode.KeyA,
when: ContextKeyExpr.and(
ChatContextKeys.location.isEqualTo(ChatAgentLocation.Panel),
ChatContextKeys.inChatSession,
CONTEXT_ACCESSIBILITY_MODE_ENABLED
)
},
menu: [
{
id: MenuId.ChatConfirmationMenu,
when: ChatContextKeys.inChatSession,
group: '0_main'
}
]
});
}

async run(accessor: ServicesAccessor): Promise<void> {
const chatWidgetService = accessor.get(IChatWidgetService);
const lastFocusedWidget = chatWidgetService.lastFocusedWidget;

if (!lastFocusedWidget) {
alert(localize('noChatSession', 'No active chat session found.'));
return;
}

const viewModel = lastFocusedWidget.viewModel;
if (!viewModel) {
alert(localize('chatNotReady', 'Chat interface not ready.'));
return;
}

// Check for active confirmations in the chat responses
let firstConfirmationElement: HTMLElement | undefined;

const lastResponse = viewModel.getItems()[viewModel.getItems().length - 1];
if (isResponseVM(lastResponse)) {
const confirmationWidgets = lastFocusedWidget.domNode.querySelectorAll('.chat-confirmation-widget-container');
if (confirmationWidgets.length > 0) {
firstConfirmationElement = confirmationWidgets[0] as HTMLElement;
}
}

if (firstConfirmationElement) {
firstConfirmationElement.focus();
} else {
alert(localize('noConfirmationRequired', 'No chat confirmation required'));
}
}
}

export function registerChatAccessibilityActions(): void {
registerAction2(AnnounceChatConfirmationAction);
}
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export function getAccessibilityHelpText(type: 'panelChat' | 'inlineChat' | 'qui
content.push(localize('workbench.action.chat.focus', 'To focus the chat request/response list, which can be navigated with up and down arrows, invoke the Focus Chat command{0}.', getChatFocusKeybindingLabel(keybindingService, type, false)));
content.push(localize('workbench.action.chat.focusInput', 'To focus the input box for chat requests, invoke the Focus Chat Input command{0}.', getChatFocusKeybindingLabel(keybindingService, type, true)));
content.push(localize('workbench.action.chat.nextCodeBlock', 'To focus the next code block within a response, invoke the Chat: Next Code Block command{0}.', '<keybinding:workbench.action.chat.nextCodeBlock>'));
content.push(localize('workbench.action.chat.announceConfirmation', 'To focus pending chat confirmation dialogs, invoke the Focus Chat Confirmation Status command{0}.', '<keybinding:workbench.action.chat.focusConfirmation>'));
if (type === 'panelChat') {
content.push(localize('workbench.action.chat.newChat', 'To create a new chat session, invoke the New Chat command{0}.', '<keybinding:workbench.action.chat.new>'));
}
Expand Down
2 changes: 2 additions & 0 deletions src/vs/workbench/contrib/chat/browser/chat.contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ import { LanguageModelToolsExtensionPointHandler } from '../common/tools/languag
import { BuiltinToolsContribution } from '../common/tools/tools.js';
import { IVoiceChatService, VoiceChatService } from '../common/voiceChatService.js';
import { AgentChatAccessibilityHelp, EditsChatAccessibilityHelp, PanelChatAccessibilityHelp, QuickChatAccessibilityHelp } from './actions/chatAccessibilityHelp.js';
import { registerChatAccessibilityActions } from './actions/chatAccessibilityActions.js';
import { ACTION_ID_NEW_CHAT, CopilotTitleBarMenuRendering, registerChatActions } from './actions/chatActions.js';
import { registerNewChatActions } from './actions/chatClearActions.js';
import { CodeBlockActionRendering, registerChatCodeBlockActions, registerChatCodeCompareBlockActions } from './actions/chatCodeblockActions.js';
Expand Down Expand Up @@ -802,6 +803,7 @@ registerWorkbenchContribution2(PromptUrlHandler.ID, PromptUrlHandler, WorkbenchP
registerWorkbenchContribution2(ChatSessionsView.ID, ChatSessionsView, WorkbenchPhase.AfterRestored);

registerChatActions();
registerChatAccessibilityActions();
registerChatCopyActions();
registerChatCodeBlockActions();
registerChatCodeCompareBlockActions();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export interface IChatConfirmationButton {

export interface IChatConfirmationWidgetOptions {
title: string | IMarkdownString;
message: string | IMarkdownString;
subtitle?: string | IMarkdownString;
buttons: IChatConfirmationButton[];
toolbarData?: { arg: any; partType: string; partSource?: string };
Expand Down Expand Up @@ -144,9 +145,10 @@ abstract class BaseSimpleChatConfirmationWidget extends Disposable {
) {
super();

const { title, subtitle, buttons } = options;
const { title, subtitle, message, buttons } = options;
this.title = title;


const elements = dom.h('.chat-confirmation-widget@root', [
dom.h('.chat-confirmation-widget-title@title'),
dom.h('.chat-confirmation-widget-message@message'),
Expand All @@ -155,7 +157,9 @@ abstract class BaseSimpleChatConfirmationWidget extends Disposable {
dom.h('.chat-toolbar@toolbar'),
]),
]);
this._domNode = elements.root;
const container = createAccessibilityContainer(title, message);
container.appendChild(elements.root);
this._domNode = container;
this.markdownRenderer = this.instantiationService.createInstance(MarkdownRenderer, {});

const titlePart = this._register(instantiationService.createInstance(
Expand Down Expand Up @@ -302,6 +306,7 @@ export class SimpleChatConfirmationWidget extends BaseSimpleChatConfirmationWidg

export interface IChatConfirmationWidget2Options {
title: string | IMarkdownString;
message: string | IMarkdownString | HTMLElement;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think message is being used?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that is being used - it gets passed to the base chat confirmation widget to be used in the aria label

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

const container = createAccessibilityContainer(title, message);

icon?: ThemeIcon;
subtitle?: string | IMarkdownString;
buttons: IChatConfirmationButton[];
Expand Down Expand Up @@ -345,7 +350,7 @@ abstract class BaseChatConfirmationWidget extends Disposable {
) {
super();

const { title, subtitle, buttons, icon } = options;
const { title, subtitle, message, buttons, icon } = options;
this.title = title;

const elements = dom.h('.chat-confirmation-widget2@root', [
Expand All @@ -360,7 +365,9 @@ abstract class BaseChatConfirmationWidget extends Disposable {
dom.h('.chat-buttons@buttons'),
]),
]);
this._domNode = elements.root;
const container = createAccessibilityContainer(title, message);
container.appendChild(elements.root);
this._domNode = container;
this.markdownRenderer = this.instantiationService.createInstance(MarkdownRenderer, {});

const titlePart = this._register(instantiationService.createInstance(
Expand Down Expand Up @@ -538,3 +545,13 @@ export class ChatCustomConfirmationWidget extends BaseChatConfirmationWidget {
this.renderMessage(options.message, container);
}
}

function createAccessibilityContainer(title: string | IMarkdownString, message?: string | IMarkdownString | HTMLElement): HTMLElement {
const container = document.createElement('div');
container.tabIndex = 0;
const titleAsString = typeof title === 'string' ? title : title.value;
const messageAsString = typeof message === 'string' ? message : message && 'value' in message ? message.value : message && 'textContent' in message ? message.textContent : '';
container.setAttribute('aria-label', localize('chat.confirmationWidget.ariaLabel', "Chat Confirmation Dialog {0} {1}", titleAsString, messageAsString));
container.classList.add('chat-confirmation-widget-container');
return container;
}
Loading