Add toggle for thinking content in chat accessible view (#295017)

This commit is contained in:
Copilot
2026-02-18 21:15:07 +00:00
committed by GitHub
parent 4831ff53df
commit 430b0f928e
4 changed files with 114 additions and 6 deletions

View File

@@ -13,6 +13,7 @@ import { localize } from '../../../../../nls.js';
import { AccessibleViewProviderId, AccessibleViewType, IAccessibleViewContentProvider } from '../../../../../platform/accessibility/browser/accessibleView.js';
import { IAccessibleViewImplementation } from '../../../../../platform/accessibility/browser/accessibleViewRegistry.js';
import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js';
import { IStorageService, StorageScope } from '../../../../../platform/storage/common/storage.js';
import { AccessibilityVerbositySettingId } from '../../../accessibility/browser/accessibilityConfiguration.js';
import { migrateLegacyTerminalToolSpecificData } from '../../common/chat.js';
import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';
@@ -29,6 +30,7 @@ export class ChatResponseAccessibleView implements IAccessibleViewImplementation
readonly when = ChatContextKeys.inChatSession;
getProvider(accessor: ServicesAccessor) {
const widgetService = accessor.get(IChatWidgetService);
const storageService = accessor.get(IStorageService);
const widget = widgetService.lastFocusedWidget;
if (!widget) {
return;
@@ -53,13 +55,20 @@ export class ChatResponseAccessibleView implements IAccessibleViewImplementation
return;
}
return new ChatResponseAccessibleProvider(verifiedWidget, focusedItem, chatInputFocused);
return new ChatResponseAccessibleProvider(verifiedWidget, focusedItem, chatInputFocused, storageService);
}
}
type ToolSpecificData = IChatTerminalToolInvocationData | ILegacyChatTerminalToolInvocationData | IChatToolInputInvocationData | IChatExtensionsContent | IChatPullRequestContent | IChatTodoListContent | IChatSubagentToolInvocationData | IChatSimpleToolInvocationData | IChatToolResourcesInvocationData;
type ResultDetails = Array<URI | Location> | IToolResultInputOutputDetails | IToolResultOutputDetails | IToolResultOutputDetailsSerialized;
export const CHAT_ACCESSIBLE_VIEW_INCLUDE_THINKING_STORAGE_KEY = 'chat.accessibleView.includeThinking';
const CHAT_ACCESSIBLE_VIEW_INCLUDE_THINKING_DEFAULT = true;
export function isThinkingContentIncludedInAccessibleView(storageService: IStorageService): boolean {
return storageService.getBoolean(CHAT_ACCESSIBLE_VIEW_INCLUDE_THINKING_STORAGE_KEY, StorageScope.PROFILE, CHAT_ACCESSIBLE_VIEW_INCLUDE_THINKING_DEFAULT);
}
function isOutputDetailsSerialized(obj: unknown): obj is IToolResultOutputDetailsSerialized {
return typeof obj === 'object' && obj !== null && 'output' in obj &&
typeof (obj as IToolResultOutputDetailsSerialized).output === 'object' &&
@@ -212,14 +221,19 @@ export function getToolInvocationA11yDescription(
class ChatResponseAccessibleProvider extends Disposable implements IAccessibleViewContentProvider {
private _focusedItem!: ChatTreeItem;
private readonly _focusedItemDisposables = this._register(new DisposableStore());
private readonly _storageDisposables = this._register(new DisposableStore());
private readonly _onDidChangeContent = this._register(new Emitter<void>());
readonly onDidChangeContent: Event<void> = this._onDidChangeContent.event;
constructor(
private readonly _widget: IChatWidget,
item: ChatTreeItem,
private readonly _wasOpenedFromInput: boolean
private readonly _wasOpenedFromInput: boolean,
private readonly _storageService: IStorageService
) {
super();
this._storageDisposables.add(this._storageService.onDidChangeValue(StorageScope.PROFILE, CHAT_ACCESSIBLE_VIEW_INCLUDE_THINKING_STORAGE_KEY, this._storageDisposables)(() => {
this._onDidChangeContent.fire();
}));
this._setFocusedItem(item);
}
@@ -258,6 +272,9 @@ class ChatResponseAccessibleProvider extends Disposable implements IAccessibleVi
for (const part of item.response.value) {
switch (part.kind) {
case 'thinking': {
if (!this._shouldIncludeThinkingContent()) {
break;
}
const thinkingValue = Array.isArray(part.value) ? part.value.join('') : (part.value || '');
const trimmed = thinkingValue.trim();
if (trimmed) {
@@ -360,6 +377,10 @@ class ChatResponseAccessibleProvider extends Disposable implements IAccessibleVi
return normalized.join('\n');
}
private _shouldIncludeThinkingContent(): boolean {
return isThinkingContentIncludedInAccessibleView(this._storageService);
}
onClose(): void {
this._widget.reveal(this._focusedItem);
if (this._wasOpenedFromInput) {

View File

@@ -9,13 +9,18 @@ import { Action2, registerAction2 } from '../../../../../platform/actions/common
import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js';
import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js';
import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js';
import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js';
import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js';
import { IChatWidgetService } from '../chat.js';
import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';
import { isResponseVM } from '../../common/model/chatViewModel.js';
import { AccessibleViewProviderId } from '../../../../../platform/accessibility/browser/accessibleView.js';
import { CONTEXT_ACCESSIBILITY_MODE_ENABLED } from '../../../../../platform/accessibility/common/accessibility.js';
import { accessibleViewCurrentProviderId, accessibleViewIsShown } from '../../../../contrib/accessibility/browser/accessibilityConfiguration.js';
import { CHAT_ACCESSIBLE_VIEW_INCLUDE_THINKING_STORAGE_KEY, isThinkingContentIncludedInAccessibleView } from '../accessibility/chatResponseAccessibleView.js';
export const ACTION_ID_FOCUS_CHAT_CONFIRMATION = 'workbench.action.chat.focusConfirmation';
export const ACTION_ID_TOGGLE_THINKING_CONTENT_ACCESSIBLE_VIEW = 'workbench.action.chat.toggleThinkingContentAccessibleView';
class AnnounceChatConfirmationAction extends Action2 {
constructor() {
@@ -73,6 +78,35 @@ class AnnounceChatConfirmationAction extends Action2 {
}
}
class ToggleThinkingContentAccessibleViewAction extends Action2 {
constructor() {
super({
id: ACTION_ID_TOGGLE_THINKING_CONTENT_ACCESSIBLE_VIEW,
title: { value: localize('toggleThinkingContentAccessibleView', 'Toggle Thinking Content in Accessible View'), original: 'Toggle Thinking Content in Accessible View' },
category: { value: localize('chat.category', 'Chat'), original: 'Chat' },
precondition: ChatContextKeys.enabled,
f1: true,
keybinding: {
primary: KeyMod.Alt | KeyCode.KeyT,
weight: KeybindingWeight.WorkbenchContrib,
when: ContextKeyExpr.and(accessibleViewIsShown, ContextKeyExpr.equals(accessibleViewCurrentProviderId.key, AccessibleViewProviderId.PanelChat))
}
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const storageService = accessor.get(IStorageService);
const includeThinking = isThinkingContentIncludedInAccessibleView(storageService);
const updatedValue = !includeThinking;
storageService.store(CHAT_ACCESSIBLE_VIEW_INCLUDE_THINKING_STORAGE_KEY, updatedValue, StorageScope.PROFILE, StorageTarget.USER);
alert(updatedValue
? localize('thinkingContentShown', 'Thinking content will be included in the accessible view.')
: localize('thinkingContentHidden', 'Thinking content will be hidden from the accessible view.')
);
}
}
export function registerChatAccessibilityActions(): void {
registerAction2(AnnounceChatConfirmationAction);
registerAction2(ToggleThinkingContentAccessibleViewAction);
}

View File

@@ -74,7 +74,8 @@ export function getAccessibilityHelpText(type: 'panelChat' | 'inlineChat' | 'age
}
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.'));
content.push(localize('chat.attachments.removal', 'To remove attached contexts, focus an attachment and press Delete or Backspace.'));
content.push(localize('chat.inspectResponse', 'In the input box, inspect the last response in the accessible view{0}. Thinking content is included in order.', '<keybinding:editor.action.accessibleView>'));
content.push(localize('chat.inspectResponse', 'In the input box, inspect the last response in the accessible view{0}. Thinking content is included in order by default.', '<keybinding:editor.action.accessibleView>'));
content.push(localize('chat.inspectResponseThinkingToggle', 'To include or exclude thinking content in the accessible view, run the Toggle Thinking Content in Accessible View command from the Command Palette.'));
content.push(localize('workbench.action.chat.focus', 'To focus the chat request and response list, invoke the Focus Chat command{0}. This will move focus to the most recent response, which you can then navigate using the up and down arrow keys.', getChatFocusKeybindingLabel(keybindingService, type, 'last')));
content.push(localize('workbench.action.chat.focusLastFocusedItem', 'To return to the last chat response you focused, invoke the Focus Last Focused Chat Response command{0}.', getChatFocusKeybindingLabel(keybindingService, type, 'lastFocused')));
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, 'input')));

View File

@@ -11,9 +11,11 @@ import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../../base/
import { Range } from '../../../../../../editor/common/core/range.js';
import { Location } from '../../../../../../editor/common/languages.js';
import { TestInstantiationService } from '../../../../../../platform/instantiation/test/common/instantiationServiceMock.js';
import { ChatResponseAccessibleView, getToolSpecificDataDescription, getResultDetailsDescription, getToolInvocationA11yDescription } from '../../../browser/accessibility/chatResponseAccessibleView.js';
import { IStorageService, StorageScope, StorageTarget } from '../../../../../../platform/storage/common/storage.js';
import { ChatResponseAccessibleView, CHAT_ACCESSIBLE_VIEW_INCLUDE_THINKING_STORAGE_KEY, getToolSpecificDataDescription, getResultDetailsDescription, getToolInvocationA11yDescription } from '../../../browser/accessibility/chatResponseAccessibleView.js';
import { IChatWidget, IChatWidgetService } from '../../../browser/chat.js';
import { IChatExtensionsContent, IChatPullRequestContent, IChatSubagentToolInvocationData, IChatTerminalToolInvocationData, IChatTodoListContent, IChatToolInputInvocationData, IChatToolResourcesInvocationData } from '../../../common/chatService/chatService.js';
import { TestStorageService } from '../../../../../test/common/workbenchTestServices.js';
suite('ChatResponseAccessibleView', () => {
const store = ensureNoDisposablesAreLeakedInTestSuite();
@@ -394,10 +396,57 @@ suite('ChatResponseAccessibleView', () => {
});
suite('getProvider', () => {
test('omits thinking content when disabled in storage', () => {
const instantiationService = store.add(new TestInstantiationService());
const storageService = store.add(new TestStorageService());
storageService.store(CHAT_ACCESSIBLE_VIEW_INCLUDE_THINKING_STORAGE_KEY, false, StorageScope.PROFILE, StorageTarget.USER);
const responseItem = {
response: { value: [{ kind: 'thinking', value: 'Hidden reasoning' }, { kind: 'markdownContent', content: new MarkdownString('Response content') }] },
model: { onDidChange: Event.None },
setVote: () => undefined
};
const items = [responseItem];
let focusedItem: unknown = responseItem;
const widget = {
hasInputFocus: () => false,
focusResponseItem: () => { focusedItem = responseItem; },
getFocus: () => focusedItem,
focus: (item: unknown) => { focusedItem = item; },
viewModel: { getItems: () => items }
} as unknown as IChatWidget;
const widgetService = {
_serviceBrand: undefined,
lastFocusedWidget: widget,
onDidAddWidget: Event.None,
onDidBackgroundSession: Event.None,
reveal: async () => true,
revealWidget: async () => widget,
getAllWidgets: () => [widget],
getWidgetByInputUri: () => widget,
openSession: async () => widget,
getWidgetBySessionResource: () => widget
} as unknown as IChatWidgetService;
instantiationService.stub(IChatWidgetService, widgetService);
instantiationService.stub(IStorageService, storageService);
const accessibleView = new ChatResponseAccessibleView();
const provider = instantiationService.invokeFunction(accessor => accessibleView.getProvider(accessor));
assert.ok(provider);
store.add(provider);
const content = provider.provideContent();
assert.ok(content.includes('Response content'));
assert.ok(!content.includes('Thinking: Hidden reasoning'));
});
test('prefers the latest response when focus is on a queued request', () => {
const instantiationService = store.add(new TestInstantiationService());
const storageService = store.add(new TestStorageService());
const responseItem = {
response: { value: [{ kind: 'markdownContent', content: new MarkdownString('Response content') }] },
response: { value: [{ kind: 'thinking', value: 'Reasoning' }, { kind: 'markdownContent', content: new MarkdownString('Response content') }] },
model: { onDidChange: Event.None },
setVote: () => undefined
};
@@ -427,12 +476,15 @@ suite('ChatResponseAccessibleView', () => {
} as unknown as IChatWidgetService;
instantiationService.stub(IChatWidgetService, widgetService);
instantiationService.stub(IStorageService, storageService);
const accessibleView = new ChatResponseAccessibleView();
const provider = instantiationService.invokeFunction(accessor => accessibleView.getProvider(accessor));
assert.ok(provider);
store.add(provider);
assert.ok(provider.provideContent().includes('Response content'));
const content = provider.provideContent();
assert.ok(content.includes('Response content'));
assert.ok(content.includes('Thinking: Reasoning'));
});
});
});