diff --git a/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts b/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts index 9928b50f616112..247501dfc64e9c 100644 --- a/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts +++ b/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts @@ -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 }, }; diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index d2e482f504a0f4..f3b328a06a39fd 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -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()); private readonly _chatQuestionCarouselWidget = this._register(new MutableDisposable()); private readonly _chatQuestionCarouselDisposables = this._register(new DisposableStore()); @@ -220,6 +221,11 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private _onDidLoadInputState: Emitter = this._register(new Emitter()); readonly onDidLoadInputState: Event = 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()); readonly onDidFocus: Event = this._onDidFocus.event; @@ -2152,10 +2158,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 @@ -2173,7 +2176,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) { @@ -2234,17 +2237,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); @@ -2262,7 +2261,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, { @@ -2277,7 +2276,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) { @@ -2998,6 +2997,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge */ layout(width: number) { this.cachedWidth = width; + this._stableInputPartWidth.set(width, undefined); return this._layout(width); } @@ -3041,10 +3041,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); @@ -3136,3 +3137,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'; + } +} diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPickerActionItem.ts index 1377aa5260793d..13682b907d93c1 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPickerActionItem.ts @@ -24,7 +24,7 @@ export interface IChatInputPickerOptions { readonly actionContext?: IChatExecuteActionContext; - readonly onlyShowIconsForDefaultActions: IObservable; + readonly hideChevrons: IObservable; 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); + } } } diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts index e5c907aed38d14..0257f20252dd0d 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatModelPicker.ts @@ -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 | undefined; private _domNode: HTMLElement | undefined; private _badgeIcon: HTMLElement | undefined; @@ -484,6 +486,17 @@ export class ModelPickerWidget extends Disposable { super(); } + setHideChevrons(hideChevrons: IObservable): 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); diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts index 3b552379ad1e86..773f896f6e846b 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/modePickerActionItem.ts @@ -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; diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/modelPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/modelPickerActionItem.ts index af61812b3a91ff..04bfc652890fc1 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/modelPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/modelPickerActionItem.ts @@ -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); diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/modelPickerActionItem2.ts b/src/vs/workbench/contrib/chat/browser/widget/input/modelPickerActionItem2.ts index 6d302d2e00366f..5a4d402e790742 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/modelPickerActionItem2.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/modelPickerActionItem2.ts @@ -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 => { diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts index 5a31e7f187cb8c..897c927f25fd86 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/sessionTargetPickerActionItem.ts @@ -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); diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/workspacePickerActionItem.ts b/src/vs/workbench/contrib/chat/browser/widget/input/workspacePickerActionItem.ts index 989c3bc64e8edf..828664f9a8853a 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/workspacePickerActionItem.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/workspacePickerActionItem.ts @@ -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); diff --git a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css index c3ec962151a34f..f1b00e2dfb71ea 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css @@ -1317,6 +1317,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; @@ -1394,12 +1398,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;