Merge pull request #288120 from microsoft/benibenj/meaningful-junglefowl

Improved chat input pickers
This commit is contained in:
Benjamin Christopher Simmonds
2026-01-15 18:39:23 +01:00
committed by GitHub
16 changed files with 218 additions and 88 deletions

View File

@@ -993,6 +993,7 @@
"--vscode-chat-font-size-body-xl",
"--vscode-chat-font-size-body-xs",
"--vscode-chat-font-size-body-xxl",
"--vscode-toolbar-action-min-width",
"--comment-thread-editor-font-family",
"--comment-thread-editor-font-weight",
"--comment-thread-state-color",

View File

@@ -12,9 +12,21 @@
padding: 0;
}
.monaco-toolbar.responsive {
.monaco-toolbar.responsive.responsive-all {
.monaco-action-bar > .actions-container > .action-item {
flex-shrink: 1;
min-width: 20px;
min-width: var(--vscode-toolbar-action-min-width, 20px);
}
}
.monaco-toolbar.responsive.responsive-last {
.monaco-action-bar > .actions-container > .action-item {
flex-shrink: 0;
}
.monaco-action-bar:not(.has-overflow) > .actions-container > .action-item:last-child,
.monaco-action-bar.has-overflow > .actions-container > .action-item:nth-last-child(2) {
flex-shrink: 1;
min-width: var(--vscode-toolbar-action-min-width, 20px);
}
}

View File

@@ -18,7 +18,10 @@ import * as nls from '../../../../nls.js';
import { IHoverDelegate } from '../hover/hoverDelegate.js';
import { createInstantHoverDelegate } from '../hover/hoverDelegateFactory.js';
const ACTION_MIN_WIDTH = 24; /* 20px codicon + 4px left padding*/
const ACTION_MIN_WIDTH = 20; /* 20px codicon */
const ACTION_PADDING = 4; /* 4px padding */
const ACTION_MIN_WIDTH_VAR = '--vscode-toolbar-action-min-width';
export interface IToolBarOptions {
orientation?: ActionsOrientation;
@@ -53,9 +56,11 @@ export interface IToolBarOptions {
/**
* Controls the responsive behavior of the primary group of the toolbar.
* - `enabled`: Whether the responsive behavior is enabled.
* - `kind`: The kind of responsive behavior to apply. Can be either `last` to only shrink the last item, or `all` to shrink all items equally.
* - `minItems`: The minimum number of items that should always be visible.
* - `actionMinWidth`: The minimum width of each action item. Defaults to `ACTION_MIN_WIDTH` (24px).
*/
responsiveBehavior?: { enabled: boolean; minItems?: number };
responsiveBehavior?: { enabled: boolean; kind: 'last' | 'all'; minItems?: number; actionMinWidth?: number };
}
/**
@@ -76,6 +81,7 @@ export class ToolBar extends Disposable {
private originalSecondaryActions: ReadonlyArray<IAction> = [];
private hiddenActions: { action: IAction; size: number }[] = [];
private readonly disposables = this._register(new DisposableStore());
private readonly actionMinWidth: number;
constructor(private readonly container: HTMLElement, contextMenuProvider: IContextMenuProvider, options: IToolBarOptions = { orientation: ActionsOrientation.HORIZONTAL }) {
super();
@@ -155,9 +161,15 @@ export class ToolBar extends Disposable {
}
}));
// Store effective action min width
this.actionMinWidth = (options.responsiveBehavior?.actionMinWidth ?? ACTION_MIN_WIDTH) + ACTION_PADDING;
// Responsive support
if (this.options.responsiveBehavior?.enabled) {
this.element.classList.add('responsive');
this.element.classList.toggle('responsive', true);
this.element.classList.toggle('responsive-all', this.options.responsiveBehavior.kind === 'all');
this.element.classList.toggle('responsive-last', this.options.responsiveBehavior.kind === 'last');
this.element.style.setProperty(ACTION_MIN_WIDTH_VAR, `${this.actionMinWidth - ACTION_PADDING}px`);
const observer = new ResizeObserver(() => {
this.updateActions(this.element.getBoundingClientRect().width);
@@ -239,27 +251,30 @@ export class ToolBar extends Disposable {
this.actionBar.push(action, { icon: this.options.icon ?? true, label: this.options.label ?? false, keybinding: this.getKeybindingLabel(action) });
});
this.actionBar.domNode.classList.toggle('has-overflow', this.actionBar.hasAction(this.toggleMenuAction));
if (this.options.responsiveBehavior?.enabled) {
// Reset hidden actions
this.hiddenActions.length = 0;
// Set the minimum width
if (this.options.responsiveBehavior?.minItems !== undefined) {
let itemCount = this.options.responsiveBehavior.minItems;
const itemCount = this.options.responsiveBehavior.minItems;
// Account for overflow menu
let overflowWidth = 0;
if (
this.originalSecondaryActions.length > 0 ||
itemCount < this.originalPrimaryActions.length
) {
itemCount += 1;
overflowWidth = ACTION_MIN_WIDTH + ACTION_PADDING;
}
this.container.style.minWidth = `${itemCount * ACTION_MIN_WIDTH}px`;
this.element.style.minWidth = `${itemCount * ACTION_MIN_WIDTH}px`;
this.container.style.minWidth = `${itemCount * this.actionMinWidth + overflowWidth}px`;
this.element.style.minWidth = `${itemCount * this.actionMinWidth + overflowWidth}px`;
} else {
this.container.style.minWidth = `${ACTION_MIN_WIDTH}px`;
this.element.style.minWidth = `${ACTION_MIN_WIDTH}px`;
this.container.style.minWidth = `${ACTION_MIN_WIDTH + ACTION_PADDING}px`;
this.element.style.minWidth = `${ACTION_MIN_WIDTH + ACTION_PADDING}px`;
}
// Update toolbar actions to fit with container width
@@ -290,14 +305,33 @@ export class ToolBar extends Disposable {
// Each action is assumed to have a minimum width so that actions with a label
// can shrink to the action's minimum width. We do this so that action visibility
// takes precedence over the action label.
const actionBarWidth = () => this.actionBar.length() * ACTION_MIN_WIDTH;
const actionBarWidth = (actualWidth: boolean) => {
if (this.options.responsiveBehavior?.kind === 'last') {
const hasToggleMenuAction = this.actionBar.hasAction(this.toggleMenuAction);
const primaryActionsCount = hasToggleMenuAction
? this.actionBar.length() - 1
: this.actionBar.length();
let itemsWidth = 0;
for (let i = 0; i < primaryActionsCount - 1; i++) {
itemsWidth += this.actionBar.getWidth(i) + ACTION_PADDING;
}
itemsWidth += actualWidth ? this.actionBar.getWidth(primaryActionsCount - 1) : this.actionMinWidth; // item to shrink
itemsWidth += hasToggleMenuAction ? ACTION_MIN_WIDTH + ACTION_PADDING : 0; // toggle menu action
return itemsWidth;
} else {
return this.actionBar.length() * this.actionMinWidth;
}
};
// Action bar fits and there are no hidden actions to show
if (actionBarWidth() <= containerWidth && this.hiddenActions.length === 0) {
if (actionBarWidth(false) <= containerWidth && this.hiddenActions.length === 0) {
return;
}
if (actionBarWidth() > containerWidth) {
if (actionBarWidth(false) > containerWidth) {
// Check for max items limit
if (this.options.responsiveBehavior?.minItems !== undefined) {
const primaryActionsCount = this.actionBar.hasAction(this.toggleMenuAction)
@@ -310,14 +344,14 @@ export class ToolBar extends Disposable {
}
// Hide actions from the right
while (actionBarWidth() > containerWidth && this.actionBar.length() > 0) {
while (actionBarWidth(true) > containerWidth && this.actionBar.length() > 0) {
const index = this.originalPrimaryActions.length - this.hiddenActions.length - 1;
if (index < 0) {
break;
}
// Store the action and its size
const size = Math.min(ACTION_MIN_WIDTH, this.getItemWidth(index));
const size = Math.min(this.actionMinWidth, this.getItemWidth(index));
const action = this.originalPrimaryActions[index];
this.hiddenActions.unshift({ action, size });
@@ -339,7 +373,7 @@ export class ToolBar extends Disposable {
// Show actions from the top of the toggle menu
while (this.hiddenActions.length > 0) {
const entry = this.hiddenActions.shift()!;
if (actionBarWidth() + entry.size > containerWidth) {
if (actionBarWidth(true) + entry.size > containerWidth) {
// Not enough space to show the action
this.hiddenActions.unshift(entry);
break;
@@ -355,7 +389,7 @@ export class ToolBar extends Disposable {
// There are no secondary actions, and there is only one hidden item left so we
// remove the overflow menu making space for the last hidden action to be shown.
if (this.originalSecondaryActions.length === 0 && this.hiddenActions.length === 1) {
if (this.originalSecondaryActions.length === 0 && this.hiddenActions.length === 0) {
this.toggleMenuAction.menuActions = [];
this.actionBar.pull(this.actionBar.length() - 1);
}
@@ -368,6 +402,8 @@ export class ToolBar extends Disposable {
const secondaryActions = this.originalSecondaryActions.slice(0);
this.toggleMenuAction.menuActions = Separator.join(hiddenActions, secondaryActions);
}
this.actionBar.domNode.classList.toggle('has-overflow', this.actionBar.hasAction(this.toggleMenuAction));
}
private clear(): void {

View File

@@ -33,6 +33,9 @@ export interface IActionWidgetDropdownOptions extends IBaseDropdownOptions {
readonly actionBarActions?: IAction[];
readonly actionBarActionProvider?: IActionProvider;
readonly showItemKeybindings?: boolean;
// Function that returns the anchor element for the dropdown
getAnchor?: () => HTMLElement;
}
/**
@@ -169,7 +172,7 @@ export class ActionWidgetDropdown extends BaseDropdown {
false,
actionWidgetItems,
actionWidgetDelegate,
this.element,
this._options.getAnchor?.() ?? this.element,
undefined,
actionBarActions,
accessibilityProvider

View File

@@ -15,6 +15,7 @@ export enum AgentSessionProviders {
Local = localChatSessionType,
Background = 'copilotcli',
Cloud = 'copilot-cloud-agent',
ClaudeCode = 'claude-code',
}
export function getAgentSessionProvider(sessionResource: URI | string): AgentSessionProviders | undefined {
@@ -23,6 +24,7 @@ export function getAgentSessionProvider(sessionResource: URI | string): AgentSes
case AgentSessionProviders.Local:
case AgentSessionProviders.Background:
case AgentSessionProviders.Cloud:
case AgentSessionProviders.ClaudeCode:
return type;
default:
return undefined;
@@ -37,6 +39,8 @@ export function getAgentSessionProviderName(provider: AgentSessionProviders): st
return localize('chat.session.providerLabel.background', "Background");
case AgentSessionProviders.Cloud:
return localize('chat.session.providerLabel.cloud', "Cloud");
case AgentSessionProviders.ClaudeCode:
return localize('chat.session.providerLabel.claude', "Claude");
}
}
@@ -48,6 +52,8 @@ export function getAgentSessionProviderIcon(provider: AgentSessionProviders): Th
return Codicon.worktree;
case AgentSessionProviders.Cloud:
return Codicon.cloud;
case AgentSessionProviders.ClaudeCode:
return Codicon.code;
}
}

View File

@@ -109,11 +109,10 @@ export class AgentSessionsFilter extends Disposable implements Required<IAgentSe
}
private registerProviderActions(disposables: DisposableStore): void {
const providers: { id: string; label: string }[] = [
{ id: AgentSessionProviders.Local, label: getAgentSessionProviderName(AgentSessionProviders.Local) },
{ id: AgentSessionProviders.Background, label: getAgentSessionProviderName(AgentSessionProviders.Background) },
{ id: AgentSessionProviders.Cloud, label: getAgentSessionProviderName(AgentSessionProviders.Cloud) },
];
const providers: { id: string; label: string }[] = Object.values(AgentSessionProviders).map(provider => ({
id: provider,
label: getAgentSessionProviderName(provider)
}));
for (const provider of this.chatSessionsService.getAllChatSessionContributions()) {
if (providers.find(p => p.id === provider.type)) {

View File

@@ -18,7 +18,7 @@ import { ILogService } from '../../../../../platform/log/common/log.js';
import { IStorageService, StorageScope, StorageTarget } from '../../../../../platform/storage/common/storage.js';
import { ILifecycleService } from '../../../../services/lifecycle/common/lifecycle.js';
import { ChatSessionStatus as AgentSessionStatus, IChatSessionFileChange, IChatSessionItem, IChatSessionsExtensionPoint, IChatSessionsService, isSessionInProgressStatus } from '../../common/chatSessionsService.js';
import { AgentSessionProviders, getAgentSessionProviderIcon, getAgentSessionProviderName } from './agentSessions.js';
import { AgentSessionProviders, getAgentSessionProvider, getAgentSessionProviderIcon, getAgentSessionProviderName } from './agentSessions.js';
//#region Interfaces, Types
@@ -305,23 +305,13 @@ export class AgentSessionsModel extends Disposable implements IAgentSessionsMode
// Icon + Label
let icon: ThemeIcon;
let providerLabel: string;
switch ((provider.chatSessionType)) {
case AgentSessionProviders.Local:
providerLabel = getAgentSessionProviderName(AgentSessionProviders.Local);
icon = getAgentSessionProviderIcon(AgentSessionProviders.Local);
break;
case AgentSessionProviders.Background:
providerLabel = getAgentSessionProviderName(AgentSessionProviders.Background);
icon = getAgentSessionProviderIcon(AgentSessionProviders.Background);
break;
case AgentSessionProviders.Cloud:
providerLabel = getAgentSessionProviderName(AgentSessionProviders.Cloud);
icon = getAgentSessionProviderIcon(AgentSessionProviders.Cloud);
break;
default: {
providerLabel = mapSessionContributionToType.get(provider.chatSessionType)?.name ?? provider.chatSessionType;
icon = session.iconPath ?? Codicon.terminal;
}
const agentSessionProvider = getAgentSessionProvider(provider.chatSessionType);
if (agentSessionProvider !== undefined) {
providerLabel = getAgentSessionProviderName(agentSessionProvider);
icon = getAgentSessionProviderIcon(agentSessionProvider);
} else {
providerLabel = mapSessionContributionToType.get(provider.chatSessionType)?.name ?? provider.chatSessionType;
icon = session.iconPath ?? Codicon.terminal;
}
// State + Timings

View File

@@ -30,17 +30,8 @@ import { IChatEditingService, ModifiedFileEntryState } from '../../common/editin
/**
* Provider types that support agent session projection mode.
* Only sessions from these providers will trigger focus view.
*
* Configuration:
* - AgentSessionProviders.Local: Local chat sessions (enabled)
* - AgentSessionProviders.Background: Background CLI agents (enabled)
* - AgentSessionProviders.Cloud: Cloud agents (enabled)
*/
const AGENT_SESSION_PROJECTION_ENABLED_PROVIDERS: Set<string> = new Set([
AgentSessionProviders.Local,
AgentSessionProviders.Background,
AgentSessionProviders.Cloud,
]);
const AGENT_SESSION_PROJECTION_ENABLED_PROVIDERS: Set<string> = new Set(Object.values(AgentSessionProviders));
//#endregion

View File

@@ -115,6 +115,7 @@ import { resizeImage } from '../../chatImageUtils.js';
import { IModelPickerDelegate, ModelPickerActionItem } from './modelPickerActionItem.js';
import { IModePickerDelegate, ModePickerActionItem } from './modePickerActionItem.js';
import { ISessionTypePickerDelegate, SessionTypePickerActionItem } from './sessionTargetPickerActionItem.js';
import { IChatInputPickerOptions } from './chatInputPickerActionItem.js';
import { getAgentSessionProvider } from '../../agentSessions/agentSessions.js';
import { SearchableOptionPickerActionItem } from '../../chatSessions/searchableOptionPickerActionItem.js';
import { mixin } from '../../../../../../base/common/objects.js';
@@ -1754,6 +1755,9 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
}));
const hoverDelegate = this._register(createInstantHoverDelegate());
const pickerOptions: IChatInputPickerOptions = {
getOverflowAnchor: () => this.inputActionsToolbar.getElement(),
};
this._register(dom.addStandardDisposableListener(toolbarsContainer, dom.EventType.CLICK, e => this.inputEditor.focus()));
this._register(dom.addStandardDisposableListener(this.attachmentsContainer, dom.EventType.CLICK, e => this.inputEditor.focus()));
@@ -1762,6 +1766,12 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
menuOptions: { shouldForwardArgs: true },
hiddenItemStrategy: HiddenItemStrategy.NoHide,
hoverDelegate,
responsiveBehavior: {
enabled: true,
kind: 'last',
minItems: 1,
actionMinWidth: 40
},
actionViewItemProvider: (action, options) => {
if (action.id === OpenModelPickerAction.ID && action instanceof MenuItemAction) {
if (!this._currentLanguageModel) {
@@ -1778,13 +1788,13 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
},
getModels: () => this.getModels()
};
return this.modelWidget = this.instantiationService.createInstance(ModelPickerActionItem, action, this._currentLanguageModel, undefined, itemDelegate);
return this.modelWidget = this.instantiationService.createInstance(ModelPickerActionItem, action, this._currentLanguageModel, undefined, itemDelegate, pickerOptions);
} else if (action.id === OpenModePickerAction.ID && action instanceof MenuItemAction) {
const delegate: IModePickerDelegate = {
currentMode: this._currentModeObservable,
sessionResource: () => this._widget?.viewModel?.sessionResource,
};
return this.modeWidget = this.instantiationService.createInstance(ModePickerActionItem, action, delegate);
return this.modeWidget = this.instantiationService.createInstance(ModePickerActionItem, action, delegate, pickerOptions);
} else if (action.id === OpenSessionTargetPickerAction.ID && action instanceof MenuItemAction) {
const delegate: ISessionTypePickerDelegate = {
getActiveSessionProvider: () => {
@@ -1793,7 +1803,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge
},
};
const chatSessionPosition = isIChatResourceViewContext(widget.viewContext) ? 'editor' : 'sidebar';
return this.sessionTargetWidget = this.instantiationService.createInstance(SessionTypePickerActionItem, action, chatSessionPosition, delegate);
return this.sessionTargetWidget = this.instantiationService.createInstance(SessionTypePickerActionItem, action, chatSessionPosition, delegate, pickerOptions);
} else if (action.id === ChatSessionPrimaryPickerAction.ID && action instanceof MenuItemAction) {
// Create all pickers and return a container action view item
const widgets = this.createChatSessionPickerWidgets(action);

View File

@@ -0,0 +1,60 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { getActiveWindow } from '../../../../../../base/browser/dom.js';
import { IAction } from '../../../../../../base/common/actions.js';
import { ActionWidgetDropdownActionViewItem } from '../../../../../../platform/actions/browser/actionWidgetDropdownActionViewItem.js';
import { IActionWidgetService } from '../../../../../../platform/actionWidget/browser/actionWidget.js';
import { IActionWidgetDropdownOptions } from '../../../../../../platform/actionWidget/browser/actionWidgetDropdown.js';
import { IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js';
import { IKeybindingService } from '../../../../../../platform/keybinding/common/keybinding.js';
export interface IChatInputPickerOptions {
/**
* Provides a fallback anchor element when the picker's own element
* is not available in the DOM (e.g., when inside an overflow menu).
*/
readonly getOverflowAnchor?: () => HTMLElement | undefined;
}
/**
* Base class for chat input picker action items (model picker, mode picker, session target picker).
* Provides common anchor resolution logic for dropdown positioning.
*/
export abstract class ChatInputPickerActionViewItem extends ActionWidgetDropdownActionViewItem {
constructor(
action: IAction,
actionWidgetOptions: Omit<IActionWidgetDropdownOptions, 'label' | 'labelRenderer'>,
private readonly pickerOptions: IChatInputPickerOptions,
@IActionWidgetService actionWidgetService: IActionWidgetService,
@IKeybindingService keybindingService: IKeybindingService,
@IContextKeyService contextKeyService: IContextKeyService,
) {
// Inject the anchor getter into the options
const optionsWithAnchor: Omit<IActionWidgetDropdownOptions, 'label' | 'labelRenderer'> = {
...actionWidgetOptions,
getAnchor: () => this.getAnchorElement(),
};
super(action, optionsWithAnchor, actionWidgetService, keybindingService, contextKeyService);
}
/**
* Returns the anchor element for the dropdown.
* Falls back to the overflow anchor if this element is not in the DOM.
*/
protected getAnchorElement(): HTMLElement {
if (this.element && getActiveWindow().document.contains(this.element)) {
return this.element;
}
return this.pickerOptions.getOverflowAnchor?.() ?? this.element!;
}
override render(container: HTMLElement): void {
super.render(container);
container.classList.add('chat-input-picker-item');
}
}

View File

@@ -14,7 +14,6 @@ import { autorun, IObservable } from '../../../../../../base/common/observable.j
import { ThemeIcon } from '../../../../../../base/common/themables.js';
import { URI } from '../../../../../../base/common/uri.js';
import { localize } from '../../../../../../nls.js';
import { ActionWidgetDropdownActionViewItem } from '../../../../../../platform/actions/browser/actionWidgetDropdownActionViewItem.js';
import { getFlatActionBarActions } from '../../../../../../platform/actions/browser/menuEntryActionViewItem.js';
import { IMenuService, MenuId, MenuItemAction } from '../../../../../../platform/actions/common/actions.js';
import { IActionWidgetService } from '../../../../../../platform/actionWidget/browser/actionWidget.js';
@@ -30,16 +29,18 @@ import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../../com
import { ExtensionAgentSourceType, PromptsStorage } from '../../../common/promptSyntax/service/promptsService.js';
import { getOpenChatActionIdForMode } from '../../actions/chatActions.js';
import { IToggleChatModeArgs, ToggleAgentModeActionId } from '../../actions/chatExecuteActions.js';
import { ChatInputPickerActionViewItem, IChatInputPickerOptions } from './chatInputPickerActionItem.js';
export interface IModePickerDelegate {
readonly currentMode: IObservable<IChatMode>;
readonly sessionResource: () => URI | undefined;
}
export class ModePickerActionItem extends ActionWidgetDropdownActionViewItem {
export class ModePickerActionItem extends ChatInputPickerActionViewItem {
constructor(
action: MenuItemAction,
private readonly delegate: IModePickerDelegate,
pickerOptions: IChatInputPickerOptions,
@IActionWidgetService actionWidgetService: IActionWidgetService,
@IChatAgentService chatAgentService: IChatAgentService,
@IKeybindingService keybindingService: IKeybindingService,
@@ -68,7 +69,7 @@ export class ModePickerActionItem extends ActionWidgetDropdownActionViewItem {
...action,
id: getOpenChatActionIdForMode(mode),
label: mode.label.get(),
icon: isDisabledViaPolicy ? ThemeIcon.fromId(Codicon.lock.id) : undefined,
icon: isDisabledViaPolicy ? ThemeIcon.fromId(Codicon.lock.id) : mode.icon.get(),
class: isDisabledViaPolicy ? 'disabled-by-policy' : undefined,
enabled: !isDisabledViaPolicy,
checked: !isDisabledViaPolicy && currentMode.id === mode.id,
@@ -94,6 +95,7 @@ export class ModePickerActionItem extends ActionWidgetDropdownActionViewItem {
return {
...makeAction(mode, currentMode),
tooltip: mode.description.get() ?? chatAgentService.getDefaultAgent(ChatAgentLocation.Chat, mode.kind)?.description ?? action.tooltip,
icon: mode.icon.get(),
category: agentModeDisabledViaPolicy ? policyDisabledCategory : customCategory
};
};
@@ -132,7 +134,7 @@ export class ModePickerActionItem extends ActionWidgetDropdownActionViewItem {
showItemKeybindings: true
};
super(action, modePickerActionWidgetOptions, actionWidgetService, keybindingService, contextKeyService);
super(action, modePickerActionWidgetOptions, pickerOptions, actionWidgetService, keybindingService, contextKeyService);
// Listen to changes in the current mode and its properties
this._register(autorun(reader => {
@@ -153,13 +155,19 @@ export class ModePickerActionItem extends ActionWidgetDropdownActionViewItem {
protected override renderLabel(element: HTMLElement): IDisposable | null {
this.setAriaLabelAttributes(element);
const isDefault = this.delegate.currentMode.get().id === ChatMode.Agent.id;
const state = this.delegate.currentMode.get().label.get();
dom.reset(element, dom.$('span.chat-input-picker-label', undefined, state), ...renderLabelWithIcons(`$(chevron-down)`));
const icon = this.delegate.currentMode.get().icon.get();
const labelElements = [];
labelElements.push(...renderLabelWithIcons(`$(${icon.id})`));
if (!isDefault) {
labelElements.push(dom.$('span.chat-input-picker-label', undefined, state));
}
labelElements.push(...renderLabelWithIcons(`$(chevron-down)`));
dom.reset(element, ...labelElements);
return null;
}
override render(container: HTMLElement): void {
super.render(container);
container.classList.add('chat-input-picker-item');
}
}

View File

@@ -10,7 +10,6 @@ import { localize } from '../../../../../../nls.js';
import * as dom from '../../../../../../base/browser/dom.js';
import { renderIcon, renderLabelWithIcons } from '../../../../../../base/browser/ui/iconLabel/iconLabels.js';
import { IDisposable } from '../../../../../../base/common/lifecycle.js';
import { ActionWidgetDropdownActionViewItem } from '../../../../../../platform/actions/browser/actionWidgetDropdownActionViewItem.js';
import { IActionWidgetService } from '../../../../../../platform/actionWidget/browser/actionWidget.js';
import { IActionWidgetDropdownAction, IActionWidgetDropdownActionProvider, IActionWidgetDropdownOptions } from '../../../../../../platform/actionWidget/browser/actionWidgetDropdown.js';
import { IContextKeyService } from '../../../../../../platform/contextkey/common/contextkey.js';
@@ -24,6 +23,7 @@ import { IProductService } from '../../../../../../platform/product/common/produ
import { MANAGE_CHAT_COMMAND_ID } from '../../../common/constants.js';
import { TelemetryTrustedValue } from '../../../../../../platform/telemetry/common/telemetryUtils.js';
import { IManagedHoverContent } from '../../../../../../base/browser/ui/hover/hover.js';
import { ChatInputPickerActionViewItem, IChatInputPickerOptions } from './chatInputPickerActionItem.js';
export interface IModelPickerDelegate {
readonly onDidChangeModel: Event<ILanguageModelChatMetadataAndIdentifier>;
@@ -138,13 +138,14 @@ function getModelPickerActionBarActionProvider(commandService: ICommandService,
/**
* Action view item for selecting a language model in the chat interface.
*/
export class ModelPickerActionItem extends ActionWidgetDropdownActionViewItem {
export class ModelPickerActionItem extends ChatInputPickerActionViewItem {
constructor(
action: IAction,
protected currentModel: ILanguageModelChatMetadataAndIdentifier | undefined,
widgetOptions: Omit<IActionWidgetDropdownOptions, 'label' | 'labelRenderer'> | undefined,
delegate: IModelPickerDelegate,
pickerOptions: IChatInputPickerOptions,
@IActionWidgetService actionWidgetService: IActionWidgetService,
@IContextKeyService contextKeyService: IContextKeyService,
@ICommandService commandService: ICommandService,
@@ -162,10 +163,10 @@ export class ModelPickerActionItem extends ActionWidgetDropdownActionViewItem {
const modelPickerActionWidgetOptions: Omit<IActionWidgetDropdownOptions, 'label' | 'labelRenderer'> = {
actionProvider: modelDelegateToWidgetActionsProvider(delegate, telemetryService),
actionBarActionProvider: getModelPickerActionBarActionProvider(commandService, chatEntitlementService, productService)
actionBarActionProvider: getModelPickerActionBarActionProvider(commandService, chatEntitlementService, productService),
};
super(actionWithLabel, widgetOptions ?? modelPickerActionWidgetOptions, actionWidgetService, keybindingService, contextKeyService);
super(actionWithLabel, widgetOptions ?? modelPickerActionWidgetOptions, pickerOptions, actionWidgetService, keybindingService, contextKeyService);
// Listen for model changes from the delegate
this._register(delegate.onDidChangeModel(model => {
@@ -200,8 +201,4 @@ export class ModelPickerActionItem extends ActionWidgetDropdownActionViewItem {
return null;
}
override render(container: HTMLElement): void {
super.render(container);
container.classList.add('chat-input-picker-item');
}
}

View File

@@ -9,7 +9,6 @@ import { IAction } from '../../../../../../base/common/actions.js';
import { IDisposable } from '../../../../../../base/common/lifecycle.js';
import { URI } from '../../../../../../base/common/uri.js';
import { localize } from '../../../../../../nls.js';
import { ActionWidgetDropdownActionViewItem } from '../../../../../../platform/actions/browser/actionWidgetDropdownActionViewItem.js';
import { MenuItemAction } from '../../../../../../platform/actions/common/actions.js';
import { IActionWidgetService } from '../../../../../../platform/actionWidget/browser/actionWidget.js';
import { IActionWidgetDropdownAction, IActionWidgetDropdownActionProvider, IActionWidgetDropdownOptions } from '../../../../../../platform/actionWidget/browser/actionWidgetDropdown.js';
@@ -19,6 +18,7 @@ import { IKeybindingService } from '../../../../../../platform/keybinding/common
import { IOpenerService } from '../../../../../../platform/opener/common/opener.js';
import { IChatSessionsService } from '../../../common/chatSessionsService.js';
import { AgentSessionProviders, getAgentSessionProvider, getAgentSessionProviderIcon, getAgentSessionProviderName } from '../../agentSessions/agentSessions.js';
import { ChatInputPickerActionViewItem, IChatInputPickerOptions } from './chatInputPickerActionItem.js';
export interface ISessionTypePickerDelegate {
getActiveSessionProvider(): AgentSessionProviders | undefined;
@@ -35,13 +35,14 @@ interface ISessionTypeItem {
* Action view item for selecting a session target in the chat interface.
* This picker allows switching between different chat session types contributed via extensions.
*/
export class SessionTypePickerActionItem extends ActionWidgetDropdownActionViewItem {
export class SessionTypePickerActionItem extends ChatInputPickerActionViewItem {
private _sessionTypeItems: ISessionTypeItem[] = [];
constructor(
action: MenuItemAction,
private readonly chatSessionPosition: 'sidebar' | 'editor',
private readonly delegate: ISessionTypePickerDelegate,
pickerOptions: IChatInputPickerOptions,
@IActionWidgetService actionWidgetService: IActionWidgetService,
@IKeybindingService keybindingService: IKeybindingService,
@IContextKeyService contextKeyService: IContextKeyService,
@@ -97,7 +98,7 @@ export class SessionTypePickerActionItem extends ActionWidgetDropdownActionViewI
showItemKeybindings: true,
};
super(action, sessionTargetPickerOptions, actionWidgetService, keybindingService, contextKeyService);
super(action, sessionTargetPickerOptions, pickerOptions, actionWidgetService, keybindingService, contextKeyService);
this._updateAgentSessionItems();
this._register(this.chatSessionsService.onDidChangeAvailability(() => {
@@ -139,12 +140,15 @@ export class SessionTypePickerActionItem extends ActionWidgetDropdownActionViewI
const label = getAgentSessionProviderName(currentType ?? AgentSessionProviders.Local);
const icon = getAgentSessionProviderIcon(currentType ?? AgentSessionProviders.Local);
dom.reset(element, ...renderLabelWithIcons(`$(${icon.id})`), dom.$('span.chat-input-picker-label', undefined, label), ...renderLabelWithIcons(`$(chevron-down)`));
const labelElements = [];
labelElements.push(...renderLabelWithIcons(`$(${icon.id})`));
if (currentType !== AgentSessionProviders.Local) {
labelElements.push(dom.$('span.chat-input-picker-label', undefined, label));
}
labelElements.push(...renderLabelWithIcons(`$(chevron-down)`));
dom.reset(element, ...labelElements);
return null;
}
override render(container: HTMLElement): void {
super.render(container);
container.classList.add('chat-input-picker-item');
}
}

View File

@@ -1308,7 +1308,7 @@ have to be updated for changes to the rules above, or to support more deeply nes
display: flex;
}
.interactive-session .chat-input-toolbars :first-child {
.interactive-session .chat-input-toolbars :not(.responsive.chat-input-toolbar) .actions-container:first-child {
margin-right: auto;
}
@@ -1327,12 +1327,15 @@ have to be updated for changes to the rules above, or to support more deeply nes
.interactive-session .chat-input-toolbars > .chat-input-toolbar {
overflow: hidden;
min-width: 0px;
width: 100%;
.chat-input-picker-item {
min-width: 0px;
overflow: hidden;
.action-label {
min-width: 0px;
overflow: hidden;
.chat-input-picker-label {
overflow: hidden;

View File

@@ -21,6 +21,8 @@ import { ChatContextKeys } from './actions/chatContextKeys.js';
import { ChatConfiguration, ChatModeKind } from './constants.js';
import { IHandOff } from './promptSyntax/promptFileParser.js';
import { ExtensionAgentSourceType, IAgentSource, ICustomAgent, IPromptsService, PromptsStorage } from './promptSyntax/service/promptsService.js';
import { ThemeIcon } from '../../../../base/common/themables.js';
import { Codicon } from '../../../../base/common/codicons.js';
export const IChatModeService = createDecorator<IChatModeService>('chatModeService');
export interface IChatModeService {
@@ -248,6 +250,7 @@ export interface IChatMode {
readonly id: string;
readonly name: IObservable<string>;
readonly label: IObservable<string>;
readonly icon: IObservable<ThemeIcon>;
readonly description: IObservable<string | undefined>;
readonly isBuiltin: boolean;
readonly kind: ChatModeKind;
@@ -317,6 +320,10 @@ export class CustomChatMode implements IChatMode {
return this._descriptionObservable;
}
get icon(): IObservable<ThemeIcon> {
return constObservable(Codicon.tasklist);
}
public get isBuiltin(): boolean {
return isBuiltinChatMode(this);
}
@@ -457,15 +464,18 @@ export class BuiltinChatMode implements IChatMode {
public readonly name: IObservable<string>;
public readonly label: IObservable<string>;
public readonly description: IObservable<string>;
public readonly icon: IObservable<ThemeIcon>;
constructor(
public readonly kind: ChatModeKind,
label: string,
description: string
description: string,
icon: ThemeIcon,
) {
this.name = constObservable(kind);
this.label = constObservable(label);
this.description = observableValue('description', description);
this.icon = constObservable(icon);
}
public get isBuiltin(): boolean {
@@ -495,9 +505,9 @@ export class BuiltinChatMode implements IChatMode {
}
export namespace ChatMode {
export const Ask = new BuiltinChatMode(ChatModeKind.Ask, 'Ask', localize('chatDescription', "Explore and understand your code"));
export const Edit = new BuiltinChatMode(ChatModeKind.Edit, 'Edit', localize('editsDescription', "Edit or refactor selected code"));
export const Agent = new BuiltinChatMode(ChatModeKind.Agent, 'Agent', localize('agentDescription', "Describe what to build next"));
export const Ask = new BuiltinChatMode(ChatModeKind.Ask, 'Ask', localize('chatDescription', "Explore and understand your code"), Codicon.question);
export const Edit = new BuiltinChatMode(ChatModeKind.Edit, 'Edit', localize('editsDescription', "Edit or refactor selected code"), Codicon.edit);
export const Agent = new BuiltinChatMode(ChatModeKind.Agent, 'Agent', localize('agentDescription', "Describe what to build next"), Codicon.agent);
}
export function isBuiltinChatMode(mode: IChatMode): boolean {

View File

@@ -94,7 +94,7 @@ export class RepositoryRenderer implements ICompressibleTreeRenderer<ISCMReposit
const label = new IconLabel(provider, { supportIcons: false });
const actions = append(provider, $('.actions'));
const toolBar = new WorkbenchToolBar(actions, { actionViewItemProvider: this.actionViewItemProvider, resetMenu: this.toolbarMenuId, responsiveBehavior: { enabled: true, minItems: 2 } }, this.menuService, this.contextKeyService, this.contextMenuService, this.keybindingService, this.commandService, this.telemetryService);
const toolBar = new WorkbenchToolBar(actions, { actionViewItemProvider: this.actionViewItemProvider, resetMenu: this.toolbarMenuId, responsiveBehavior: { enabled: true, kind: 'all', minItems: 2 } }, this.menuService, this.contextKeyService, this.contextMenuService, this.keybindingService, this.commandService, this.telemetryService);
const countContainer = append(provider, $('.count'));
const count = new CountBadge(countContainer, {}, defaultCountBadgeStyles);
const visibilityDisposable = toolBar.onDidChangeDropdownVisibility(e => provider.classList.toggle('active', e));