Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,8 @@ export const enum AccessibilityVerbositySettingId {
Walkthrough = 'accessibility.verbosity.walkthrough',
SourceControl = 'accessibility.verbosity.sourceControl',
Find = 'accessibility.verbosity.find',
SessionsChat = 'accessibility.verbosity.sessionsChat'
SessionsChat = 'accessibility.verbosity.sessionsChat',
ChatQuestionCarousel = 'accessibility.verbosity.chatQuestionCarousel'
}

const baseVerbosityProperty: IConfigurationPropertySchema = {
Expand Down Expand Up @@ -210,6 +211,10 @@ const configuration: IConfigurationNode = {
description: localize('verbosity.sessionsChat', 'Provide information about how to access the Agents app accessibility help menu when the chat input is focused.'),
...baseVerbosityProperty
},
[AccessibilityVerbositySettingId.ChatQuestionCarousel]: {
description: localize('verbosity.chatQuestionCarousel', 'Provide information about how to navigate and interact with the chat question carousel, including how to focus the terminal when applicable.'),
...baseVerbosityProperty
},
'accessibility.signalOptions.volume': {
'description': localize('accessibility.signalOptions.volume', "The volume of the sounds in percent (0-100)."),
'type': 'number',
Expand Down
24 changes: 24 additions & 0 deletions src/vs/workbench/contrib/chat/browser/actions/chatActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1013,6 +1013,30 @@ export function registerChatActions() {
}
});

registerAction2(class FocusQuestionCarouselTerminalAction extends Action2 {
static readonly ID = 'workbench.action.chat.focusQuestionCarouselTerminal';

constructor() {
super({
id: FocusQuestionCarouselTerminalAction.ID,
title: localize2('interactiveSession.focusQuestionCarouselTerminal.label', "Chat: Focus Terminal from Question Carousel"),
category: CHAT_CATEGORY,
f1: true,
precondition: ContextKeyExpr.and(ChatContextKeys.inChatSession, ChatContextKeys.Editing.hasQuestionCarousel, ChatContextKeys.chatQuestionCarouselHasTerminal),
keybinding: [{
weight: KeybindingWeight.WorkbenchContrib,
primary: KeyMod.Alt | KeyCode.KeyT,
when: ContextKeyExpr.and(ChatContextKeys.inChatQuestionCarousel, ChatContextKeys.Editing.hasQuestionCarousel, ChatContextKeys.chatQuestionCarouselHasTerminal),
}]
});
}

run(accessor: ServicesAccessor): void {
const widgetService = accessor.get(IChatWidgetService);
widgetService.lastFocusedWidget?.focusQuestionCarouselTerminal();
}
});

registerAction2(class FocusTipAction extends Action2 {
static readonly ID = 'workbench.action.chat.focusTip';

Expand Down
5 changes: 5 additions & 0 deletions src/vs/workbench/contrib/chat/browser/chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,11 @@ export interface IChatWidget {
* @returns Whether the operation succeeded (i.e., a next question exists).
*/
navigateToNextQuestion(): boolean;
/**
* Focuses the terminal associated with the active question carousel.
* @returns Whether the operation succeeded (i.e., a terminal was found and focused).
*/
focusQuestionCarouselTerminal(): boolean;
/**
* Toggles focus between the tip widget and the chat input.
* Returns false if no tip is visible.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,10 @@ import { IHoverService } from '../../../../../../platform/hover/browser/hover.js
import { IContextKey, IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js';
import { IKeybindingService } from '../../../../../../platform/keybinding/common/keybinding.js';
import { ChatContextKeys } from '../../../common/actions/chatContextKeys.js';
import { AccessibilityVerbositySettingId } from '../../../../accessibility/browser/accessibilityConfiguration.js';
import { ScrollbarVisibility } from '../../../../../../base/common/scrollable.js';
import { ICommandService } from '../../../../../../platform/commands/common/commands.js';
import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js';
import { RunInTerminalTool } from '../../../../terminal/terminalContribChatExports.js';
import './media/chatQuestionCarousel.css';

Expand Down Expand Up @@ -87,6 +89,7 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent
*/
private readonly _interactiveUIStore: MutableDisposable<DisposableStore> = this._register(new MutableDisposable());
private readonly _inChatQuestionCarouselContextKey: IContextKey<boolean>;
private readonly _chatQuestionCarouselHasTerminalContextKey: IContextKey<boolean>;
private _validationMessageElement: HTMLElement | undefined;
private _currentValidationError: string | undefined;
private _focusTerminalButtonContainer: HTMLElement | undefined;
Expand All @@ -101,16 +104,24 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent
@IContextKeyService private readonly _contextKeyService: IContextKeyService,
@IKeybindingService private readonly _keybindingService: IKeybindingService,
@ICommandService private readonly _commandService: ICommandService,
@IConfigurationService private readonly _configurationService: IConfigurationService,
) {
super();

this.domNode = dom.$('.chat-question-carousel-container');
this.domNode.id = generateUuid();
this._inChatQuestionCarouselContextKey = ChatContextKeys.inChatQuestionCarousel.bindTo(this._contextKeyService);
this._chatQuestionCarouselHasTerminalContextKey = ChatContextKeys.chatQuestionCarouselHasTerminal.bindTo(this._contextKeyService);
const focusTracker = this._register(dom.trackFocus(this.domNode));
this._register(focusTracker.onDidFocus(() => this._inChatQuestionCarouselContextKey.set(true)));
this._register(focusTracker.onDidBlur(() => this._inChatQuestionCarouselContextKey.set(false)));
this._register({ dispose: () => this._inChatQuestionCarouselContextKey.reset() });
this._register(focusTracker.onDidFocus(() => {
this._inChatQuestionCarouselContextKey.set(true);
this._chatQuestionCarouselHasTerminalContextKey.set(!!this.carousel.terminalId);
}));
this._register(focusTracker.onDidBlur(() => {
this._inChatQuestionCarouselContextKey.set(false);
this._chatQuestionCarouselHasTerminalContextKey.reset();
}));
this._register({ dispose: () => { this._inChatQuestionCarouselContextKey.reset(); this._chatQuestionCarouselHasTerminalContextKey.reset(); } });

// Set up accessibility attributes for the carousel container
this.domNode.tabIndex = 0;
Expand Down Expand Up @@ -183,10 +194,14 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent
if (carousel.terminalId) {
this._focusTerminalButtonContainer = dom.$('.chat-question-focus-terminal-container');
const focusTerminalTitle = localize('chat.questionCarousel.focusTerminalTitle', 'Focus Terminal');
const kbLabel = this._keybindingService.lookupKeybinding('workbench.action.chat.focusQuestionCarouselTerminal')?.getLabel();
const focusTerminalAriaLabel = kbLabel
? localize('chat.questionCarousel.focusTerminalAriaLabel', 'Focus Terminal ({0})', kbLabel)
: focusTerminalTitle;
const focusTerminalButton = interactiveStore.add(new Button(this._focusTerminalButtonContainer, { ...defaultButtonStyles, secondary: true, supportIcons: true }));
focusTerminalButton.label = `$(${Codicon.terminal.id})`;
focusTerminalButton.element.classList.add('chat-question-focus-terminal');
focusTerminalButton.element.setAttribute('aria-label', focusTerminalTitle);
focusTerminalButton.element.setAttribute('aria-label', focusTerminalAriaLabel);
interactiveStore.add(this._hoverService.setupDelayedHover(focusTerminalButton.element, { content: focusTerminalTitle }));
interactiveStore.add(focusTerminalButton.onDidClick(() => this._focusTerminal()));

Expand Down Expand Up @@ -606,11 +621,24 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent
const messageContent = this.getQuestionText(questionText);
const questionCount = this.carousel.questions.length;

let label: string;
if (questionCount === 1) {
this.domNode.setAttribute('aria-label', localize('chat.questionCarousel.singleQuestionLabel', 'Chat question: {0}', messageContent));
label = localize('chat.questionCarousel.singleQuestionLabel', 'Chat question: {0}', messageContent);
} else {
this.domNode.setAttribute('aria-label', localize('chat.questionCarousel.multiQuestionLabel', 'Chat question {0} of {1}: {2}', this._currentIndex + 1, questionCount, messageContent));
label = localize('chat.questionCarousel.multiQuestionLabel', 'Chat question {0} of {1}: {2}', this._currentIndex + 1, questionCount, messageContent);
}

const verbose = this._configurationService.getValue<boolean>(AccessibilityVerbositySettingId.ChatQuestionCarousel);
if (verbose && this.carousel.terminalId) {
const kbLabel = this._keybindingService.lookupKeybinding('workbench.action.chat.focusQuestionCarouselTerminal')?.getLabel();
if (kbLabel) {
label = localize('chat.questionCarousel.combinedFocusTerminalHint', "{0} Use {1} to focus the terminal.", label, kbLabel);
} else {
label = localize('chat.questionCarousel.combinedFocusTerminalHintNoKb', "{0} Use the Focus Terminal from Question Carousel command to focus the terminal.", label);
}
}

this.domNode.setAttribute('aria-label', label);
}

/**
Expand Down Expand Up @@ -645,6 +673,14 @@ export class ChatQuestionCarouselPart extends Disposable implements IChatContent
return true;
}

public focusTerminal(): boolean {
if (!this.carousel.terminalId) {
return false;
}
this._focusTerminal();
return true;
}

private renderCurrentQuestion(focusContainerForScreenReader: boolean = false): void {
if (!this._questionContainer) {
return;
Expand Down
4 changes: 4 additions & 0 deletions src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -873,6 +873,10 @@ export class ChatWidget extends Disposable implements IChatWidget {
return this.input.navigateToNextQuestion();
}

focusQuestionCarouselTerminal(): boolean {
return this.input.focusQuestionCarouselTerminal();
}

toggleTipFocus(): boolean {
if (this._gettingStartedTipPartRef?.hasFocus()) {
this.focusInput();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2993,6 +2993,11 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
return carousel?.navigateToNextQuestion() ?? false;
}

focusQuestionCarouselTerminal(): boolean {
const carousel = this.questionCarousel;
return carousel?.focusTerminal() ?? false;
}

// --- Plan Review ---

renderPlanReview(review: IChatPlanReview, context: IChatContentPartRenderContext, options: IChatPlanReviewPartOptions): ChatPlanReviewPart {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export namespace ChatContextKeys {
export const inChatInput = new RawContextKey<boolean>('inChatInput', false, { type: 'boolean', description: localize('inInteractiveInput', "True when focus is in the chat input, false otherwise.") });
export const inChatSession = new RawContextKey<boolean>('inChat', false, { type: 'boolean', description: localize('inChat', "True when focus is in the chat widget, false otherwise.") });
export const inChatQuestionCarousel = new RawContextKey<boolean>('inChatQuestionCarousel', false, { type: 'boolean', description: localize('inChatQuestionCarousel', "True when focus is in the chat question carousel.") });
export const chatQuestionCarouselHasTerminal = new RawContextKey<boolean>('chatQuestionCarouselHasTerminal', false, { type: 'boolean', description: localize('chatQuestionCarouselHasTerminal', "True when the chat question carousel was triggered by a terminal and has a terminal to focus.") });
export const inChatEditor = new RawContextKey<boolean>('inChatEditor', false, { type: 'boolean', description: localize('inChatEditor', "Whether focus is in a chat editor.") });
export const inChatTodoList = new RawContextKey<boolean>('inChatTodoList', false, { type: 'boolean', description: localize('inChatTodoList', "True when focus is in the chat todo list.") });
export const inChatTip = new RawContextKey<boolean>('inChatTip', false, { type: 'boolean', description: localize('inChatTip', "True when focus is in a chat tip.") });
Expand Down
Loading