mirror of
https://github.com/microsoft/vscode.git
synced 2026-07-11 05:30:03 -05:00
Add toggle for thinking content in chat accessible view (#295017)
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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')));
|
||||
|
||||
@@ -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'));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user