Skip to content

Commit 618daea

Browse files
authored
Make chat elicitation request part accessible (microsoft#257491)
1 parent a3dc9ad commit 618daea

File tree

7 files changed

+54
-8
lines changed

7 files changed

+54
-8
lines changed

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { IChatAgentCommand, IChatAgentData } from '../common/chatAgents.js';
1717
import { IChatResponseModel } from '../common/chatModel.js';
1818
import { IParsedChatRequest } from '../common/chatParserTypes.js';
1919
import { CHAT_PROVIDER_ID } from '../common/chatParticipantContribTypes.js';
20-
import { IChatSendRequestOptions } from '../common/chatService.js';
20+
import { IChatElicitationRequest, IChatSendRequestOptions } from '../common/chatService.js';
2121
import { IChatRequestViewModel, IChatResponseViewModel, IChatViewModel } from '../common/chatViewModel.js';
2222
import { ChatAgentLocation, ChatModeKind } from '../common/constants.js';
2323
import { ChatAttachmentModel } from './chatAttachmentModel.js';
@@ -92,6 +92,7 @@ export interface IChatAccessibilityService {
9292
readonly _serviceBrand: undefined;
9393
acceptRequest(): number;
9494
acceptResponse(response: IChatResponseViewModel | string | undefined, requestId: number, isVoiceInput?: boolean): void;
95+
acceptElicitation(message: IChatElicitationRequest): void;
9596
}
9697

9798
export interface IChatCodeBlockInfo {

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

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,16 +127,31 @@ export class ChatAccessibilityProvider implements IListAccessibilityProvider<Cha
127127
fileTreeCountHint = localize('multiFileTreeHint', "{0} file trees ", fileTreeCount);
128128
break;
129129
}
130+
131+
const elicitationCount = element.response.value.filter(v => v.kind === 'elicitation');
132+
let elicitationHint = '';
133+
for (const elicitation of elicitationCount) {
134+
const title = typeof elicitation.title === 'string' ? elicitation.title : elicitation.title.value;
135+
const message = typeof elicitation.message === 'string' ? elicitation.message : elicitation.message.value;
136+
elicitationHint += title + ' ' + message;
137+
}
138+
130139
const codeBlockCount = marked.lexer(element.response.toString()).filter(token => token.type === 'code')?.length ?? 0;
131140
switch (codeBlockCount) {
132141
case 0:
133-
label = accessibleViewHint ? localize('noCodeBlocksHint', "{0}{1}{2}{3} {4}", toolInvocationHint, fileTreeCountHint, tableCountHint, element.response.toString(), accessibleViewHint) : localize('noCodeBlocks', "{0} {1}", fileTreeCountHint, element.response.toString());
142+
label = accessibleViewHint
143+
? localize('noCodeBlocksHint', "{0}{1}{2}{3}{4} {5}", toolInvocationHint, fileTreeCountHint, elicitationHint, tableCountHint, element.response.toString(), accessibleViewHint)
144+
: localize('noCodeBlocks', "{0}{1}{2} {3}", fileTreeCountHint, elicitationHint, tableCountHint, element.response.toString());
134145
break;
135146
case 1:
136-
label = accessibleViewHint ? localize('singleCodeBlockHint', "{0}{1}1 code block: {2} {3}{4}", toolInvocationHint, fileTreeCountHint, tableCountHint, element.response.toString(), accessibleViewHint) : localize('singleCodeBlock', "{0} 1 code block: {1}", fileTreeCountHint, element.response.toString());
147+
label = accessibleViewHint
148+
? localize('singleCodeBlockHint', "{0}{1}{2}1 code block: {3} {4}{5}", toolInvocationHint, fileTreeCountHint, elicitationHint, tableCountHint, element.response.toString(), accessibleViewHint)
149+
: localize('singleCodeBlock', "{0}{1}1 code block: {2} {3}", fileTreeCountHint, elicitationHint, tableCountHint, element.response.toString());
137150
break;
138151
default:
139-
label = accessibleViewHint ? localize('multiCodeBlockHint', "{0}{1}{2} code blocks: {3}{4}", toolInvocationHint, fileTreeCountHint, tableCountHint, codeBlockCount, element.response.toString(), accessibleViewHint) : localize('multiCodeBlock', "{0} {1} code blocks", fileTreeCountHint, codeBlockCount, element.response.toString());
152+
label = accessibleViewHint
153+
? localize('multiCodeBlockHint', "{0}{1}{2}{3} code blocks: {4}{5} {6}", toolInvocationHint, fileTreeCountHint, elicitationHint, tableCountHint, codeBlockCount, element.response.toString(), accessibleViewHint)
154+
: localize('multiCodeBlock', "{0}{1}{2} code blocks: {3} {4}", fileTreeCountHint, elicitationHint, codeBlockCount, tableCountHint, element.response.toString());
140155
break;
141156
}
142157
return label;

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6-
import { status } from '../../../../base/browser/ui/aria/aria.js';
6+
import { alert, status } from '../../../../base/browser/ui/aria/aria.js';
77
import { Disposable, DisposableMap } from '../../../../base/common/lifecycle.js';
88
import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js';
99
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
@@ -14,6 +14,7 @@ import { renderAsPlaintext } from '../../../../base/browser/markdownRenderer.js'
1414
import { MarkdownString } from '../../../../base/common/htmlContent.js';
1515
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
1616
import { AccessibilityVoiceSettingId } from '../../accessibility/browser/accessibilityConfiguration.js';
17+
import { IChatElicitationRequest } from '../common/chatService.js';
1718

1819
const CHAT_RESPONSE_PENDING_ALLOWANCE_MS = 4000;
1920
export class ChatAccessibilityService extends Disposable implements IChatAccessibilityService {
@@ -51,4 +52,10 @@ export class ChatAccessibilityService extends Disposable implements IChatAccessi
5152
status(plainTextResponse + errorDetails);
5253
}
5354
}
55+
acceptElicitation(elicitation: IChatElicitationRequest): void {
56+
const title = typeof elicitation.title === 'string' ? elicitation.title : elicitation.title.value;
57+
const message = typeof elicitation.message === 'string' ? elicitation.message : elicitation.message.value;
58+
alert(title + ' ' + message);
59+
this._accessibilitySignalService.playSignal(AccessibilitySignal.chatUserActionRequired, { allowManyInParallel: true });
60+
}
5461
}

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

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { Disposable, IDisposable } from '../../../../../base/common/lifecycle.js
99
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
1010
import { IChatProgressRenderableResponseContent } from '../../common/chatModel.js';
1111
import { IChatElicitationRequest } from '../../common/chatService.js';
12+
import { IChatAccessibilityService } from '../chat.js';
1213
import { ChatConfirmationWidget } from './chatConfirmationWidget.js';
1314
import { IChatContentPart, IChatContentPartRenderContext } from './chatContentParts.js';
1415

@@ -22,6 +23,7 @@ export class ChatElicitationContentPart extends Disposable implements IChatConte
2223
elicitation: IChatElicitationRequest,
2324
context: IChatContentPartRenderContext,
2425
@IInstantiationService private readonly instantiationService: IInstantiationService,
26+
@IChatAccessibilityService private readonly chatAccessibilityService: IChatAccessibilityService
2527
) {
2628
super();
2729

@@ -36,6 +38,8 @@ export class ChatElicitationContentPart extends Disposable implements IChatConte
3638

3739
this._register(confirmationWidget.onDidChangeHeight(() => this._onDidChangeHeight.fire()));
3840

41+
const messageToRender = this.getMessageToRender(elicitation);
42+
3943
this._register(confirmationWidget.onDidClick(async e => {
4044
if (e.data) {
4145
await elicitation.accept();
@@ -44,12 +48,16 @@ export class ChatElicitationContentPart extends Disposable implements IChatConte
4448
}
4549

4650
confirmationWidget.setShowButtons(false);
47-
confirmationWidget.updateMessage(this.getMessageToRender(elicitation));
51+
confirmationWidget.updateMessage(messageToRender);
4852

4953
this._onDidChangeHeight.fire();
5054
}));
5155

56+
57+
this.chatAccessibilityService.acceptElicitation(elicitation);
5258
this.domNode = confirmationWidget.domNode;
59+
this.domNode.tabIndex = 0;
60+
this.domNode.ariaLabel = elicitation.title + ' ' + (typeof messageToRender === 'string' ? messageToRender : messageToRender.value || '');
5361
}
5462

5563
private getMessageToRender(elicitation: IChatElicitationRequest): IMarkdownString | string {

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

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

66
import { renderAsPlaintext } from '../../../../base/browser/markdownRenderer.js';
7-
import { MarkdownString } from '../../../../base/common/htmlContent.js';
7+
import { isMarkdownString, MarkdownString } from '../../../../base/common/htmlContent.js';
88
import { stripIcons } from '../../../../base/common/iconLabels.js';
99
import { Disposable } from '../../../../base/common/lifecycle.js';
1010
import { AccessibleViewProviderId, AccessibleViewType, IAccessibleViewContentProvider } from '../../../../platform/accessibility/browser/accessibleView.js';
@@ -66,7 +66,20 @@ class ChatResponseAccessibleProvider extends Disposable implements IAccessibleVi
6666
responseContent = item.errorDetails.message;
6767
}
6868
if (isResponseVM(item)) {
69-
69+
item.response.value.filter(item => item.kind === 'elicitation').forEach(elicitation => {
70+
const title = elicitation.title;
71+
if (typeof title === 'string') {
72+
responseContent += `${title}\n`;
73+
} else if (isMarkdownString(title)) {
74+
responseContent += renderAsPlaintext(title, { includeCodeBlocksFences: true }) + '\n';
75+
}
76+
const message = elicitation.message;
77+
if (isMarkdownString(message)) {
78+
responseContent += renderAsPlaintext(message, { includeCodeBlocksFences: true });
79+
} else {
80+
responseContent += message;
81+
}
82+
});
7083
const toolInvocations = item.response.value.filter(item => item.kind === 'toolInvocation');
7184
for (const toolInvocation of toolInvocations) {
7285
if (toolInvocation.confirmationMessages) {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ suite('InlineChatController', function () {
186186
[IChatAccessibilityService, new class extends mock<IChatAccessibilityService>() {
187187
override acceptResponse(response: IChatResponseViewModel | undefined, requestId: number): void { }
188188
override acceptRequest(): number { return -1; }
189+
override acceptElicitation(): void { }
189190
}],
190191
[IAccessibleViewService, new class extends mock<IAccessibleViewService>() {
191192
override getOpenAriaHint(verbositySettingKey: AccessibilityVerbositySettingId): string | null {

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,7 @@ suite('InlineChatSession', function () {
117117
[IChatAccessibilityService, new class extends mock<IChatAccessibilityService>() {
118118
override acceptResponse(response: IChatResponseViewModel | undefined, requestId: number): void { }
119119
override acceptRequest(): number { return -1; }
120+
override acceptElicitation(): void { }
120121
}],
121122
[IAccessibleViewService, new class extends mock<IAccessibleViewService>() {
122123
override getOpenAriaHint(verbositySettingKey: AccessibilityVerbositySettingId): string | null {

0 commit comments

Comments
 (0)