mirror of
https://github.com/microsoft/vscode.git
synced 2026-02-15 07:28:05 +00:00
Merge pull request #288120 from microsoft/benibenj/meaningful-junglefowl
Improved chat input pickers
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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));
|
||||
|
||||
Reference in New Issue
Block a user