Simplify chat input toolbar responsive behavior (#298467)

* Reduce chat input label hide threshold from 650 to 400

* Collapse chat input picker buttons to 22x22 icons at narrow widths

When the chat input is narrow (<250px), hide chevrons on mode, session
target, model, and workspace pickers. Mode and session target pickers
collapse to centered 22x22 icon-only buttons matching the add-context
button size. Update actionMinWidth to 22 and toolbar gap to 4px.

* Simplify chat input toolbar responsive behavior

* Apply initial hideChevrons state in render()
This commit is contained in:
David Dossett
2026-03-02 09:04:27 -08:00
committed by GitHub
parent 834fd36b6e
commit 0321be04fe
10 changed files with 120 additions and 28 deletions

View File

@@ -619,7 +619,7 @@ class NewChatWidget extends Disposable implements IHistoryNavigationWidget {
};
const pickerOptions: IChatInputPickerOptions = {
onlyShowIconsForDefaultActions: observableValue('onlyShowIcons', false),
hideChevrons: observableValue('hideChevrons', false),
hoverPosition: { hoverPosition: HoverPosition.ABOVE },
};

View File

@@ -35,7 +35,7 @@ import { URI } from '../../../../../../base/common/uri.js';
import { IEditorConstructionOptions } from '../../../../../../editor/browser/config/editorConfiguration.js';
import { EditorExtensionsRegistry } from '../../../../../../editor/browser/editorExtensions.js';
import { CodeEditorWidget } from '../../../../../../editor/browser/widget/codeEditor/codeEditorWidget.js';
import { EditorLayoutInfo, EditorOptions, IEditorOptions } from '../../../../../../editor/common/config/editorOptions.js';
import { EditorOptions, IEditorOptions } from '../../../../../../editor/common/config/editorOptions.js';
import { IDimension } from '../../../../../../editor/common/core/2d/dimension.js';
import { IPosition } from '../../../../../../editor/common/core/position.js';
import { IRange, Range } from '../../../../../../editor/common/core/range.js';
@@ -209,6 +209,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
private static _counter = 0;
private _workingSetCollapsed = observableValue('chatInputPart.workingSetCollapsed', true);
private _stableInputPartWidth = observableValue('chatInputPart.stableInputPartWidth', 0);
private readonly _chatInputTodoListWidget = this._register(new MutableDisposable<ChatTodoListWidget>());
private readonly _chatQuestionCarouselWidget = this._register(new MutableDisposable<ChatQuestionCarouselPart>());
private readonly _chatQuestionCarouselDisposables = this._register(new DisposableStore());
@@ -220,6 +221,11 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
private _onDidLoadInputState: Emitter<void> = this._register(new Emitter());
readonly onDidLoadInputState: Event<void> = this._onDidLoadInputState.event;
private readonly _toolbarRelayoutScheduler = this._register(new RunOnceScheduler(() => {
if (typeof this.cachedWidth === 'number') {
this.layout(this.cachedWidth);
}
}, 0));
private _onDidFocus = this._register(new Emitter<void>());
readonly onDidFocus: Event<void> = this._onDidFocus.event;
@@ -2148,10 +2154,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
const pickerOptions: IChatInputPickerOptions = {
getOverflowAnchor: () => this.inputActionsToolbar.getElement(),
actionContext: { widget },
onlyShowIconsForDefaultActions: observableFromEvent(
this._inputEditor.onDidLayoutChange,
(l?: EditorLayoutInfo) => (l?.width ?? this._inputEditor.getLayoutInfo().width) < 650 /* This is a magical number based on testing*/
).recomputeInitiallyAndOnChange(this._store),
hideChevrons: derived(reader => this._stableInputPartWidth.read(reader) < 400),
hoverPosition: {
forcePosition: true,
hoverPosition: location === ChatWidgetLocation.SidebarRight && !isMaximized ? HoverPosition.LEFT : HoverPosition.RIGHT
@@ -2169,7 +2172,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
enabled: true,
kind: 'last',
minItems: 1,
actionMinWidth: 40
actionMinWidth: 22
},
actionViewItemProvider: (action, options) => {
if (action.id === OpenModelPickerAction.ID && action instanceof MenuItemAction) {
@@ -2230,17 +2233,13 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
if (this.workspaceContextService.getWorkbenchState() === WorkbenchState.EMPTY && this.options.workspacePickerDelegate) {
return this.instantiationService.createInstance(WorkspacePickerActionItem, action, this.options.workspacePickerDelegate, pickerOptions);
} else {
const empty = new BaseActionViewItem(undefined, action);
if (empty.element) {
empty.element.style.display = 'none';
}
return empty;
return new HiddenActionViewItem(action);
}
} else if (action.id === ChatSessionPrimaryPickerAction.ID && action instanceof MenuItemAction) {
// Create all pickers and return a container action view item
const widgets = this.createChatSessionPickerWidgets(action);
if (widgets.length === 0) {
return undefined;
return new HiddenActionViewItem(action);
}
// Create a container to hold all picker widgets
return this.instantiationService.createInstance(ChatSessionPickersContainerActionItem, action, widgets);
@@ -2258,7 +2257,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
this.chatSessionPickerContainer = container as HTMLElement | undefined;
if (this.cachedWidth && typeof this.cachedInputToolbarWidth === 'number' && this.cachedInputToolbarWidth !== this.inputActionsToolbar.getItemsWidth()) {
this.layout(this.cachedWidth);
this._toolbarRelayoutScheduler.schedule();
}
}));
this.executeToolbar = this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, toolbarsContainer, this.options.menus.executeToolbar, {
@@ -2273,7 +2272,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
this.executeToolbar.context = { widget } satisfies IChatExecuteActionContext;
this._register(this.executeToolbar.onDidChangeMenuItems(() => {
if (this.cachedWidth && typeof this.cachedExecuteToolbarWidth === 'number' && this.cachedExecuteToolbarWidth !== this.executeToolbar.getItemsWidth()) {
this.layout(this.cachedWidth);
this._toolbarRelayoutScheduler.schedule();
}
}));
if (this.options.menus.inputSideToolbar) {
@@ -2994,6 +2993,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
*/
layout(width: number) {
this.cachedWidth = width;
this._stableInputPartWidth.set(width, undefined);
return this._layout(width);
}
@@ -3037,10 +3037,11 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
const inputSideToolbarWidth = this.inputSideToolbarContainer ? dom.getTotalWidth(this.inputSideToolbarContainer) : 0;
const getToolbarsWidthCompact = () => {
const toolbarItemGap = 4;
const executeToolbarWidth = this.cachedExecuteToolbarWidth = this.executeToolbar.getItemsWidth();
const inputToolbarWidth = this.cachedInputToolbarWidth = this.inputActionsToolbar.getItemsWidth();
const executeToolbarPadding = (this.executeToolbar.getItemsLength() - 1) * 4;
const inputToolbarPadding = this.inputActionsToolbar.getItemsLength() ? (this.inputActionsToolbar.getItemsLength() - 1) * 4 : 0;
const executeToolbarPadding = (this.executeToolbar.getItemsLength() - 1) * toolbarItemGap;
const inputToolbarPadding = this.inputActionsToolbar.getItemsLength() ? (this.inputActionsToolbar.getItemsLength() - 1) * toolbarItemGap : 0;
const contextUsageWidth = dom.getTotalWidth(this.contextUsageWidgetContainer);
const inputToolbarsPadding = 12; // pdading between input toolbar/execute toolbar/contextUsage.
return executeToolbarWidth + executeToolbarPadding + contextUsageWidth + (this.options.renderInputToolbarBelowInput ? 0 : inputToolbarWidth + inputToolbarPadding + inputToolbarsPadding);
@@ -3132,3 +3133,14 @@ class ChatSessionPickersContainerActionItem extends ActionViewItem {
super.dispose();
}
}
class HiddenActionViewItem extends BaseActionViewItem {
constructor(action: IAction) {
super(undefined, action);
}
override render(container: HTMLElement): void {
super.render(container);
container.style.display = 'none';
}
}

View File

@@ -24,7 +24,7 @@ export interface IChatInputPickerOptions {
readonly actionContext?: IChatExecuteActionContext;
readonly onlyShowIconsForDefaultActions: IObservable<boolean>;
readonly hideChevrons: IObservable<boolean>;
readonly hoverPosition?: IHoverPositionOptions;
}
@@ -53,8 +53,9 @@ export abstract class ChatInputPickerActionViewItem extends ActionWidgetDropdown
super(action, optionsWithAnchor, actionWidgetService, keybindingService, contextKeyService, telemetryService);
this._register(autorun(reader => {
this.pickerOptions.onlyShowIconsForDefaultActions.read(reader);
const hideChevrons = this.pickerOptions.hideChevrons.read(reader);
if (this.element) {
this.element.classList.toggle('hide-chevrons', hideChevrons);
this.renderLabel(this.element);
}
}));
@@ -74,5 +75,12 @@ export abstract class ChatInputPickerActionViewItem extends ActionWidgetDropdown
override render(container: HTMLElement): void {
super.render(container);
container.classList.add('chat-input-picker-item');
// Apply initial collapsed state now that this.element exists
const hideChevrons = this.pickerOptions.hideChevrons.get();
if (this.element) {
this.element.classList.toggle('hide-chevrons', hideChevrons);
this.renderLabel(this.element);
}
}
}

View File

@@ -12,6 +12,7 @@ import { Emitter, Event } from '../../../../../../base/common/event.js';
import { MarkdownString } from '../../../../../../base/common/htmlContent.js';
import { KeyCode } from '../../../../../../base/common/keyCodes.js';
import { Disposable } from '../../../../../../base/common/lifecycle.js';
import { autorun, IObservable } from '../../../../../../base/common/observable.js';
import { ThemeIcon } from '../../../../../../base/common/themables.js';
import { localize } from '../../../../../../nls.js';
import { ActionListItemKind, IActionListItem } from '../../../../../../platform/actionWidget/browser/actionList.js';
@@ -459,6 +460,7 @@ export class ModelPickerWidget extends Disposable {
private _selectedModel: ILanguageModelChatMetadataAndIdentifier | undefined;
private _badge: ModelPickerBadge | undefined;
private _hideChevrons: IObservable<boolean> | undefined;
private _domNode: HTMLElement | undefined;
private _badgeIcon: HTMLElement | undefined;
@@ -484,6 +486,17 @@ export class ModelPickerWidget extends Disposable {
super();
}
setHideChevrons(hideChevrons: IObservable<boolean>): void {
this._hideChevrons = hideChevrons;
this._register(autorun(reader => {
const hide = hideChevrons.read(reader);
if (this._domNode) {
this._domNode.classList.toggle('hide-chevrons', hide);
}
this._renderLabel();
}));
}
setSelectedModel(model: ILanguageModelChatMetadataAndIdentifier | undefined): void {
this._selectedModel = model;
this._renderLabel();
@@ -501,6 +514,11 @@ export class ModelPickerWidget extends Disposable {
this._domNode.setAttribute('aria-haspopup', 'true');
this._domNode.setAttribute('aria-expanded', 'false');
// Apply initial collapsed state now that _domNode exists
if (this._hideChevrons?.get()) {
this._domNode.classList.toggle('hide-chevrons', true);
}
this._badgeIcon = dom.append(this._domNode, dom.$('span.model-picker-badge'));
this._updateBadge();
@@ -637,7 +655,9 @@ export class ModelPickerWidget extends Disposable {
domChildren.push(this._badgeIcon);
}
domChildren.push(...renderLabelWithIcons(`$(chevron-down)`));
if (!this._hideChevrons?.get()) {
domChildren.push(...renderLabelWithIcons(`$(chevron-down)`));
}
dom.reset(this._domNode, ...domChildren);

View File

@@ -260,11 +260,15 @@ export class ModePickerActionItem extends ChatInputPickerActionViewItem {
return menuContributions;
}
override render(container: HTMLElement): void {
super.render(container);
container.classList.add('chat-mode-picker-item');
}
protected override renderLabel(element: HTMLElement): IDisposable | null {
this.setAriaLabelAttributes(element);
const currentMode = this.delegate.currentMode.get();
const isDefault = currentMode.id === ChatMode.Agent.id;
const state = currentMode.label.get();
let icon = currentMode.icon.get();
@@ -274,13 +278,16 @@ export class ModePickerActionItem extends ChatInputPickerActionViewItem {
}
const labelElements = [];
const collapsed = this.pickerOptions.hideChevrons.get();
if (icon) {
labelElements.push(...renderLabelWithIcons(`$(${icon.id})`));
}
if (!isDefault || !icon || !this.pickerOptions.onlyShowIconsForDefaultActions.get()) {
if (!collapsed || !icon) {
labelElements.push(dom.$('span.chat-input-picker-label', undefined, state));
}
labelElements.push(...renderLabelWithIcons(`$(chevron-down)`));
if (!collapsed) {
labelElements.push(...renderLabelWithIcons(`$(chevron-down)`));
}
dom.reset(element, ...labelElements);
return null;

View File

@@ -209,7 +209,9 @@ export class ModelPickerActionItem extends ChatInputPickerActionViewItem {
}
domChildren.push(dom.$('span.chat-input-picker-label', undefined, name ?? localize('chat.modelPicker.auto', "Auto")));
domChildren.push(...renderLabelWithIcons(`$(chevron-down)`));
if (!this.pickerOptions.hideChevrons.get()) {
domChildren.push(...renderLabelWithIcons(`$(chevron-down)`));
}
dom.reset(element, ...domChildren);
this.setAriaLabelAttributes(element);

View File

@@ -41,6 +41,7 @@ export class EnhancedModelPickerActionItem extends BaseActionViewItem {
this._pickerWidget = this._register(instantiationService.createInstance(ModelPickerWidget, delegate));
this._pickerWidget.setSelectedModel(delegate.currentModel.get());
this._pickerWidget.setHideChevrons(pickerOptions.hideChevrons);
// Sync delegate → widget when model list or selection changes externally
this._register(autorun(t => {

View File

@@ -200,6 +200,11 @@ export class SessionTypePickerActionItem extends ChatInputPickerActionViewItem {
return undefined;
}
override render(container: HTMLElement): void {
super.render(container);
container.classList.add('chat-session-target-picker-item');
}
protected override renderLabel(element: HTMLElement): IDisposable | null {
this.setAriaLabelAttributes(element);
const currentType = this._getSelectedSessionType();
@@ -208,11 +213,12 @@ export class SessionTypePickerActionItem extends ChatInputPickerActionViewItem {
const icon = getAgentSessionProviderIcon(currentType ?? AgentSessionProviders.Local);
const labelElements = [];
const collapsed = this.pickerOptions.hideChevrons.get();
labelElements.push(...renderLabelWithIcons(`$(${icon.id})`));
if (!this.pickerOptions.onlyShowIconsForDefaultActions.get()) {
if (!collapsed) {
labelElements.push(dom.$('span.chat-input-picker-label', undefined, label));
labelElements.push(...renderLabelWithIcons(`$(chevron-down)`));
}
labelElements.push(...renderLabelWithIcons(`$(chevron-down)`));
dom.reset(element, ...labelElements);

View File

@@ -118,7 +118,9 @@ export class WorkspacePickerActionItem extends ChatInputPickerActionViewItem {
labelElements.push(dom.$('span.chat-input-picker-label', undefined, localize('selectWorkspace', "Workspace")));
}
labelElements.push(...renderLabelWithIcons(`$(chevron-down)`));
if (!this.pickerOptions.hideChevrons.get()) {
labelElements.push(...renderLabelWithIcons(`$(chevron-down)`));
}
dom.reset(element, ...labelElements);

View File

@@ -1325,6 +1325,10 @@ have to be updated for changes to the rules above, or to support more deeply nes
margin-right: auto;
}
.interactive-session .chat-input-toolbars > .chat-input-toolbar .actions-container:first-child {
margin-right: 0;
}
.interactive-session .chat-input-toolbars .tool-warning-indicator {
position: absolute;
bottom: 0;
@@ -1402,12 +1406,42 @@ have to be updated for changes to the rules above, or to support more deeply nes
.interactive-session .chat-input-toolbar .chat-input-picker-item .action-label,
.interactive-session .chat-input-toolbar .chat-sessionPicker-item .action-label {
height: 16px;
padding: 3px 0px 3px 6px;
padding: 3px 1px 3px 7px;
display: flex;
align-items: center;
color: var(--vscode-icon-foreground);
}
/* When chevrons are hidden and only showing an icon (no label), size to 22x22 with centered icon */
.interactive-session .chat-input-toolbar .chat-input-picker-item .action-label.hide-chevrons:not(:has(.chat-input-picker-label)),
.interactive-session .chat-input-toolbar .chat-input-picker-item.hide-chevrons .action-label:not(:has(.chat-input-picker-label)),
.interactive-session .chat-input-toolbar .chat-sessionPicker-item .action-label.hide-chevrons:not(:has(.chat-input-picker-label)) {
width: 22px;
min-width: 22px;
height: 22px;
padding: 0;
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
.codicon {
justify-content: center;
}
}
/* When chevrons are hidden but label is still shown (e.g. model picker), use equal padding */
.interactive-session .chat-input-toolbar .chat-input-picker-item .action-label.hide-chevrons:has(.chat-input-picker-label),
.interactive-session .chat-input-toolbar .chat-input-picker-item.hide-chevrons .action-label:has(.chat-input-picker-label),
.interactive-session .chat-input-toolbar .chat-sessionPicker-item .action-label.hide-chevrons:has(.chat-input-picker-label) {
padding: 3px 7px;
}
/* Hide the tools button when the toolbar is in collapsed state */
.interactive-session .chat-input-toolbar:has(.hide-chevrons) .action-item:has(.codicon-tools) {
display: none;
}
.monaco-workbench .interactive-session .chat-input-toolbar .chat-input-picker-item .action-label .codicon-chevron-down,
.monaco-workbench .interactive-session .chat-input-toolbar .chat-sessionPicker-item .action-label .codicon-chevron-down {
font-size: 12px;