Skip to content

Commit be94045

Browse files
authored
Merge pull request microsoft#185803 from microsoft/merogge/inline-chat-revamp
improve inline chat accessibility
2 parents 1f9d544 + 73d4c39 commit be94045

File tree

8 files changed

+77
-43
lines changed

8 files changed

+77
-43
lines changed

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export function getAccessibilityHelpText(accessor: ServicesAccessor, type: 'pane
3939
content.push(localize('inlineChat.explain', "When a request is prefixed with /explain, a response will explain the code in the current selection and the chat view will be focused."));
4040
content.push(localize('inlineChat.toolbar', "Use tab to reach conditional parts like commands, status, message responses and more."));
4141
}
42+
content.push(localize('chat.audioCues', "Audio cues can be changed via settings with a prefix of audioCues.chat."));
4243
return content.join('\n');
4344
}
4445

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import { ChatTreeItem, IChatAccessibilityService, IChatWidget, IChatWidgetServic
2626
import { ChatContributionService } from 'vs/workbench/contrib/chat/browser/chatContributionServiceImpl';
2727
import { ChatEditor, IChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatEditor';
2828
import { ChatEditorInput, ChatEditorInputSerializer } from 'vs/workbench/contrib/chat/browser/chatEditorInput';
29-
import { ChatAccessibilityService, ChatWidgetService } from 'vs/workbench/contrib/chat/browser/chatWidget';
29+
import { ChatWidgetService } from 'vs/workbench/contrib/chat/browser/chatWidget';
3030
import 'vs/workbench/contrib/chat/browser/contrib/chatInputEditorContrib';
3131
import { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService';
3232
import { IChatService } from 'vs/workbench/contrib/chat/common/chatService';
@@ -42,6 +42,7 @@ import { AccessibleViewType, IAccessibleViewService } from 'vs/workbench/contrib
4242
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
4343
import { IChatResponseViewModel, isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel';
4444
import { CONTEXT_IN_CHAT_SESSION } from 'vs/workbench/contrib/chat/common/chatContextKeys';
45+
import { ChatAccessibilityService } from 'vs/workbench/contrib/chat/browser/chatAccessibilityService';
4546

4647
// Register configuration
4748
const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export interface IChatWidgetService {
3434
export interface IChatAccessibilityService {
3535
readonly _serviceBrand: undefined;
3636
acceptRequest(): void;
37-
acceptResponse(response?: IChatResponseViewModel): void;
37+
acceptResponse(response?: IChatResponseViewModel | string): void;
3838
}
3939

4040
export interface IChatCodeBlockInfo {
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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 { status } from 'vs/base/browser/ui/aria/aria';
7+
import { RunOnceScheduler } from 'vs/base/common/async';
8+
import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
9+
import { AudioCue, AudioCueGroupId, IAudioCueService } from 'vs/platform/audioCues/browser/audioCueService';
10+
import { IChatAccessibilityService } from 'vs/workbench/contrib/chat/browser/chat';
11+
import { IChatResponseViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel';
12+
13+
const CHAT_RESPONSE_PENDING_AUDIO_CUE_LOOP_MS = 5000;
14+
export class ChatAccessibilityService extends Disposable implements IChatAccessibilityService {
15+
16+
declare readonly _serviceBrand: undefined;
17+
18+
private _responsePendingAudioCue: IDisposable | undefined;
19+
private _hasReceivedRequest: boolean = false;
20+
private _runOnceScheduler: RunOnceScheduler;
21+
22+
constructor(@IAudioCueService private readonly _audioCueService: IAudioCueService) {
23+
super();
24+
this._register(this._runOnceScheduler = new RunOnceScheduler(() => {
25+
if (!this._hasReceivedRequest) {
26+
this._responsePendingAudioCue = this._audioCueService.playAudioCueLoop(AudioCue.chatResponsePending, CHAT_RESPONSE_PENDING_AUDIO_CUE_LOOP_MS);
27+
}
28+
}, CHAT_RESPONSE_PENDING_AUDIO_CUE_LOOP_MS));
29+
}
30+
acceptRequest(): void {
31+
this._audioCueService.playAudioCue(AudioCue.chatRequestSent, true);
32+
this._runOnceScheduler.schedule();
33+
}
34+
acceptResponse(response?: IChatResponseViewModel | string): void {
35+
this._hasReceivedRequest = true;
36+
const isPanelChat = typeof response !== 'string';
37+
this._responsePendingAudioCue?.dispose();
38+
this._runOnceScheduler?.cancel();
39+
this._audioCueService.playRandomAudioCue(AudioCueGroupId.chatResponseReceived, true);
40+
this._hasReceivedRequest = false;
41+
if (!response) {
42+
return;
43+
}
44+
const errorDetails = isPanelChat && response.errorDetails ? ` ${response.errorDetails.message}` : '';
45+
const content = isPanelChat ? response.response.value : response;
46+
status(content + errorDetails);
47+
}
48+
}

src/vs/workbench/contrib/chat/browser/chatWidget.ts

Lines changed: 0 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,7 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import * as dom from 'vs/base/browser/dom';
7-
import { status } from 'vs/base/browser/ui/aria/aria';
87
import { ITreeContextMenuEvent, ITreeElement } from 'vs/base/browser/ui/tree/tree';
9-
import { disposableTimeout } from 'vs/base/common/async';
108
import { CancellationToken } from 'vs/base/common/cancellation';
119
import { Emitter } from 'vs/base/common/event';
1210
import { Disposable, DisposableStore, IDisposable, combinedDisposable, toDisposable } from 'vs/base/common/lifecycle';
@@ -17,7 +15,6 @@ import 'vs/css!./media/chat';
1715
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
1816
import { localize } from 'vs/nls';
1917
import { MenuId } from 'vs/platform/actions/common/actions';
20-
import { AudioCue, AudioCueGroupId, IAudioCueService } from 'vs/platform/audioCues/browser/audioCueService';
2118
import { IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
2219
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
2320
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
@@ -520,36 +517,3 @@ export class ChatWidgetService implements IChatWidgetService {
520517
}
521518
}
522519

523-
524-
const CHAT_RESPONSE_PENDING_AUDIO_CUE_LOOP_MS = 5000;
525-
export class ChatAccessibilityService extends Disposable implements IChatAccessibilityService {
526-
527-
declare readonly _serviceBrand: undefined;
528-
529-
private _responsePendingAudioCue: IDisposable | undefined;
530-
private _hasReceivedRequest: boolean = false;
531-
532-
constructor(@IAudioCueService private readonly _audioCueService: IAudioCueService) {
533-
super();
534-
}
535-
acceptRequest(): void {
536-
this._audioCueService.playAudioCue(AudioCue.chatRequestSent, true);
537-
this._register(disposableTimeout(() => {
538-
if (!this._hasReceivedRequest) {
539-
this._responsePendingAudioCue = this._audioCueService.playAudioCueLoop(AudioCue.chatResponsePending, CHAT_RESPONSE_PENDING_AUDIO_CUE_LOOP_MS);
540-
}
541-
}, CHAT_RESPONSE_PENDING_AUDIO_CUE_LOOP_MS));
542-
}
543-
acceptResponse(response?: IChatResponseViewModel): void {
544-
this._hasReceivedRequest = true;
545-
this._responsePendingAudioCue?.dispose();
546-
this._audioCueService.playRandomAudioCue(AudioCueGroupId.chatResponseReceived, true);
547-
if (!response) {
548-
return;
549-
}
550-
const errorDetails = response.errorDetails ? ` ${response.errorDetails.message}` : '';
551-
status(response.response.value + errorDetails);
552-
this._hasReceivedRequest = false;
553-
}
554-
}
555-

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

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { renderMarkdown } from 'vs/base/browser/markdownRenderer';
7+
import * as aria from 'vs/base/browser/ui/aria/aria';
78
import { Barrier, raceCancellationError } from 'vs/base/common/async';
89
import { CancellationTokenSource } from 'vs/base/common/cancellation';
910
import { toErrorMessage } from 'vs/base/common/errorMessage';
@@ -32,7 +33,7 @@ import { EditResponse, EmptyResponse, ErrorResponse, ExpansionState, IInlineChat
3233
import { EditModeStrategy, LivePreviewStrategy, LiveStrategy, PreviewStrategy } from 'vs/workbench/contrib/inlineChat/browser/inlineChatStrategies';
3334
import { InlineChatZoneWidget } from 'vs/workbench/contrib/inlineChat/browser/inlineChatWidget';
3435
import { CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST, CTX_INLINE_CHAT_LAST_FEEDBACK, IInlineChatRequest, IInlineChatResponse, INLINE_CHAT_ID, EditMode, InlineChatResponseFeedbackKind, CTX_INLINE_CHAT_LAST_RESPONSE_TYPE, InlineChatResponseType, CTX_INLINE_CHAT_DID_EDIT, CTX_INLINE_CHAT_HAS_STASHED_SESSION, InlineChateResponseTypes, CTX_INLINE_CHAT_RESPONSE_TYPES } from 'vs/workbench/contrib/inlineChat/common/inlineChat';
35-
import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat';
36+
import { IChatAccessibilityService, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat';
3637
import { IChatService } from 'vs/workbench/contrib/chat/common/chatService';
3738
import { INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/services/notebookEditorService';
3839
import { CellUri } from 'vs/workbench/contrib/notebook/common/notebookCommon';
@@ -115,6 +116,7 @@ export class InlineChatController implements IEditorContribution {
115116
@IContextKeyService contextKeyService: IContextKeyService,
116117
@IAccessibilityService private readonly _accessibilityService: IAccessibilityService,
117118
@IKeybindingService private readonly _keybindingService: IKeybindingService,
119+
@IChatAccessibilityService private readonly _chatAccessibilityService: IChatAccessibilityService
118120
) {
119121
this._ctxHasActiveRequest = CTX_INLINE_CHAT_HAS_ACTIVE_REQUEST.bindTo(contextKeyService);
120122
this._ctxDidEdit = CTX_INLINE_CHAT_DID_EDIT.bindTo(contextKeyService);
@@ -412,6 +414,7 @@ export class InlineChatController implements IEditorContribution {
412414
if (options.message) {
413415
this._zone.value.widget.value = options.message;
414416
this._zone.value.widget.selectAll();
417+
aria.alert(options.message);
415418
delete options.message;
416419
}
417420

@@ -506,6 +509,7 @@ export class InlineChatController implements IEditorContribution {
506509
selection: this._editor.getSelection(),
507510
wholeRange: this._activeSession.wholeRange.value,
508511
};
512+
this._chatAccessibilityService.acceptRequest();
509513
const task = this._activeSession.provider.provideResponse(this._activeSession.session, request, requestCts.token);
510514
this._log('request started', this._activeSession.provider.debugName, this._activeSession.session, request);
511515

@@ -596,6 +600,8 @@ export class InlineChatController implements IEditorContribution {
596600
const { response } = this._activeSession.lastExchange!;
597601
this._showWidget(false);
598602

603+
let status: string | undefined;
604+
599605
this._ctxLastResponseType.set(response instanceof EditResponse || response instanceof MarkdownResponse
600606
? response.raw.type
601607
: undefined);
@@ -618,13 +624,15 @@ export class InlineChatController implements IEditorContribution {
618624

619625
if (response instanceof EmptyResponse) {
620626
// show status message
621-
this._zone.value.widget.updateStatus(localize('empty', "No results, please refine your input and try again"), { classes: ['warn'] });
627+
status = localize('empty', "No results, please refine your input and try again");
628+
this._zone.value.widget.updateStatus(status, { classes: ['warn'] });
622629
return State.WAIT_FOR_INPUT;
623630

624631
} else if (response instanceof ErrorResponse) {
625632
// show error
626633
if (!response.isCancellation) {
627-
this._zone.value.widget.updateStatus(response.message, { classes: ['error'] });
634+
status = response.message;
635+
this._zone.value.widget.updateStatus(status, { classes: ['error'] });
628636
}
629637

630638
} else if (response instanceof MarkdownResponse) {
@@ -633,6 +641,10 @@ export class InlineChatController implements IEditorContribution {
633641
this._zone.value.widget.updateStatus('');
634642
this._zone.value.widget.updateMarkdownMessage(renderedMarkdown.element);
635643
this._zone.value.widget.updateToolbar(true);
644+
const content = renderedMarkdown.element.textContent;
645+
if (content) {
646+
status = localize('markdownResponseMessage', "{0}", content);
647+
}
636648
this._activeSession.lastExpansionState = this._zone.value.widget.expansionState;
637649

638650
} else if (response instanceof EditResponse) {
@@ -644,9 +656,10 @@ export class InlineChatController implements IEditorContribution {
644656
if (!canContinue) {
645657
return State.ACCEPT;
646658
}
647-
659+
status = localize('editResponseMessage', "Navigate to the diff editor to review proposed changes.");
648660
await this._strategy.renderChanges(response);
649661
}
662+
this._chatAccessibilityService.acceptResponse(status);
650663

651664
return State.WAIT_FOR_INPUT;
652665
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -835,5 +835,6 @@ export class InlineChatZoneWidget extends ZoneWidget {
835835
this._ctxCursorPosition.reset();
836836
this.widget.reset();
837837
super.hide();
838+
aria.status(localize('inlineChatClosed', 'Closed inline chat widget'));
838839
}
839840
}

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,10 @@ import { mock } from 'vs/base/test/common/mock';
2525
import { Emitter, Event } from 'vs/base/common/event';
2626
import { equals } from 'vs/base/common/arrays';
2727
import { timeout } from 'vs/base/common/async';
28+
import { IChatAccessibilityService } from 'vs/workbench/contrib/chat/browser/chat';
29+
import { IChatResponseViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel';
2830

29-
suite('InteractiveChatontroller', function () {
31+
suite('InteractiveChatController', function () {
3032

3133
class TestController extends InlineChatController {
3234

@@ -98,6 +100,10 @@ suite('InteractiveChatontroller', function () {
98100
done() { },
99101
};
100102
}
103+
}],
104+
[IChatAccessibilityService, new class extends mock<IChatAccessibilityService>() {
105+
override acceptResponse(response?: IChatResponseViewModel): void { }
106+
override acceptRequest(): void { }
101107
}]
102108
);
103109

0 commit comments

Comments
 (0)