add experiment to elevate AI terminal profiles (#299270)

This commit is contained in:
Megan Rogge
2026-03-04 15:41:25 -05:00
committed by GitHub
parent b3ad9079ba
commit d405135f71
5 changed files with 139 additions and 37 deletions

View File

@@ -122,6 +122,7 @@ export const enum TerminalSettingId {
FontLigaturesFallbackLigatures = 'terminal.integrated.fontLigatures.fallbackLigatures',
EnableKittyKeyboardProtocol = 'terminal.integrated.enableKittyKeyboardProtocol',
EnableWin32InputMode = 'terminal.integrated.enableWin32InputMode',
ExperimentalAiProfileGrouping = 'terminal.integrated.experimental.aiProfileGrouping',
AllowInUntrustedWorkspace = 'terminal.integrated.allowInUntrustedWorkspace',
// Developer/debug settings

View File

@@ -13,6 +13,7 @@ import { IContextKeyService } from '../../../../platform/contextkey/common/conte
import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';
import { IEditorOptions } from '../../../../platform/editor/common/editor.js';
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
import { IStorageService } from '../../../../platform/storage/common/storage.js';
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
@@ -60,6 +61,7 @@ export class TerminalEditor extends EditorPane {
@ITerminalProfileResolverService private readonly _terminalProfileResolverService: ITerminalProfileResolverService,
@ITerminalService private readonly _terminalService: ITerminalService,
@ITerminalConfigurationService private readonly _terminalConfigurationService: ITerminalConfigurationService,
@IConfigurationService private readonly _configurationService: IConfigurationService,
@IContextKeyService contextKeyService: IContextKeyService,
@IMenuService menuService: IMenuService,
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@@ -156,7 +158,7 @@ export class TerminalEditor extends EditorPane {
private _updateTabActionBar(profiles: ITerminalProfile[]): void {
this._disposableStore.clear();
const actions = getTerminalActionBarArgs(TerminalLocation.Editor, profiles, this._getDefaultProfileName(), this._terminalProfileService.contributedProfiles, this._terminalService, this._dropdownMenu, this._disposableStore);
const actions = getTerminalActionBarArgs(TerminalLocation.Editor, profiles, this._getDefaultProfileName(), this._terminalProfileService.contributedProfiles, this._terminalService, this._dropdownMenu, this._disposableStore, this._configurationService);
this._newDropdown.value?.update(actions.dropdownAction, actions.dropdownMenuActions);
}
@@ -180,7 +182,7 @@ export class TerminalEditor extends EditorPane {
if (action instanceof MenuItemAction) {
const location = { viewColumn: ACTIVE_GROUP };
this._disposableStore.clear();
const actions = getTerminalActionBarArgs(location, this._terminalProfileService.availableProfiles, this._getDefaultProfileName(), this._terminalProfileService.contributedProfiles, this._terminalService, this._dropdownMenu, this._disposableStore);
const actions = getTerminalActionBarArgs(location, this._terminalProfileService.availableProfiles, this._getDefaultProfileName(), this._terminalProfileService.contributedProfiles, this._terminalService, this._dropdownMenu, this._disposableStore, this._configurationService);
this._newDropdown.value = this._instantiationService.createInstance(DropdownWithPrimaryActionViewItem, action, actions.dropdownAction, actions.dropdownMenuActions, actions.className, { hoverDelegate: options.hoverDelegate });
this._newDropdown.value?.update(actions.dropdownAction, actions.dropdownMenuActions);
return this._newDropdown.value;

View File

@@ -8,6 +8,7 @@ import { Codicon } from '../../../../base/common/codicons.js';
import { Schemas } from '../../../../base/common/network.js';
import { localize, localize2 } from '../../../../nls.js';
import { IMenu, MenuId, MenuRegistry } from '../../../../platform/actions/common/actions.js';
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js';
import { IExtensionTerminalProfile, ITerminalProfile, TerminalLocation, TerminalSettingId } from '../../../../platform/terminal/common/terminal.js';
import { ResourceContextKey } from '../../../common/contextkeys.js';
@@ -781,12 +782,20 @@ export function setupTerminalMenus(): void {
}
}
export function getTerminalActionBarArgs(location: ITerminalLocationOptions, profiles: ITerminalProfile[], defaultProfileName: string, contributedProfiles: readonly IExtensionTerminalProfile[], terminalService: ITerminalService, dropdownMenu: IMenu, disposableStore: DisposableStore): {
export function getTerminalActionBarArgs(location: ITerminalLocationOptions, profiles: ITerminalProfile[], defaultProfileName: string, contributedProfiles: readonly IExtensionTerminalProfile[], terminalService: ITerminalService, dropdownMenu: IMenu, disposableStore: DisposableStore, configurationService: IConfigurationService): {
dropdownAction: IAction;
dropdownMenuActions: IAction[];
className: string;
dropdownIcon?: string;
} {
const shouldElevateAiProfiles = configurationService.getValue<boolean>(TerminalSettingId.ExperimentalAiProfileGrouping);
profiles = profiles.filter(e => !e.isAutoDetected);
const [aiProfiles, otherProfiles] = shouldElevateAiProfiles
? splitProfiles(profiles)
: [[], profiles];
const [aiContributedProfiles, otherContributedProfiles] = shouldElevateAiProfiles
? splitContributedProfiles(contributedProfiles)
: [[], contributedProfiles];
const dropdownActions: IAction[] = [];
const submenuActions: IAction[] = [];
const splitLocation = (location === TerminalLocation.Editor || (typeof location === 'object' && hasKey(location, { viewColumn: true }) && location.viewColumn === ACTIVE_GROUP)) ? { viewColumn: SIDE_GROUP } : { splitActiveTerminal: true };
@@ -806,40 +815,22 @@ export function getTerminalActionBarArgs(location: ITerminalLocationOptions, pro
location: splitLocation
}))));
dropdownActions.push(new Separator());
profiles = profiles.filter(e => !e.isAutoDetected);
for (const p of profiles) {
const isDefault = p.profileName === defaultProfileName;
const options: ICreateTerminalOptions = { config: p, location };
const splitOptions: ICreateTerminalOptions = { config: p, location: splitLocation };
const sanitizedProfileName = p.profileName.replace(/[\n\r\t]/g, '');
dropdownActions.push(disposableStore.add(new Action(TerminalCommandId.NewWithProfile, isDefault ? localize('defaultTerminalProfile', "{0} (Default)", sanitizedProfileName) : sanitizedProfileName, undefined, true, async () => {
await terminalService.createAndFocusTerminal(options);
})));
submenuActions.push(disposableStore.add(new Action(TerminalCommandId.Split, isDefault ? localize('defaultTerminalProfile', "{0} (Default)", sanitizedProfileName) : sanitizedProfileName, undefined, true, async () => {
await terminalService.createAndFocusTerminal(splitOptions);
})));
for (const p of aiProfiles) {
addProfileActions(p, defaultProfileName, location, splitLocation, terminalService, dropdownActions, submenuActions, disposableStore);
}
for (const contributed of aiContributedProfiles) {
addContributedProfileActions(contributed, defaultProfileName, location, splitLocation, terminalService, dropdownActions, submenuActions, disposableStore);
}
if ((aiProfiles.length > 0 || aiContributedProfiles.length > 0) && (otherProfiles.length > 0 || otherContributedProfiles.length > 0)) {
dropdownActions.push(new Separator());
}
for (const contributed of contributedProfiles) {
const isDefault = contributed.title === defaultProfileName;
const title = isDefault ? localize('defaultTerminalProfile', "{0} (Default)", contributed.title.replace(/[\n\r\t]/g, '')) : contributed.title.replace(/[\n\r\t]/g, '');
dropdownActions.push(disposableStore.add(new Action('contributed', title, undefined, true, () => terminalService.createAndFocusTerminal({
config: {
extensionIdentifier: contributed.extensionIdentifier,
id: contributed.id,
title
},
location
}))));
submenuActions.push(disposableStore.add(new Action('contributed-split', title, undefined, true, () => terminalService.createAndFocusTerminal({
config: {
extensionIdentifier: contributed.extensionIdentifier,
id: contributed.id,
title
},
location: splitLocation
}))));
for (const p of otherProfiles) {
addProfileActions(p, defaultProfileName, location, splitLocation, terminalService, dropdownActions, submenuActions, disposableStore);
}
for (const contributed of otherContributedProfiles) {
addContributedProfileActions(contributed, defaultProfileName, location, splitLocation, terminalService, dropdownActions, submenuActions, disposableStore);
}
if (dropdownActions.length > 0) {
@@ -852,3 +843,95 @@ export function getTerminalActionBarArgs(location: ITerminalLocationOptions, pro
const dropdownAction = disposableStore.add(new Action('refresh profiles', localize('launchProfile', 'Launch Profile...'), 'codicon-chevron-down', true));
return { dropdownAction, dropdownMenuActions: dropdownActions, className: `terminal-tab-actions-${terminalService.resolveLocation(location)}` };
}
function splitProfiles(profiles: readonly ITerminalProfile[]): [ITerminalProfile[], ITerminalProfile[]] {
const aiProfiles: ITerminalProfile[] = [];
const otherProfiles: ITerminalProfile[] = [];
for (const profile of profiles) {
if (isAiProfileName(profile.profileName)) {
aiProfiles.push(profile);
} else {
otherProfiles.push(profile);
}
}
return [aiProfiles, otherProfiles];
}
function splitContributedProfiles(contributedProfiles: readonly IExtensionTerminalProfile[]): [IExtensionTerminalProfile[], IExtensionTerminalProfile[]] {
const aiContributedProfiles: IExtensionTerminalProfile[] = [];
const otherContributedProfiles: IExtensionTerminalProfile[] = [];
for (const profile of contributedProfiles) {
if (isAiContributedProfile(profile)) {
aiContributedProfiles.push(profile);
} else {
otherContributedProfiles.push(profile);
}
}
return [aiContributedProfiles, otherContributedProfiles];
}
function isAiContributedProfile(profile: IExtensionTerminalProfile): boolean {
const extensionIdentifier = profile.extensionIdentifier.toLowerCase();
if (extensionIdentifier === 'github.copilot-chat' || extensionIdentifier === 'anthropic.claude-code') {
return true;
}
return isAiProfileName(profile.title);
}
function isAiProfileName(name: string): boolean {
const lowerCaseName = name.toLowerCase();
return lowerCaseName.includes('copilot') || lowerCaseName.includes('claude');
}
function addProfileActions(
profile: ITerminalProfile,
defaultProfileName: string,
location: ITerminalLocationOptions,
splitLocation: ITerminalLocationOptions,
terminalService: ITerminalService,
dropdownActions: IAction[],
submenuActions: IAction[],
disposableStore: DisposableStore
): void {
const isDefault = profile.profileName === defaultProfileName;
const options: ICreateTerminalOptions = { config: profile, location };
const splitOptions: ICreateTerminalOptions = { config: profile, location: splitLocation };
const sanitizedProfileName = profile.profileName.replace(/[\n\r\t]/g, '');
dropdownActions.push(disposableStore.add(new Action(TerminalCommandId.NewWithProfile, isDefault ? localize('defaultTerminalProfile', "{0} (Default)", sanitizedProfileName) : sanitizedProfileName, undefined, true, async () => {
await terminalService.createAndFocusTerminal(options);
})));
submenuActions.push(disposableStore.add(new Action(TerminalCommandId.Split, isDefault ? localize('defaultTerminalProfile', "{0} (Default)", sanitizedProfileName) : sanitizedProfileName, undefined, true, async () => {
await terminalService.createAndFocusTerminal(splitOptions);
})));
}
function addContributedProfileActions(
contributed: IExtensionTerminalProfile,
defaultProfileName: string,
location: ITerminalLocationOptions,
splitLocation: ITerminalLocationOptions,
terminalService: ITerminalService,
dropdownActions: IAction[],
submenuActions: IAction[],
disposableStore: DisposableStore
): void {
const isDefault = contributed.title === defaultProfileName;
const title = isDefault ? localize('defaultTerminalProfile', "{0} (Default)", contributed.title.replace(/[\n\r\t]/g, '')) : contributed.title.replace(/[\n\r\t]/g, '');
dropdownActions.push(disposableStore.add(new Action('contributed', title, undefined, true, () => terminalService.createAndFocusTerminal({
config: {
extensionIdentifier: contributed.extensionIdentifier,
id: contributed.id,
title
},
location
}))));
submenuActions.push(disposableStore.add(new Action('contributed-split', title, undefined, true, () => terminalService.createAndFocusTerminal({
config: {
extensionIdentifier: contributed.extensionIdentifier,
id: contributed.id,
title
},
location: splitLocation
}))));
}

View File

@@ -289,7 +289,7 @@ export class TerminalViewPane extends ViewPane {
case TerminalCommandId.New: {
if (action instanceof MenuItemAction) {
this._disposableStore.clear();
const actions = getTerminalActionBarArgs(TerminalLocation.Panel, this._terminalProfileService.availableProfiles, this._getDefaultProfileName(), this._terminalProfileService.contributedProfiles, this._terminalService, this._dropdownMenu, this._disposableStore);
const actions = getTerminalActionBarArgs(TerminalLocation.Panel, this._terminalProfileService.availableProfiles, this._getDefaultProfileName(), this._terminalProfileService.contributedProfiles, this._terminalService, this._dropdownMenu, this._disposableStore, this._configurationService);
this._newDropdown.value = this._instantiationService.createInstance(DropdownWithPrimaryActionViewItem, action, actions.dropdownAction, actions.dropdownMenuActions, actions.className, {
hoverDelegate: options.hoverDelegate,
getKeyBinding: (action: IAction) => this._keybindingService.lookupKeybinding(action.id, this._contextKeyService)
@@ -318,8 +318,15 @@ export class TerminalViewPane extends ViewPane {
private _updateTabActionBar(profiles: ITerminalProfile[]): void {
this._disposableStore.clear();
const actions = getTerminalActionBarArgs(TerminalLocation.Panel, profiles, this._getDefaultProfileName(), this._terminalProfileService.contributedProfiles, this._terminalService, this._dropdownMenu, this._disposableStore);
const actions = getTerminalActionBarArgs(TerminalLocation.Panel, profiles, this._getDefaultProfileName(), this._terminalProfileService.contributedProfiles, this._terminalService, this._dropdownMenu, this._disposableStore, this._configurationService);
this._newDropdown.value?.update(actions.dropdownAction, actions.dropdownMenuActions);
this._disposableStore.add(this._configurationService.onDidChangeConfiguration(e => {
if (e.affectsConfiguration(TerminalSettingId.ExperimentalAiProfileGrouping)) {
const updatedActions = getTerminalActionBarArgs(TerminalLocation.Panel, profiles, this._getDefaultProfileName(), this._terminalProfileService.contributedProfiles, this._terminalService, this._dropdownMenu, this._disposableStore, this._configurationService);
this._newDropdown.value?.update(updatedActions.dropdownAction, updatedActions.dropdownMenuActions);
}
}));
}
override focus() {

View File

@@ -604,6 +604,15 @@ const terminalConfiguration: IStringDictionary<IConfigurationPropertySchema> = {
mode: 'auto'
}
},
[TerminalSettingId.ExperimentalAiProfileGrouping]: {
markdownDescription: localize('terminal.integrated.experimental.aiProfileGrouping', "Whether to elevate AI-contributed terminal profiles (for example Copilot CLI and Claude Agent) in the new terminal dropdown."),
type: 'boolean',
default: false,
tags: ['experimental'],
experiment: {
mode: 'auto'
}
},
[TerminalSettingId.ShellIntegrationEnabled]: {
restricted: true,
markdownDescription: localize('terminal.integrated.shellIntegration.enabled', "Determines whether or not shell integration is auto-injected to support features like enhanced command tracking and current working directory detection. \n\nShell integration works by injecting the shell with a startup script. The script gives VS Code insight into what is happening within the terminal.\n\nSupported shells:\n\n- Linux/macOS: bash, fish, pwsh, zsh\n - Windows: pwsh, git bash\n\nThis setting applies only when terminals are created, so you will need to restart your terminals for it to take effect.\n\n Note that the script injection may not work if you have custom arguments defined in the terminal profile, have enabled {1}, have a [complex bash `PROMPT_COMMAND`](https://code.visualstudio.com/docs/editor/integrated-terminal#_complex-bash-promptcommand), or other unsupported setup. To disable decorations, see {0}", '`#terminal.integrated.shellIntegration.decorationsEnabled#`', '`#editor.accessibilitySupport#`'),