Skip to content

Commit fd1ddc6

Browse files
authored
Merge pull request microsoft#188517 from microsoft/merogge/inline-help-chat
add accessible view for inline chat
2 parents 432b958 + 2dccf7d commit fd1ddc6

File tree

8 files changed

+83
-11
lines changed

8 files changed

+83
-11
lines changed

.vscode/notebooks/my-endgame.github-issues

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@
4444
"language": "github-issues",
4545
"value": "$REPOS $MILESTONE $MINE is:issue is:closed reason:completed label:feature-request -label:verification-needed -label:on-testplan -label:verified -label:*duplicate"
4646
},
47+
{
48+
"kind": 1,
49+
"language": "markdown",
50+
"value": ""
51+
},
4752
{
4853
"kind": 1,
4954
"language": "markdown",

src/vs/workbench/contrib/accessibility/browser/accessibleView.ts

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ export interface IAccessibleViewService {
5353
* If the setting is enabled, provides the open accessible view hint as a localized string.
5454
* @param verbositySettingKey The setting key for the verbosity of the feature
5555
*/
56-
getOpenAriaHint(verbositySettingKey: AccessibilityVerbositySettingId): string | undefined;
56+
getOpenAriaHint(verbositySettingKey: AccessibilityVerbositySettingId): string | null;
5757
}
5858

5959
export const enum AccessibleViewType {
@@ -279,15 +279,11 @@ export class AccessibleViewService extends Disposable implements IAccessibleView
279279
previous(): void {
280280
this._accessibleView?.previous();
281281
}
282-
getOpenAriaHint(verbositySettingKey: AccessibilityVerbositySettingId): string | undefined {
282+
getOpenAriaHint(verbositySettingKey: AccessibilityVerbositySettingId): string | null {
283283
if (!this._configurationService.getValue(verbositySettingKey)) {
284-
return;
284+
return null;
285285
}
286-
let hint = '';
287286
const keybinding = this._keybindingService.lookupKeybinding(AccessibleViewAction.id)?.getAriaLabel();
288-
if (this._configurationService.getValue(verbositySettingKey)) {
289-
hint = keybinding ? localize('chatAccessibleViewHint', "Inspect this in the accessible view with {0}", keybinding) : localize('chatAccessibleViewHintNoKb', "Inspect this in the accessible view via the command Open Accessible View which is currently not triggerable via keybinding");
290-
}
291-
return hint;
287+
return keybinding ? localize('chatAccessibleViewHint', "Inspect this in the accessible view with {0}", keybinding) : localize('chatAccessibleViewHintNoKb', "Inspect this in the accessible view via the command Open Accessible View which is currently not triggerable via keybinding");
292288
}
293289
}

src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@ import { AccessibleDiffViewerNext } from 'vs/editor/browser/widget/diffEditor.co
1818
export function getAccessibilityHelpText(accessor: ServicesAccessor, type: 'panelChat' | 'inlineChat'): string {
1919
const keybindingService = accessor.get(IKeybindingService);
2020
const content = [];
21+
const openAccessibleViewKeybinding = keybindingService.lookupKeybinding('editor.action.accessibleView')?.getAriaLabel();
2122
if (type === 'panelChat') {
2223
content.push(localize('chat.overview', 'The 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.'));
2324
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.'));
25+
content.push(openAccessibleViewKeybinding ? localize('chat.inspectResponse', 'In the input box, inspect the last response in the accessible view via {0}', openAccessibleViewKeybinding) : localize('chat.inspectResponseNoKb', 'With the input box focused, inspect the last response in the accessible view via the Open Accessible View command, which is currently not triggerable by a keybinding.'));
2426
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.'));
2527
content.push(descriptionForCommand('chat.action.focus', 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}).',), localize('workbench.action.chat.focusNoKb', 'To focus the chat request/response list, which can be navigated with up and down arrows, invoke The Focus Chat List command, which is currently not triggerable by a keybinding.'), keybindingService));
2628
content.push(descriptionForCommand('workbench.action.chat.focusInput', localize('workbench.action.chat.focusInput', 'To focus the input box for chat requests, invoke the Focus Chat Input command ({0})'), localize('workbench.action.interactiveSession.focusInputNoKb', 'To focus the input box for chat requests, invoke the Focus Chat Input command, which is currently not triggerable by a keybinding.'), keybindingService));
@@ -35,6 +37,7 @@ export function getAccessibilityHelpText(accessor: ServicesAccessor, type: 'pane
3537
if (upHistoryKeybinding && downHistoryKeybinding) {
3638
content.push(localize('inlineChat.requestHistory', 'In the input box, use {0} and {1} to navigate your request history. Edit input and use enter or the submit button to run a new request.', upHistoryKeybinding, downHistoryKeybinding));
3739
}
40+
content.push(openAccessibleViewKeybinding ? localize('inlineChat.inspectResponse', 'In the input box, inspect the response in the accessible view via {0}', openAccessibleViewKeybinding) : localize('inlineChat.inspectResponseNoKb', 'With the input box focused, inspect the response in the accessible view via the Open Accessible View command, which is currently not triggerable by a keybinding.'));
3841
content.push(localize('inlineChat.contextActions', "Context menu actions may run a request prefixed with a /. Type / to discover such ready-made commands."));
3942
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."));
4043
const diffReviewKeybinding = keybindingService.lookupKeybinding(AccessibleDiffViewerNext.id)?.getAriaLabel();

src/vs/workbench/contrib/inlineChat/browser/inlineChat.contribution.ts

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,20 @@ import { registerAction2 } from 'vs/platform/actions/common/actions';
77
import { EditorContributionInstantiation, registerEditorContribution } from 'vs/editor/browser/editorExtensions';
88
import { InlineChatController } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController';
99
import * as InlineChatActions from 'vs/workbench/contrib/inlineChat/browser/inlineChatActions';
10-
import { IInlineChatService, INLINE_CHAT_ID, INTERACTIVE_EDITOR_ACCESSIBILITY_HELP_ID } from 'vs/workbench/contrib/inlineChat/common/inlineChat';
10+
import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_RESPONSE_FOCUSED, IInlineChatService, INLINE_CHAT_ID, INTERACTIVE_EDITOR_ACCESSIBILITY_HELP_ID } from 'vs/workbench/contrib/inlineChat/common/inlineChat';
1111
import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions';
1212
import { InlineChatServiceImpl } from 'vs/workbench/contrib/inlineChat/common/inlineChatServiceImpl';
1313
import { IInlineChatSessionService, InlineChatSessionService } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession';
1414
import { Registry } from 'vs/platform/registry/common/platform';
15-
import { IWorkbenchContributionsRegistry, Extensions } from 'vs/workbench/common/contributions';
1615
import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';
1716
import { InlineChatNotebookContribution } from 'vs/workbench/contrib/inlineChat/browser/inlineChatNotebook';
17+
import { AccessibilityVerbositySettingId, AccessibleViewAction } from 'vs/workbench/contrib/accessibility/browser/accessibilityContribution';
18+
import { AccessibleViewType, IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView';
19+
import { Disposable } from 'vs/base/common/lifecycle';
20+
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
21+
import { localize } from 'vs/nls';
22+
import { Extensions, IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions';
23+
import { ContextKeyExpr } from 'vs/platform/contextkey/common/contextkey';
1824

1925
registerSingleton(IInlineChatService, InlineChatServiceImpl, InstantiationType.Delayed);
2026
registerSingleton(IInlineChatSessionService, InlineChatSessionService, InstantiationType.Delayed);
@@ -51,3 +57,41 @@ registerAction2(InlineChatActions.CopyRecordings);
5157

5258
Registry.as<IWorkbenchContributionsRegistry>(Extensions.Workbench)
5359
.registerWorkbenchContribution(InlineChatNotebookContribution, LifecyclePhase.Restored);
60+
61+
62+
class InlineChatAccessibleViewContribution extends Disposable {
63+
static ID: 'inlineChatAccessibleViewContribution';
64+
constructor() {
65+
super();
66+
this._register(AccessibleViewAction.addImplementation(100, 'inlineChat', accessor => {
67+
const accessibleViewService = accessor.get(IAccessibleViewService);
68+
const codeEditorService = accessor.get(ICodeEditorService);
69+
70+
const editor = (codeEditorService.getActiveCodeEditor() || codeEditorService.getFocusedCodeEditor());
71+
if (!editor) {
72+
return false;
73+
}
74+
const controller = InlineChatController.get(editor);
75+
if (!controller) {
76+
return false;
77+
}
78+
const responseContent = controller?.getMessage();
79+
if (!responseContent) {
80+
return false;
81+
}
82+
accessibleViewService.show({
83+
verbositySettingKey: AccessibilityVerbositySettingId.InlineChat,
84+
provideContent(): string { return responseContent; },
85+
onClose() {
86+
controller.focus();
87+
},
88+
89+
options: { ariaLabel: localize('inlineChatAccessibleView', "Inline Chat Accessible View"), type: AccessibleViewType.View }
90+
});
91+
return true;
92+
}, ContextKeyExpr.or(CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_RESPONSE_FOCUSED)));
93+
}
94+
}
95+
96+
const workbenchContributionsRegistry = Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench);
97+
workbenchContributionsRegistry.registerWorkbenchContribution(InlineChatAccessibleViewContribution, LifecyclePhase.Eventually);

src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,10 @@ export class InlineChatController implements IEditorContribution {
158158
}
159159
}
160160

161+
getMessage(): string | undefined {
162+
return this._zone.value.widget.responseContent;
163+
}
164+
161165
getId(): string {
162166
return INLINE_CHAT_ID;
163167
}

src/vs/workbench/contrib/inlineChat/browser/inlineChatWidget.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { localize } from 'vs/nls';
1212
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
1313
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
1414
import { ZoneWidget } from 'vs/editor/contrib/zoneWidget/browser/zoneWidget';
15-
import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_EMPTY, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_VISIBLE, MENU_INLINE_CHAT_WIDGET, MENU_INLINE_CHAT_WIDGET_STATUS, MENU_INLINE_CHAT_WIDGET_MARKDOWN_MESSAGE, CTX_INLINE_CHAT_MESSAGE_CROP_STATE, IInlineChatSlashCommand, MENU_INLINE_CHAT_WIDGET_FEEDBACK, ACTION_REGENERATE_RESPONSE, ACTION_VIEW_IN_CHAT, MENU_INLINE_CHAT_WIDGET_TOGGLE, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_INNER_CURSOR_START, CTX_INLINE_CHAT_INNER_CURSOR_END } from 'vs/workbench/contrib/inlineChat/common/inlineChat';
15+
import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_EMPTY, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_VISIBLE, MENU_INLINE_CHAT_WIDGET, MENU_INLINE_CHAT_WIDGET_STATUS, MENU_INLINE_CHAT_WIDGET_MARKDOWN_MESSAGE, CTX_INLINE_CHAT_MESSAGE_CROP_STATE, IInlineChatSlashCommand, MENU_INLINE_CHAT_WIDGET_FEEDBACK, ACTION_REGENERATE_RESPONSE, ACTION_VIEW_IN_CHAT, MENU_INLINE_CHAT_WIDGET_TOGGLE, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_INNER_CURSOR_START, CTX_INLINE_CHAT_INNER_CURSOR_END, CTX_INLINE_CHAT_RESPONSE_FOCUSED } from 'vs/workbench/contrib/inlineChat/common/inlineChat';
1616
import { IModelDeltaDecoration, ITextModel } from 'vs/editor/common/model';
1717
import { EventType, Dimension, addDisposableListener, getActiveElement, getTotalHeight, getTotalWidth, h, reset } from 'vs/base/browser/dom';
1818
import { Emitter, Event, MicrotaskEmitter } from 'vs/base/common/event';
@@ -51,6 +51,7 @@ import * as aria from 'vs/base/browser/ui/aria/aria';
5151
import { IMenuWorkbenchButtonBarOptions, MenuWorkbenchButtonBar } from 'vs/platform/actions/browser/buttonbar';
5252
import { SlashCommandContentWidget } from 'vs/workbench/contrib/chat/browser/chatSlashCommandContentWidget';
5353
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
54+
import { IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView';
5455

5556
const defaultAriaLabel = localize('aria-label', "Inline Chat Input");
5657

@@ -163,6 +164,7 @@ export class InlineChatWidget {
163164
private readonly _ctxInnerCursorStart: IContextKey<boolean>;
164165
private readonly _ctxInnerCursorEnd: IContextKey<boolean>;
165166
private readonly _ctxInputEditorFocused: IContextKey<boolean>;
167+
private readonly _ctxResponseFocused: IContextKey<boolean>;
166168

167169
private readonly _progressBar: ProgressBar;
168170

@@ -198,6 +200,7 @@ export class InlineChatWidget {
198200
@IAccessibilityService private readonly _accessibilityService: IAccessibilityService,
199201
@IConfigurationService private readonly _configurationService: IConfigurationService,
200202
@IContextMenuService private readonly _contextMenuService: IContextMenuService,
203+
@IAccessibleViewService private readonly _accessibleViewService: IAccessibleViewService
201204
) {
202205

203206
// input editor logic
@@ -215,6 +218,9 @@ export class InlineChatWidget {
215218
this._store.add(this._inputEditor.onDidChangeModelContent(() => this._onDidChangeInput.fire(this)));
216219
this._store.add(this._inputEditor.onDidLayoutChange(() => this._onDidChangeHeight.fire()));
217220
this._store.add(this._inputEditor.onDidContentSizeChange(() => this._onDidChangeHeight.fire()));
221+
this._store.add(addDisposableListener(this._elements.message, 'focus', () => this._ctxResponseFocused.set(true)));
222+
this._store.add(addDisposableListener(this._elements.message, 'blur', () => this._ctxResponseFocused.reset()));
223+
218224
this._store.add(this._configurationService.onDidChangeConfiguration(e => {
219225
if (e.affectsConfiguration(AccessibilityVerbositySettingId.InlineChat)) {
220226
this._updateAriaLabel();
@@ -235,6 +241,7 @@ export class InlineChatWidget {
235241
this._ctxInnerCursorStart = CTX_INLINE_CHAT_INNER_CURSOR_START.bindTo(this._contextKeyService);
236242
this._ctxInnerCursorEnd = CTX_INLINE_CHAT_INNER_CURSOR_END.bindTo(this._contextKeyService);
237243
this._ctxInputEditorFocused = CTX_INLINE_CHAT_FOCUSED.bindTo(this._contextKeyService);
244+
this._ctxResponseFocused = CTX_INLINE_CHAT_RESPONSE_FOCUSED.bindTo(this._contextKeyService);
238245

239246
// (1) inner cursor position (last/first line selected)
240247
const updateInnerCursorFirstLast = () => {
@@ -359,6 +366,7 @@ export class InlineChatWidget {
359366
this._previewCreateEditor = new IdleValue(() => this._store.add(_instantiationService.createInstance(EmbeddedCodeEditorWidget, this._elements.previewCreate, _previewEditorEditorOptions, codeEditorWidgetOptions, parentEditor)));
360367

361368
this._elements.message.tabIndex = 0;
369+
this._elements.message.ariaLabel = this._accessibleViewService.getOpenAriaHint(AccessibilityVerbositySettingId.InlineChat);
362370
this._elements.statusLabel.tabIndex = 0;
363371
const markdownMessageToolbar = this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.messageActions, MENU_INLINE_CHAT_WIDGET_MARKDOWN_MESSAGE, workbenchToolbarOptions);
364372
this._store.add(markdownMessageToolbar.onDidChangeMenuItems(() => this._onDidChangeHeight.fire()));
@@ -489,6 +497,10 @@ export class InlineChatWidget {
489497
this._preferredExpansionState = expansionState;
490498
}
491499

500+
get responseContent(): string | undefined {
501+
return this._elements.markdownMessage.textContent ?? undefined;
502+
}
503+
492504
updateMarkdownMessage(message: Node | undefined) {
493505
this._elements.markdownMessage.classList.toggle('hidden', !message);
494506
let expansionState: ExpansionState;

src/vs/workbench/contrib/inlineChat/common/inlineChat.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ export const INTERACTIVE_EDITOR_ACCESSIBILITY_HELP_ID = 'interactiveEditorAccess
121121
export const CTX_INLINE_CHAT_HAS_PROVIDER = new RawContextKey<boolean>('inlineChatHasProvider', false, localize('inlineChatHasProvider', "Whether a provider for interactive editors exists"));
122122
export const CTX_INLINE_CHAT_VISIBLE = new RawContextKey<boolean>('inlineChatVisible', false, localize('inlineChatVisible', "Whether the interactive editor input is visible"));
123123
export const CTX_INLINE_CHAT_FOCUSED = new RawContextKey<boolean>('inlineChatFocused', false, localize('inlineChatFocused', "Whether the interactive editor input is focused"));
124+
export const CTX_INLINE_CHAT_RESPONSE_FOCUSED = new RawContextKey<boolean>('inlineChatResponseFocused', false, localize('inlineChatResponseFocused', "Whether the interactive widget's response is focused"));
124125
export const CTX_INLINE_CHAT_EMPTY = new RawContextKey<boolean>('inlineChatEmpty', false, localize('inlineChatEmpty', "Whether the interactive editor input is empty"));
125126
export const CTX_INLINE_CHAT_INNER_CURSOR_FIRST = new RawContextKey<boolean>('inlineChatInnerCursorFirst', false, localize('inlineChatInnerCursorFirst', "Whether the cursor of the iteractive editor input is on the first line"));
126127
export const CTX_INLINE_CHAT_INNER_CURSOR_LAST = new RawContextKey<boolean>('inlineChatInnerCursorLast', false, localize('inlineChatInnerCursorLast', "Whether the cursor of the iteractive editor input is on the last line"));

src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ import { equals } from 'vs/base/common/arrays';
2727
import { timeout } from 'vs/base/common/async';
2828
import { IChatAccessibilityService } from 'vs/workbench/contrib/chat/browser/chat';
2929
import { IChatResponseViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel';
30+
import { IAccessibleViewService } from 'vs/workbench/contrib/accessibility/browser/accessibleView';
31+
import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityContribution';
3032

3133
suite('InteractiveChatController', function () {
3234

@@ -104,6 +106,11 @@ suite('InteractiveChatController', function () {
104106
[IChatAccessibilityService, new class extends mock<IChatAccessibilityService>() {
105107
override acceptResponse(response?: IChatResponseViewModel): void { }
106108
override acceptRequest(): void { }
109+
}],
110+
[IAccessibleViewService, new class extends mock<IAccessibleViewService>() {
111+
override getOpenAriaHint(verbositySettingKey: AccessibilityVerbositySettingId): string | null {
112+
return null;
113+
}
107114
}]
108115
);
109116

0 commit comments

Comments
 (0)