Path: blob/main/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts
3296 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 { ICodeEditor } from '../../../../../editor/browser/editorBrowser.js';6import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js';7import { ICodeEditorService } from '../../../../../editor/browser/services/codeEditorService.js';8import { AccessibleDiffViewerNext } from '../../../../../editor/browser/widget/diffEditor/commands.js';9import { localize } from '../../../../../nls.js';10import { AccessibleContentProvider, AccessibleViewProviderId, AccessibleViewType } from '../../../../../platform/accessibility/browser/accessibleView.js';11import { IAccessibleViewImplementation } from '../../../../../platform/accessibility/browser/accessibleViewRegistry.js';12import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js';13import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js';14import { AccessibilityVerbositySettingId } from '../../../accessibility/browser/accessibilityConfiguration.js';15import { INLINE_CHAT_ID } from '../../../inlineChat/common/inlineChat.js';16import { ChatContextKeyExprs, ChatContextKeys } from '../../common/chatContextKeys.js';17import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../common/constants.js';18import { IChatWidgetService } from '../chat.js';1920export class PanelChatAccessibilityHelp implements IAccessibleViewImplementation {21readonly priority = 107;22readonly name = 'panelChat';23readonly type = AccessibleViewType.Help;24readonly when = ContextKeyExpr.and(ChatContextKeys.location.isEqualTo(ChatAgentLocation.Panel), ChatContextKeys.inQuickChat.negate(), ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Ask), ContextKeyExpr.or(ChatContextKeys.inChatSession, ChatContextKeys.isResponse, ChatContextKeys.isRequest));25getProvider(accessor: ServicesAccessor) {26const codeEditor = accessor.get(ICodeEditorService).getActiveCodeEditor() || accessor.get(ICodeEditorService).getFocusedCodeEditor();27return getChatAccessibilityHelpProvider(accessor, codeEditor ?? undefined, 'panelChat');28}29}3031export class QuickChatAccessibilityHelp implements IAccessibleViewImplementation {32readonly priority = 107;33readonly name = 'quickChat';34readonly type = AccessibleViewType.Help;35readonly when = ContextKeyExpr.and(ChatContextKeys.inQuickChat, ContextKeyExpr.or(ChatContextKeys.inChatSession, ChatContextKeys.isResponse, ChatContextKeys.isRequest));36getProvider(accessor: ServicesAccessor) {37const codeEditor = accessor.get(ICodeEditorService).getActiveCodeEditor() || accessor.get(ICodeEditorService).getFocusedCodeEditor();38return getChatAccessibilityHelpProvider(accessor, codeEditor ?? undefined, 'quickChat');39}40}4142export class EditsChatAccessibilityHelp implements IAccessibleViewImplementation {43readonly priority = 119;44readonly name = 'editsView';45readonly type = AccessibleViewType.Help;46readonly when = ContextKeyExpr.and(ChatContextKeyExprs.inEditingMode, ChatContextKeys.inChatInput);47getProvider(accessor: ServicesAccessor) {48const codeEditor = accessor.get(ICodeEditorService).getActiveCodeEditor() || accessor.get(ICodeEditorService).getFocusedCodeEditor();49return getChatAccessibilityHelpProvider(accessor, codeEditor ?? undefined, 'editsView');50}51}5253export class AgentChatAccessibilityHelp implements IAccessibleViewImplementation {54readonly priority = 120;55readonly name = 'agentView';56readonly type = AccessibleViewType.Help;57readonly when = ContextKeyExpr.and(ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Agent), ChatContextKeys.inChatInput);58getProvider(accessor: ServicesAccessor) {59const codeEditor = accessor.get(ICodeEditorService).getActiveCodeEditor() || accessor.get(ICodeEditorService).getFocusedCodeEditor();60return getChatAccessibilityHelpProvider(accessor, codeEditor ?? undefined, 'agentView');61}62}6364export function getAccessibilityHelpText(type: 'panelChat' | 'inlineChat' | 'quickChat' | 'editsView' | 'agentView', keybindingService: IKeybindingService): string {65const content = [];66if (type === 'panelChat' || type === 'quickChat') {67if (type === 'quickChat') {68content.push(localize('chat.overview', 'The quick chat view is comprised of an input box and a request/response list. The input box is used to make requests and the list is used to display responses.'));69content.push(localize('chat.differenceQuick', 'The quick chat view is a transient interface for making and viewing requests, while the panel chat view is a persistent interface that also supports navigating suggested follow-up questions.'));70}71if (type === 'panelChat') {72content.push(localize('chat.differencePanel', 'The panel chat view is a persistent interface that also supports navigating suggested follow-up questions, while the quick chat view is a transient interface for making and viewing requests.'));73content.push(localize('chat.followUp', 'In the input box, navigate to the suggested follow up question (Shift+Tab) and press Enter to run it.'));74}75content.push(localize('chat.requestHistory', 'In the input box, use up and down arrows to navigate your request history. Edit input and use enter or the submit button to run a new request.'));76content.push(localize('chat.inspectResponse', 'In the input box, inspect the last response in the accessible view{0}.', '<keybinding:editor.action.accessibleView>'));77content.push(localize('chat.announcement', 'Chat responses will be announced as they come in. A response will indicate the number of code blocks, if any, and then the rest of the response.'));78content.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)));79content.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)));80content.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>'));81content.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>'));82if (type === 'panelChat') {83content.push(localize('workbench.action.chat.newChat', 'To create a new chat session, invoke the New Chat command{0}.', '<keybinding:workbench.action.chat.new>'));84}85}86if (type === 'editsView' || type === 'agentView') {87if (type === 'agentView') {88content.push(localize('chatAgent.overview', 'The chat agent view is used to apply edits across files in your workspace, enable running commands in the terminal, and more.'));89} else {90content.push(localize('chatEditing.overview', 'The chat editing view is used to apply edits across files.'));91}92content.push(localize('chatEditing.format', 'It is comprised of an input box and a file working set (Shift+Tab).'));93content.push(localize('chatEditing.expectation', 'When a request is made, a progress indicator will play while the edits are being applied.'));94content.push(localize('chatEditing.review', 'Once the edits are applied, a sound will play to indicate the document has been opened and is ready for review. The sound can be disabled with accessibility.signals.chatEditModifiedFile.'));95content.push(localize('chatEditing.sections', 'Navigate between edits in the editor with navigate previous{0} and next{1}', '<keybinding:chatEditor.action.navigatePrevious>', '<keybinding:chatEditor.action.navigateNext>'));96content.push(localize('chatEditing.acceptHunk', 'In the editor, Keep{0}, Undo{1}, or Toggle the Diff{2} for the current Change.', '<keybinding:chatEditor.action.acceptHunk>', '<keybinding:chatEditor.action.undoHunk>', '<keybinding:chatEditor.action.toggleDiff>'));97content.push(localize('chatEditing.undoKeepSounds', 'Sounds will play when a change is accepted or undone. The sounds can be disabled with accessibility.signals.editsKept and accessibility.signals.editsUndone.'));98if (type === 'agentView') {99content.push(localize('chatAgent.userActionRequired', 'An alert will indicate when user action is required. For example, if the agent wants to run something in the terminal, you will hear Action Required: Run Command in Terminal.'));100content.push(localize('chatAgent.runCommand', 'To take the action, use the accept tool command{0}.', '<keybinding:workbench.action.chat.acceptTool>'));101content.push(localize('chatAgent.autoApprove', 'To automatically approve tool actions without manual confirmation, set {0} to {1} in your settings.', ChatConfiguration.GlobalAutoApprove, 'true'));102content.push(localize('chatAgent.acceptTool', 'To accept a tool action, use the Accept Tool Confirmation command{0}.', '<keybinding:workbench.action.chat.acceptTool>'));103content.push(localize('chatAgent.openEditedFilesSetting', 'By default, when edits are made to files, they will be opened. To change this behavior, set accessibility.openChatEditedFiles to false in your settings.'));104}105content.push(localize('chatEditing.helpfulCommands', 'Some helpful commands include:'));106content.push(localize('workbench.action.chat.undoEdits', '- Undo Edits{0}.', '<keybinding:workbench.action.chat.undoEdits>'));107content.push(localize('workbench.action.chat.editing.attachFiles', '- Attach Files{0}.', '<keybinding:workbench.action.chat.editing.attachFiles>'));108content.push(localize('chatEditing.removeFileFromWorkingSet', '- Remove File from Working Set{0}.', '<keybinding:chatEditing.removeFileFromWorkingSet>'));109content.push(localize('chatEditing.acceptFile', '- Keep{0} and Undo File{1}.', '<keybinding:chatEditing.acceptFile>', '<keybinding:chatEditing.discardFile>'));110content.push(localize('chatEditing.saveAllFiles', '- Save All Files{0}.', '<keybinding:chatEditing.saveAllFiles>'));111content.push(localize('chatEditing.acceptAllFiles', '- Keep All Edits{0}.', '<keybinding:chatEditing.acceptAllFiles>'));112content.push(localize('chatEditing.discardAllFiles', '- Undo All Edits{0}.', '<keybinding:chatEditing.discardAllFiles>'));113content.push(localize('chatEditing.openFileInDiff', '- Open File in Diff{0}.', '<keybinding:chatEditing.openFileInDiff>'));114content.push(localize('chatEditing.viewChanges', '- View Changes{0}.', '<keybinding:chatEditing.viewChanges>'));115content.push('chatEditing.viewAllEdits', '- View All Edits{0}.', '<keybinding:chatEditing.viewAllEdits>');116content.push(localize('chatEditing.viewPreviousEdits', '- View Previous Edits{0}.', '<keybinding:chatEditing.viewPreviousEdits>'));117}118else {119content.push(localize('inlineChat.overview', "Inline chat occurs within a code editor and takes into account the current selection. It is useful for making changes to the current editor. For example, fixing diagnostics, documenting or refactoring code. Keep in mind that AI generated code may be incorrect."));120content.push(localize('inlineChat.access', "It can be activated via code actions or directly using the command: Inline Chat: Start Inline Chat{0}.", '<keybinding:inlineChat.start>'));121content.push(localize('inlineChat.requestHistory', 'In the input box, use Show Previous{0} and Show Next{1} to navigate your request history. Edit input and use enter or the submit button to run a new request.', '<keybinding:history.showPrevious>', '<keybinding:history.showNext>'));122content.push(localize('inlineChat.inspectResponse', 'In the input box, inspect the response in the accessible view{0}.', '<keybinding:editor.action.accessibleView>'));123content.push(localize('inlineChat.contextActions', "Context menu actions may run a request prefixed with a /. Type / to discover such ready-made commands."));124content.push(localize('inlineChat.fix', "If a fix action is invoked, a response will indicate the problem with the current code. A diff editor will be rendered and can be reached by tabbing."));125content.push(localize('inlineChat.diff', "Once in the diff editor, enter review mode with{0}. Use up and down arrows to navigate lines with the proposed changes.", AccessibleDiffViewerNext.id));126content.push(localize('inlineChat.toolbar', "Use tab to reach conditional parts like commands, status, message responses and more."));127}128content.push(localize('chat.signals', "Accessibility Signals can be changed via settings with a prefix of signals.chat. By default, if a request takes more than 4 seconds, you will hear a sound indicating that progress is still occurring."));129return content.join('\n');130}131132export function getChatAccessibilityHelpProvider(accessor: ServicesAccessor, editor: ICodeEditor | undefined, type: 'panelChat' | 'inlineChat' | 'quickChat' | 'editsView' | 'agentView'): AccessibleContentProvider | undefined {133const widgetService = accessor.get(IChatWidgetService);134const keybindingService = accessor.get(IKeybindingService);135const inputEditor: ICodeEditor | undefined = type === 'panelChat' || type === 'editsView' || type === 'quickChat' ? widgetService.lastFocusedWidget?.inputEditor : editor;136137if (!inputEditor) {138return;139}140const domNode = inputEditor.getDomNode() ?? undefined;141if (!domNode) {142return;143}144145const cachedPosition = inputEditor.getPosition();146inputEditor.getSupportedActions();147const helpText = getAccessibilityHelpText(type, keybindingService);148return new AccessibleContentProvider(149type === 'panelChat' ? AccessibleViewProviderId.PanelChat : type === 'inlineChat' ? AccessibleViewProviderId.InlineChat : type === 'agentView' ? AccessibleViewProviderId.AgentChat : AccessibleViewProviderId.QuickChat,150{ type: AccessibleViewType.Help },151() => helpText,152() => {153if (type === 'panelChat' && cachedPosition) {154inputEditor.setPosition(cachedPosition);155inputEditor.focus();156157} else if (type === 'inlineChat') {158// TODO@jrieken find a better way for this159const ctrl = <{ focus(): void } | undefined>editor?.getContribution(INLINE_CHAT_ID);160ctrl?.focus();161162} else if (type === 'quickChat' || type === 'editsView' || type === 'agentView') {163// For quickChat, editsView, and agentView, restore focus to the chat widget input164widgetService.lastFocusedWidget?.focusInput();165}166},167type === 'panelChat' ? AccessibilityVerbositySettingId.Chat : AccessibilityVerbositySettingId.InlineChat,168);169}170171// The when clauses for actions may not be true when we invoke the accessible view, so we need to provide the keybinding label manually172// to ensure it's correct173function getChatFocusKeybindingLabel(keybindingService: IKeybindingService, type: 'panelChat' | 'inlineChat' | 'quickChat', focusInput?: boolean): string | undefined {174let kbs;175const fallback = ' (unassigned keybinding)';176if (focusInput) {177kbs = keybindingService.lookupKeybindings('workbench.action.chat.focusInput');178} else {179kbs = keybindingService.lookupKeybindings('chat.action.focus');180}181if (!kbs?.length) {182return fallback;183}184let kb;185if (type === 'panelChat') {186if (focusInput) {187kb = kbs.find(kb => kb.getAriaLabel()?.includes('DownArrow'))?.getAriaLabel();188} else {189kb = kbs.find(kb => kb.getAriaLabel()?.includes('UpArrow'))?.getAriaLabel();190}191} else {192// Quick chat193if (focusInput) {194kb = kbs.find(kb => kb.getAriaLabel()?.includes('UpArrow'))?.getAriaLabel();195} else {196kb = kbs.find(kb => kb.getAriaLabel()?.includes('DownArrow'))?.getAriaLabel();197}198}199return !!kb ? ` (${kb})` : fallback;200}201202203