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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,7 @@ export class CodeActionController extends Disposable implements IEditorContribut
delegate,
anchor,
editorDom,
undefined,
this._getActionBarActions(actions, at, options));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ class PostEditWidget<T extends DocumentPasteEdit | DocumentDropEdit> extends Dis
return this.onSelectNewEdit(i);
}
},
}, anchor, this.editor.getDomNode() ?? undefined, this.additionalActions);
}, anchor, this.editor.getDomNode() ?? undefined, undefined, this.additionalActions);
}
}

Expand Down
47 changes: 37 additions & 10 deletions src/vs/platform/actionWidget/browser/actionList.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,23 @@ import { IListAccessibilityProvider, List } from '../../../base/browser/ui/list/
import { CancellationToken, CancellationTokenSource } from '../../../base/common/cancellation.js';
import { Codicon } from '../../../base/common/codicons.js';
import { ResolvedKeybinding } from '../../../base/common/keybindings.js';
import { Disposable } from '../../../base/common/lifecycle.js';
import { Disposable, DisposableStore } from '../../../base/common/lifecycle.js';
import { OS } from '../../../base/common/platform.js';
import { ThemeIcon } from '../../../base/common/themables.js';
import './actionWidget.css';
import { localize } from '../../../nls.js';
import { IContextViewService } from '../../contextview/browser/contextView.js';
import { IKeybindingService } from '../../keybinding/common/keybinding.js';
import { IInstantiationService } from '../../instantiation/common/instantiation.js';
import { defaultListStyles } from '../../theme/browser/defaultStyles.js';
import { asCssVariable } from '../../theme/common/colorRegistry.js';
import { ILayoutService } from '../../layout/browser/layoutService.js';
import { IHoverService } from '../../hover/browser/hover.js';
import { IHoverPositionOptions } from '../../../base/browser/ui/hover/hover.js';

export interface IActionListHoverOptions {
position?: IHoverPositionOptions;
}

export const acceptSelectedActionCommand = 'acceptSelectedCodeAction';
export const previewSelectedActionCommand = 'previewSelectedCodeAction';
Expand Down Expand Up @@ -49,6 +56,7 @@ interface IActionMenuTemplateData {
readonly text: HTMLElement;
readonly description?: HTMLElement;
readonly keybinding: KeybindingLabel;
readonly elementDisposables: DisposableStore;
}

export const enum ActionListItemKind {
Expand Down Expand Up @@ -117,7 +125,9 @@ class ActionItemRenderer<T> implements IListRenderer<IActionListItem<T>, IAction

constructor(
private readonly _supportsPreview: boolean,
@IKeybindingService private readonly _keybindingService: IKeybindingService
private readonly _customHover: IActionListHoverOptions | undefined,
@IKeybindingService private readonly _keybindingService: IKeybindingService,
@IHoverService private readonly _hoverService: IHoverService,
) { }

renderTemplate(container: HTMLElement): IActionMenuTemplateData {
Expand All @@ -136,11 +146,14 @@ class ActionItemRenderer<T> implements IListRenderer<IActionListItem<T>, IAction
container.append(description);

const keybinding = new KeybindingLabel(container, OS);
const elementDisposables = new DisposableStore();

return { container, icon, text, description, keybinding };
return { container, icon, text, description, keybinding, elementDisposables };
}

renderElement(element: IActionListItem<T>, _index: number, data: IActionMenuTemplateData): void {
data.elementDisposables.clear();

if (element.group?.icon) {
data.icon.className = ThemeIcon.asClassName(element.group.icon);
if (element.group.icon.color) {
Expand Down Expand Up @@ -175,23 +188,36 @@ class ActionItemRenderer<T> implements IListRenderer<IActionListItem<T>, IAction
const actionTitle = this._keybindingService.lookupKeybinding(acceptSelectedActionCommand)?.getLabel();
const previewTitle = this._keybindingService.lookupKeybinding(previewSelectedActionCommand)?.getLabel();
data.container.classList.toggle('option-disabled', element.disabled);

let tooltipText = '';
if (element.tooltip) {
data.container.title = element.tooltip;
tooltipText = element.tooltip;
} else if (element.disabled) {
data.container.title = element.label;
tooltipText = element.label;
} else if (actionTitle && previewTitle) {
if (this._supportsPreview && element.canPreview) {
data.container.title = localize({ key: 'label-preview', comment: ['placeholders are keybindings, e.g "F2 to Apply, Shift+F2 to Preview"'] }, "{0} to Apply, {1} to Preview", actionTitle, previewTitle);
tooltipText = localize({ key: 'label-preview', comment: ['placeholders are keybindings, e.g "F2 to Apply, Shift+F2 to Preview"'] }, "{0} to Apply, {1} to Preview", actionTitle, previewTitle);
} else {
data.container.title = localize({ key: 'label', comment: ['placeholder is a keybinding, e.g "F2 to Apply"'] }, "{0} to Apply", actionTitle);
tooltipText = localize({ key: 'label', comment: ['placeholder is a keybinding, e.g "F2 to Apply"'] }, "{0} to Apply", actionTitle);
}
} else {
}

// Use hover service if enabled
if (this._customHover && tooltipText) {
data.elementDisposables.add(this._hoverService.setupDelayedHover(
data.container,
{ content: tooltipText, position: this._customHover.position, appearance: { showPointer: true } },
{ groupId: 'actionList' }
));
data.container.title = '';
} else {
data.container.title = tooltipText;
}
}

disposeTemplate(templateData: IActionMenuTemplateData): void {
templateData.keybinding.dispose();
templateData.elementDisposables.dispose();
}
}

Expand Down Expand Up @@ -228,12 +254,13 @@ export class ActionList<T> extends Disposable {
constructor(
user: string,
preview: boolean,
customHover: IActionListHoverOptions | undefined,
items: readonly IActionListItem<T>[],
private readonly _delegate: IActionListDelegate<T>,
accessibilityProvider: Partial<IListAccessibilityProvider<IActionListItem<T>>> | undefined,
@IContextViewService private readonly _contextViewService: IContextViewService,
@IKeybindingService private readonly _keybindingService: IKeybindingService,
@ILayoutService private readonly _layoutService: ILayoutService,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
) {
super();
this.domNode = document.createElement('div');
Expand All @@ -254,7 +281,7 @@ export class ActionList<T> extends Disposable {


this._list = this._register(new List(user, this.domNode, virtualDelegate, [
new ActionItemRenderer<IActionListItem<T>>(preview, this._keybindingService),
this._instantiationService.createInstance(ActionItemRenderer<IActionListItem<T>>, preview, customHover),
new HeaderRenderer(),
new SeparatorRenderer(),
], {
Expand Down
8 changes: 4 additions & 4 deletions src/vs/platform/actionWidget/browser/actionWidget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { KeyCode, KeyMod } from '../../../base/common/keyCodes.js';
import { Disposable, DisposableStore, IDisposable, MutableDisposable } from '../../../base/common/lifecycle.js';
import './actionWidget.css';
import { localize, localize2 } from '../../../nls.js';
import { acceptSelectedActionCommand, ActionList, IActionListDelegate, IActionListItem, previewSelectedActionCommand } from './actionList.js';
import { acceptSelectedActionCommand, ActionList, IActionListDelegate, IActionListItem, IActionListHoverOptions, previewSelectedActionCommand } from './actionList.js';
import { Action2, registerAction2 } from '../../actions/common/actions.js';
import { IContextKeyService, RawContextKey } from '../../contextkey/common/contextkey.js';
import { IContextViewService } from '../../contextview/browser/contextView.js';
Expand All @@ -36,7 +36,7 @@ export const IActionWidgetService = createDecorator<IActionWidgetService>('actio
export interface IActionWidgetService {
readonly _serviceBrand: undefined;

show<T>(user: string, supportsPreview: boolean, items: readonly IActionListItem<T>[], delegate: IActionListDelegate<T>, anchor: HTMLElement | StandardMouseEvent | IAnchor, container: HTMLElement | undefined, actionBarActions?: readonly IAction[], accessibilityProvider?: Partial<IListAccessibilityProvider<IActionListItem<T>>>): void;
show<T>(user: string, supportsPreview: boolean, items: readonly IActionListItem<T>[], delegate: IActionListDelegate<T>, anchor: HTMLElement | StandardMouseEvent | IAnchor, container: HTMLElement | undefined, customHover: IActionListHoverOptions | undefined, actionBarActions?: readonly IAction[], accessibilityProvider?: Partial<IListAccessibilityProvider<IActionListItem<T>>>): void;

hide(didCancel?: boolean): void;

Expand All @@ -60,10 +60,10 @@ class ActionWidgetService extends Disposable implements IActionWidgetService {
super();
}

show<T>(user: string, supportsPreview: boolean, items: readonly IActionListItem<T>[], delegate: IActionListDelegate<T>, anchor: HTMLElement | StandardMouseEvent | IAnchor, container: HTMLElement | undefined, actionBarActions?: readonly IAction[], accessibilityProvider?: Partial<IListAccessibilityProvider<IActionListItem<T>>>): void {
show<T>(user: string, supportsPreview: boolean, items: readonly IActionListItem<T>[], delegate: IActionListDelegate<T>, anchor: HTMLElement | StandardMouseEvent | IAnchor, container: HTMLElement | undefined, customHover: IActionListHoverOptions | undefined, actionBarActions?: readonly IAction[], accessibilityProvider?: Partial<IListAccessibilityProvider<IActionListItem<T>>>): void {
const visibleContext = ActionWidgetContextKeys.Visible.bindTo(this._contextKeyService);

const list = this._instantiationService.createInstance(ActionList, user, supportsPreview, items, delegate, accessibilityProvider);
const list = this._instantiationService.createInstance(ActionList, user, supportsPreview, customHover, items, delegate, accessibilityProvider);
this._contextViewService.showContextView({
getAnchor: () => anchor,
render: (container: HTMLElement) => {
Expand Down
6 changes: 5 additions & 1 deletion src/vs/platform/actionWidget/browser/actionWidgetDropdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import { IActionWidgetService } from './actionWidget.js';
import { IAction } from '../../../base/common/actions.js';
import { BaseDropdown, IActionProvider, IBaseDropdownOptions } from '../../../base/browser/ui/dropdown/dropdown.js';
import { ActionListItemKind, IActionListDelegate, IActionListItem } from './actionList.js';
import { ActionListItemKind, IActionListDelegate, IActionListItem, IActionListHoverOptions } from './actionList.js';
import { ThemeIcon } from '../../../base/common/themables.js';
import { Codicon } from '../../../base/common/codicons.js';
import { getActiveElement, isHTMLElement } from '../../../base/browser/dom.js';
Expand Down Expand Up @@ -36,6 +36,9 @@ export interface IActionWidgetDropdownOptions extends IBaseDropdownOptions {

// Function that returns the anchor element for the dropdown
getAnchor?: () => HTMLElement;

/** When set, tooltips will be rendered using the custom workbench hover instead of title attribute */
customHover?: IActionListHoverOptions;
}

/**
Expand Down Expand Up @@ -174,6 +177,7 @@ export class ActionWidgetDropdown extends BaseDropdown {
actionWidgetDelegate,
this._options.getAnchor?.() ?? this.element,
undefined,
this._options.customHover,
actionBarActions,
accessibilityProvider
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,19 @@ export function getAgentSessionProviderIcon(provider: AgentSessionProviders): Th
}
}

export function getAgentSessionProviderDescription(provider: AgentSessionProviders): string {
switch (provider) {
case AgentSessionProviders.Local:
return localize('chat.session.providerDescription.local', "Run in your local workspace.");
case AgentSessionProviders.Background:
return localize('chat.session.providerDescription.background', "Runs locally on a copy of the workspace. Changes won't appear until you apply them. Uses GitHub Copilot CLI and runs in a git worktree.");
case AgentSessionProviders.Cloud:
return localize('chat.session.providerDescription.cloud', "Runs on a remote device in the cloud. Changes won't appear in the active workspace until you check them out. Will create a public pull request in your repository.");
case AgentSessionProviders.ClaudeCode:
return localize('chat.session.providerDescription.claude', "Run in your local workspace using Anthropic's Claude SDK.");
}
}

export enum AgentSessionsViewerOrientation {
Stacked = 1,
SideBySide,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { ExtensionAgentSourceType, PromptsStorage } from '../../../common/prompt
import { getOpenChatActionIdForMode } from '../../actions/chatActions.js';
import { IToggleChatModeArgs, ToggleAgentModeActionId } from '../../actions/chatExecuteActions.js';
import { ChatInputPickerActionViewItem, IChatInputPickerOptions } from './chatInputPickerActionItem.js';
import { HoverPosition } from '../../../../../../base/browser/ui/hover/hoverWidget.js';

export interface IModePickerDelegate {
readonly currentMode: IObservable<IChatMode>;
Expand Down Expand Up @@ -131,7 +132,8 @@ export class ModePickerActionItem extends ChatInputPickerActionViewItem {
actionBarActionProvider: {
getActions: () => this.getModePickerActionBarActions()
},
showItemKeybindings: true
showItemKeybindings: true,
customHover: { position: { hoverPosition: HoverPosition.LEFT } },
};

super(action, modePickerActionWidgetOptions, pickerOptions, actionWidgetService, keybindingService, contextKeyService);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@ import { IContextKeyService } from '../../../../../../platform/contextkey/common
import { IKeybindingService } from '../../../../../../platform/keybinding/common/keybinding.js';
import { IOpenerService } from '../../../../../../platform/opener/common/opener.js';
import { IChatSessionsService } from '../../../common/chatSessionsService.js';
import { AgentSessionProviders, getAgentSessionProvider, getAgentSessionProviderIcon, getAgentSessionProviderName } from '../../agentSessions/agentSessions.js';
import { AgentSessionProviders, getAgentSessionProvider, getAgentSessionProviderDescription, getAgentSessionProviderIcon, getAgentSessionProviderName } from '../../agentSessions/agentSessions.js';
import { ChatInputPickerActionViewItem, IChatInputPickerOptions } from './chatInputPickerActionItem.js';
import { ISessionTypePickerDelegate } from '../../chat.js';
import { HoverPosition } from '../../../../../../base/browser/ui/hover/hoverWidget.js';

interface ISessionTypeItem {
type: AgentSessionProviders;
Expand Down Expand Up @@ -97,6 +98,7 @@ export class SessionTypePickerActionItem extends ChatInputPickerActionViewItem {
actionBarActions,
actionBarActionProvider: undefined,
showItemKeybindings: true,
customHover: { position: { hoverPosition: HoverPosition.LEFT } },
};

super(action, sessionTargetPickerOptions, pickerOptions, actionWidgetService, keybindingService, contextKeyService);
Expand All @@ -111,7 +113,7 @@ export class SessionTypePickerActionItem extends ChatInputPickerActionViewItem {
const localSessionItem = {
type: AgentSessionProviders.Local,
label: getAgentSessionProviderName(AgentSessionProviders.Local),
description: localize('chat.sessionTarget.local.description', "Local chat session"),
description: getAgentSessionProviderDescription(AgentSessionProviders.Local),
commandId: `workbench.action.chat.openNewChatSessionInPlace.${AgentSessionProviders.Local}`,
};

Expand All @@ -124,10 +126,11 @@ export class SessionTypePickerActionItem extends ChatInputPickerActionViewItem {
continue;
}

const description = getAgentSessionProviderDescription(agentSessionType);
agentSessionItems.push({
type: agentSessionType,
label: getAgentSessionProviderName(agentSessionType),
description: contribution.description,
description: description,
Copy link

Copilot AI Jan 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The variable assignment description: description, is redundant. The description variable can be inlined directly into the object since it's only used once and doesn't add clarity.

See below for a potential fix:

			agentSessionItems.push({
				type: agentSessionType,
				label: getAgentSessionProviderName(agentSessionType),
				description: getAgentSessionProviderDescription(agentSessionType),

Copilot uses AI. Check for mistakes.
commandId: `workbench.action.chat.openNewChatSessionInPlace.${contribution.type}`,
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ export class TerminalQuickFixAddon extends Disposable implements ITerminalAddon,
this._terminal?.focus();
},
};
this._actionWidgetService.show('quickFixWidget', false, toActionWidgetItems(actionSet.validActions, true), delegate, this._currentRenderContext.anchor, this._currentRenderContext.parentElement);
this._actionWidgetService.show('quickFixWidget', false, toActionWidgetItems(actionSet.validActions, true), delegate, this._currentRenderContext.anchor, this._currentRenderContext.parentElement, undefined);
}

registerCommandSelector(selector: ITerminalCommandSelector): void {
Expand Down
Loading