diff --git a/src/vs/workbench/contrib/chat/browser/accessibility/chatResponseAccessibleView.ts b/src/vs/workbench/contrib/chat/browser/accessibility/chatResponseAccessibleView.ts index 0cd00519f0e..6cad0431d35 100644 --- a/src/vs/workbench/contrib/chat/browser/accessibility/chatResponseAccessibleView.ts +++ b/src/vs/workbench/contrib/chat/browser/accessibility/chatResponseAccessibleView.ts @@ -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 | 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()); readonly onDidChangeContent: Event = 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) { diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityActions.ts index 7be04e1760c..99bad32ce49 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityActions.ts @@ -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 { + 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); } diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts index 3c23b729b2a..bfff1a5f030 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatAccessibilityHelp.ts @@ -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.', '')); + 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.', '')); + 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'))); diff --git a/src/vs/workbench/contrib/chat/test/browser/accessibility/chatResponseAccessibleView.test.ts b/src/vs/workbench/contrib/chat/test/browser/accessibility/chatResponseAccessibleView.test.ts index d756c702087..ba7821dd358 100644 --- a/src/vs/workbench/contrib/chat/test/browser/accessibility/chatResponseAccessibleView.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/accessibility/chatResponseAccessibleView.test.ts @@ -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')); }); }); });