Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.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 { ICodeEditor } from '../../../../../editor/browser/editorBrowser.js';
7
import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js';
8
import { ICodeEditorService } from '../../../../../editor/browser/services/codeEditorService.js';
9
import { AccessibleDiffViewerNext } from '../../../../../editor/browser/widget/diffEditor/commands.js';
10
import { localize } from '../../../../../nls.js';
11
import { AccessibleContentProvider, AccessibleViewProviderId, AccessibleViewType } from '../../../../../platform/accessibility/browser/accessibleView.js';
12
import { IAccessibleViewImplementation } from '../../../../../platform/accessibility/browser/accessibleViewRegistry.js';
13
import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js';
14
import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js';
15
import { AccessibilityVerbositySettingId } from '../../../accessibility/browser/accessibilityConfiguration.js';
16
import { INLINE_CHAT_ID } from '../../../inlineChat/common/inlineChat.js';
17
import { ChatContextKeyExprs, ChatContextKeys } from '../../common/chatContextKeys.js';
18
import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../common/constants.js';
19
import { IChatWidgetService } from '../chat.js';
20
21
export class PanelChatAccessibilityHelp implements IAccessibleViewImplementation {
22
readonly priority = 107;
23
readonly name = 'panelChat';
24
readonly type = AccessibleViewType.Help;
25
readonly when = ContextKeyExpr.and(ChatContextKeys.location.isEqualTo(ChatAgentLocation.Panel), ChatContextKeys.inQuickChat.negate(), ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Ask), ContextKeyExpr.or(ChatContextKeys.inChatSession, ChatContextKeys.isResponse, ChatContextKeys.isRequest));
26
getProvider(accessor: ServicesAccessor) {
27
const codeEditor = accessor.get(ICodeEditorService).getActiveCodeEditor() || accessor.get(ICodeEditorService).getFocusedCodeEditor();
28
return getChatAccessibilityHelpProvider(accessor, codeEditor ?? undefined, 'panelChat');
29
}
30
}
31
32
export class QuickChatAccessibilityHelp implements IAccessibleViewImplementation {
33
readonly priority = 107;
34
readonly name = 'quickChat';
35
readonly type = AccessibleViewType.Help;
36
readonly when = ContextKeyExpr.and(ChatContextKeys.inQuickChat, ContextKeyExpr.or(ChatContextKeys.inChatSession, ChatContextKeys.isResponse, ChatContextKeys.isRequest));
37
getProvider(accessor: ServicesAccessor) {
38
const codeEditor = accessor.get(ICodeEditorService).getActiveCodeEditor() || accessor.get(ICodeEditorService).getFocusedCodeEditor();
39
return getChatAccessibilityHelpProvider(accessor, codeEditor ?? undefined, 'quickChat');
40
}
41
}
42
43
export class EditsChatAccessibilityHelp implements IAccessibleViewImplementation {
44
readonly priority = 119;
45
readonly name = 'editsView';
46
readonly type = AccessibleViewType.Help;
47
readonly when = ContextKeyExpr.and(ChatContextKeyExprs.inEditingMode, ChatContextKeys.inChatInput);
48
getProvider(accessor: ServicesAccessor) {
49
const codeEditor = accessor.get(ICodeEditorService).getActiveCodeEditor() || accessor.get(ICodeEditorService).getFocusedCodeEditor();
50
return getChatAccessibilityHelpProvider(accessor, codeEditor ?? undefined, 'editsView');
51
}
52
}
53
54
export class AgentChatAccessibilityHelp implements IAccessibleViewImplementation {
55
readonly priority = 120;
56
readonly name = 'agentView';
57
readonly type = AccessibleViewType.Help;
58
readonly when = ContextKeyExpr.and(ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Agent), ChatContextKeys.inChatInput);
59
getProvider(accessor: ServicesAccessor) {
60
const codeEditor = accessor.get(ICodeEditorService).getActiveCodeEditor() || accessor.get(ICodeEditorService).getFocusedCodeEditor();
61
return getChatAccessibilityHelpProvider(accessor, codeEditor ?? undefined, 'agentView');
62
}
63
}
64
65
export function getAccessibilityHelpText(type: 'panelChat' | 'inlineChat' | 'quickChat' | 'editsView' | 'agentView', keybindingService: IKeybindingService): string {
66
const content = [];
67
if (type === 'panelChat' || type === 'quickChat') {
68
if (type === 'quickChat') {
69
content.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.'));
70
content.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.'));
71
}
72
if (type === 'panelChat') {
73
content.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.'));
74
content.push(localize('chat.followUp', 'In the input box, navigate to the suggested follow up question (Shift+Tab) and press Enter to run it.'));
75
}
76
content.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.'));
77
content.push(localize('chat.inspectResponse', 'In the input box, inspect the last response in the accessible view{0}.', '<keybinding:editor.action.accessibleView>'));
78
content.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.'));
79
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)));
80
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)));
81
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>'));
82
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>'));
83
if (type === 'panelChat') {
84
content.push(localize('workbench.action.chat.newChat', 'To create a new chat session, invoke the New Chat command{0}.', '<keybinding:workbench.action.chat.new>'));
85
}
86
}
87
if (type === 'editsView' || type === 'agentView') {
88
if (type === 'agentView') {
89
content.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.'));
90
} else {
91
content.push(localize('chatEditing.overview', 'The chat editing view is used to apply edits across files.'));
92
}
93
content.push(localize('chatEditing.format', 'It is comprised of an input box and a file working set (Shift+Tab).'));
94
content.push(localize('chatEditing.expectation', 'When a request is made, a progress indicator will play while the edits are being applied.'));
95
content.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.'));
96
content.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>'));
97
content.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>'));
98
content.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.'));
99
if (type === 'agentView') {
100
content.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.'));
101
content.push(localize('chatAgent.runCommand', 'To take the action, use the accept tool command{0}.', '<keybinding:workbench.action.chat.acceptTool>'));
102
content.push(localize('chatAgent.autoApprove', 'To automatically approve tool actions without manual confirmation, set {0} to {1} in your settings.', ChatConfiguration.GlobalAutoApprove, 'true'));
103
content.push(localize('chatAgent.acceptTool', 'To accept a tool action, use the Accept Tool Confirmation command{0}.', '<keybinding:workbench.action.chat.acceptTool>'));
104
content.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.'));
105
}
106
content.push(localize('chatEditing.helpfulCommands', 'Some helpful commands include:'));
107
content.push(localize('workbench.action.chat.undoEdits', '- Undo Edits{0}.', '<keybinding:workbench.action.chat.undoEdits>'));
108
content.push(localize('workbench.action.chat.editing.attachFiles', '- Attach Files{0}.', '<keybinding:workbench.action.chat.editing.attachFiles>'));
109
content.push(localize('chatEditing.removeFileFromWorkingSet', '- Remove File from Working Set{0}.', '<keybinding:chatEditing.removeFileFromWorkingSet>'));
110
content.push(localize('chatEditing.acceptFile', '- Keep{0} and Undo File{1}.', '<keybinding:chatEditing.acceptFile>', '<keybinding:chatEditing.discardFile>'));
111
content.push(localize('chatEditing.saveAllFiles', '- Save All Files{0}.', '<keybinding:chatEditing.saveAllFiles>'));
112
content.push(localize('chatEditing.acceptAllFiles', '- Keep All Edits{0}.', '<keybinding:chatEditing.acceptAllFiles>'));
113
content.push(localize('chatEditing.discardAllFiles', '- Undo All Edits{0}.', '<keybinding:chatEditing.discardAllFiles>'));
114
content.push(localize('chatEditing.openFileInDiff', '- Open File in Diff{0}.', '<keybinding:chatEditing.openFileInDiff>'));
115
content.push(localize('chatEditing.viewChanges', '- View Changes{0}.', '<keybinding:chatEditing.viewChanges>'));
116
content.push('chatEditing.viewAllEdits', '- View All Edits{0}.', '<keybinding:chatEditing.viewAllEdits>');
117
content.push(localize('chatEditing.viewPreviousEdits', '- View Previous Edits{0}.', '<keybinding:chatEditing.viewPreviousEdits>'));
118
}
119
else {
120
content.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."));
121
content.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>'));
122
content.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>'));
123
content.push(localize('inlineChat.inspectResponse', 'In the input box, inspect the response in the accessible view{0}.', '<keybinding:editor.action.accessibleView>'));
124
content.push(localize('inlineChat.contextActions', "Context menu actions may run a request prefixed with a /. Type / to discover such ready-made commands."));
125
content.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."));
126
content.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));
127
content.push(localize('inlineChat.toolbar', "Use tab to reach conditional parts like commands, status, message responses and more."));
128
}
129
content.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."));
130
return content.join('\n');
131
}
132
133
export function getChatAccessibilityHelpProvider(accessor: ServicesAccessor, editor: ICodeEditor | undefined, type: 'panelChat' | 'inlineChat' | 'quickChat' | 'editsView' | 'agentView'): AccessibleContentProvider | undefined {
134
const widgetService = accessor.get(IChatWidgetService);
135
const keybindingService = accessor.get(IKeybindingService);
136
const inputEditor: ICodeEditor | undefined = type === 'panelChat' || type === 'editsView' || type === 'quickChat' ? widgetService.lastFocusedWidget?.inputEditor : editor;
137
138
if (!inputEditor) {
139
return;
140
}
141
const domNode = inputEditor.getDomNode() ?? undefined;
142
if (!domNode) {
143
return;
144
}
145
146
const cachedPosition = inputEditor.getPosition();
147
inputEditor.getSupportedActions();
148
const helpText = getAccessibilityHelpText(type, keybindingService);
149
return new AccessibleContentProvider(
150
type === 'panelChat' ? AccessibleViewProviderId.PanelChat : type === 'inlineChat' ? AccessibleViewProviderId.InlineChat : type === 'agentView' ? AccessibleViewProviderId.AgentChat : AccessibleViewProviderId.QuickChat,
151
{ type: AccessibleViewType.Help },
152
() => helpText,
153
() => {
154
if (type === 'panelChat' && cachedPosition) {
155
inputEditor.setPosition(cachedPosition);
156
inputEditor.focus();
157
158
} else if (type === 'inlineChat') {
159
// TODO@jrieken find a better way for this
160
const ctrl = <{ focus(): void } | undefined>editor?.getContribution(INLINE_CHAT_ID);
161
ctrl?.focus();
162
163
} else if (type === 'quickChat' || type === 'editsView' || type === 'agentView') {
164
// For quickChat, editsView, and agentView, restore focus to the chat widget input
165
widgetService.lastFocusedWidget?.focusInput();
166
}
167
},
168
type === 'panelChat' ? AccessibilityVerbositySettingId.Chat : AccessibilityVerbositySettingId.InlineChat,
169
);
170
}
171
172
// The when clauses for actions may not be true when we invoke the accessible view, so we need to provide the keybinding label manually
173
// to ensure it's correct
174
function getChatFocusKeybindingLabel(keybindingService: IKeybindingService, type: 'panelChat' | 'inlineChat' | 'quickChat', focusInput?: boolean): string | undefined {
175
let kbs;
176
const fallback = ' (unassigned keybinding)';
177
if (focusInput) {
178
kbs = keybindingService.lookupKeybindings('workbench.action.chat.focusInput');
179
} else {
180
kbs = keybindingService.lookupKeybindings('chat.action.focus');
181
}
182
if (!kbs?.length) {
183
return fallback;
184
}
185
let kb;
186
if (type === 'panelChat') {
187
if (focusInput) {
188
kb = kbs.find(kb => kb.getAriaLabel()?.includes('DownArrow'))?.getAriaLabel();
189
} else {
190
kb = kbs.find(kb => kb.getAriaLabel()?.includes('UpArrow'))?.getAriaLabel();
191
}
192
} else {
193
// Quick chat
194
if (focusInput) {
195
kb = kbs.find(kb => kb.getAriaLabel()?.includes('UpArrow'))?.getAriaLabel();
196
} else {
197
kb = kbs.find(kb => kb.getAriaLabel()?.includes('DownArrow'))?.getAriaLabel();
198
}
199
}
200
return !!kb ? ` (${kb})` : fallback;
201
}
202
203