Skip to content
4 changes: 4 additions & 0 deletions src/vs/platform/terminal/common/terminal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -493,6 +493,7 @@ export interface IHeartbeatService {


export interface IShellLaunchConfig {
shellType?: string;
/**
* The name of the terminal, if this is not set the name of the process will be used.
*/
Expand Down Expand Up @@ -731,6 +732,7 @@ export interface IShellLaunchConfigDto {
tabActions?: ITerminalTabAction[];
shellIntegrationEnvironmentReporting?: boolean;
titleTemplate?: string;
shellType?: string;
}

/**
Expand Down Expand Up @@ -938,6 +940,7 @@ export interface ITerminalProfile {
overrideName?: boolean;
color?: string;
icon?: ThemeIcon | URI | { light: URI; dark: URI };
shellType?: string;
}

export interface ITerminalDimensionsOverride extends Readonly<ITerminalDimensions> {
Expand All @@ -960,6 +963,7 @@ export interface IBaseUnresolvedTerminalProfile {
color?: string;
env?: ITerminalEnvironment;
requiresPath?: string | ITerminalUnsafePath;
shellType?: string;
}

export interface ITerminalUnsafePath {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ export const terminalProfileBaseProperties: IJSONSchemaMap = {
type: ['string', 'null']
},
default: {}
},
shellType: {
description: localize('terminalProfile.shellType', 'Override the automatically detected shell executable basename for supported shells. Supports both variants with and without the \'.exe\' extension (e.g., \'bash\' and \'bash.exe\') for cross-platform profile sharing. Useful when using wrappers like host-spawn or flatpak-spawn. Set to \'none\' to explicitly disable shell integration for this profile.'),
type: 'string',
enum: ['none', 'bash', 'bash.exe', 'zsh', 'zsh.exe', 'pwsh', 'pwsh.exe', 'powershell', 'powershell.exe', 'fish', 'fish.exe']
}
};

Expand Down
311 changes: 180 additions & 131 deletions src/vs/platform/terminal/node/terminalEnvironment.ts

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions src/vs/platform/terminal/node/terminalProfiles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,7 @@ async function getValidatedProfile(
validatedProfile.isAutoDetected = profile.isAutoDetected;
validatedProfile.icon = icon;
validatedProfile.color = profile.color;
validatedProfile.shellType = profile.shellType;
return validatedProfile;
}

Expand Down
36 changes: 36 additions & 0 deletions src/vs/platform/terminal/test/node/terminalEnvironment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

/* eslint-disable local/code-no-test-async-suite */
import { deepStrictEqual, ok, strictEqual } from 'assert';
import { isWindows } from '../../../../base/common/platform.js';
import { homedir, userInfo } from 'os';
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';
import { NullLogService } from '../../../log/common/log.js';
Expand Down Expand Up @@ -39,6 +40,10 @@ suite('platform - terminalEnvironment', async () => {
});
});

test('when executable or shellType is "none"', async () => {
strictEqual((await getShellIntegrationInjection({ executable: 'none', args: [] }, enabledProcessOptions, defaultEnvironment, logService, productService, true)).type, 'failure');
strictEqual((await getShellIntegrationInjection({ executable: pwshExe, shellType: 'none', args: [] } as unknown as IShellLaunchConfig, enabledProcessOptions, defaultEnvironment, logService, productService, true)).type, 'failure');
});
// These tests are only expected to work on Windows 10 build 18309 and above
(getWindowsBuildNumberSync() < 18309 ? suite.skip : suite)('pwsh', async () => {
const expectedPs1 = process.platform === 'win32'
Expand All @@ -61,6 +66,9 @@ suite('platform - terminalEnvironment', async () => {
deepStrictEqualIgnoreStableVar(await getShellIntegrationInjection({ executable: pwshExe, args: [] }, enabledProcessOptions, defaultEnvironment, logService, productService, true), enabledExpectedResult);
deepStrictEqualIgnoreStableVar(await getShellIntegrationInjection({ executable: pwshExe, args: undefined }, enabledProcessOptions, defaultEnvironment, logService, productService, true), enabledExpectedResult);
});
test('executable should be normalized to remove .exe', async () => {
deepStrictEqualIgnoreStableVar(await getShellIntegrationInjection({ executable: 'pwsh.exe', args: [] }, enabledProcessOptions, defaultEnvironment, logService, productService, true), enabledExpectedResult);
});
suite('when no logo', async () => {
test('array - case insensitive', async () => {
deepStrictEqualIgnoreStableVar(await getShellIntegrationInjection({ executable: pwshExe, args: ['-NoLogo'] }, enabledProcessOptions, defaultEnvironment, logService, productService, true), enabledExpectedResult);
Expand Down Expand Up @@ -268,6 +276,34 @@ suite('platform - terminalEnvironment', async () => {
strictEqual(customProcessOptions.shellIntegration.nonce, 'custom-nonce-12345');
});
});

suite('shellType auto-detection and overrides', async () => {
(isWindows ? test.skip : test)('should auto-detect host-spawn wrapper', async () => {
const result = await getShellIntegrationInjection({ executable: '/app/bin/host-spawn', args: ['bash'] }, enabledProcessOptions, defaultEnvironment, logService, productService, false);
strictEqual(result.type, 'injection');
const injection = result as IShellIntegrationConfigInjection;
strictEqual(injection.newArgs?.[0], 'bash'); // The wrapperArg
});
(isWindows ? test.skip : test)('should auto-detect flatpak-spawn wrapper', async () => {
const result = await getShellIntegrationInjection({ executable: 'flatpak-spawn', args: ['--host', 'bash'] }, enabledProcessOptions, defaultEnvironment, logService, productService, false);
strictEqual(result.type, 'injection');
const injection = result as IShellIntegrationConfigInjection;
strictEqual(injection.newArgs?.[0], '--host'); // The wrapperArg
strictEqual(injection.newArgs?.[1], 'bash'); // The wrapperArg
});
(isWindows ? test.skip : test)('should respect a shellType=bash override when executable=unknown-shell', async () => {
const result = await getShellIntegrationInjection({ executable: 'unknown-shell', shellType: 'bash', args: ['-l'] }, enabledProcessOptions, defaultEnvironment, logService, productService, false);
strictEqual(result.type, 'injection');
const injection = result as IShellIntegrationConfigInjection;
strictEqual(injection.envMixin?.['VSCODE_SHELL_LOGIN'], '1');
});
(isWindows ? test.skip : test)('should respect a shellType=powershell override when executable=unknown-shell', async () => {
const result = await getShellIntegrationInjection({ executable: 'unknown-shell', shellType: 'powershell', args: [] }, enabledProcessOptions, defaultEnvironment, logService, productService, false);
strictEqual(result.type, 'injection');
const injection = result as IShellIntegrationConfigInjection;
strictEqual(injection.newArgs?.some(arg => arg.includes('shellIntegration')), true);
});
});
Comment on lines +280 to +306
});

suite('sanitizeEnvForLogging', () => {
Expand Down
1 change: 1 addition & 0 deletions src/vs/server/node/remoteTerminalChannel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ export class RemoteTerminalChannel extends Disposable implements IServerChannel<
forceShellIntegration: args.shellLaunchConfig.forceShellIntegration,
tabActions: args.shellLaunchConfig.tabActions,
shellIntegrationEnvironmentReporting: args.shellLaunchConfig.shellIntegrationEnvironmentReporting,
shellType: args.shellLaunchConfig.shellType
};


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,7 @@ class RemoteTerminalBackend extends BaseTerminalBackend implements ITerminalBack
forceShellIntegration: shellLaunchConfig.forceShellIntegration,
tabActions: shellLaunchConfig.tabActions,
shellIntegrationEnvironmentReporting: shellLaunchConfig.shellIntegrationEnvironmentReporting,
shellType: shellLaunchConfig.shellType,
};
const activeWorkspaceRootUri = getWorkspaceForTerminal(shellLaunchConfig.cwd, this._workspaceContextService, this._historyService)?.uri;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,9 @@ export abstract class BaseTerminalProfileResolverService extends Disposable impl
shellLaunchConfig.env = resolvedProfile.env;
}
}
if (resolvedProfile.shellType) {
shellLaunchConfig.shellType = resolvedProfile.shellType;
}

Comment on lines +132 to +135
// Verify the icon is valid, and fallback correctly to the generic terminal id if there is
// an issue
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,7 @@ function profilesEqual(one: ITerminalProfile, other: ITerminalProfile) {
one.isAutoDetected === other.isAutoDetected &&
one.isDefault === other.isDefault &&
one.overrideName === other.overrideName &&
one.shellType === other.shellType &&
one.path === other.path;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import { deepStrictEqual, fail, ok, strictEqual } from 'assert';
import { isWindows } from '../../../../../base/common/platform.js';
import { ITerminalProfile, ProfileSource } from '../../../../../platform/terminal/common/terminal.js';
import { ITerminalProfile, ProfileSource, GeneralShellType, PosixShellType } from '../../../../../platform/terminal/common/terminal.js';
import { ITerminalConfiguration, ITerminalProfiles } from '../../common/terminal.js';
import { detectAvailableProfiles, IFsProvider } from '../../../../../platform/terminal/node/terminalProfiles.js';
import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js';
Expand Down Expand Up @@ -148,6 +148,23 @@ suite('Workbench - TerminalProfiles', () => {
strictEqual(profiles[0].profileName, 'PowerShell');
});
});
test('should preserve shellType from profile', async () => {
const config = {
profiles: {
windows: {
'PowerShell': { path: 'C:\\pwsh.exe', shellType: GeneralShellType.PowerShell }
},
linux: {},
osx: {}
},
useWslProfiles: false
} as ITestTerminalConfig as ITerminalConfiguration;
const fsProvider = createFsProvider(['C:\\pwsh.exe']);
const configurationService = new TestConfigurationService({ terminal: { integrated: config } });
const profiles = await detectAvailableProfiles(undefined, undefined, false, configurationService, process.env, fsProvider, undefined, undefined, undefined);
const expected = [{ profileName: 'PowerShell', path: 'C:\\pwsh.exe', isDefault: true, shellType: GeneralShellType.PowerShell }];
profilesEqual(profiles, expected);
});
} else {
const absoluteConfig = ({
profiles: {
Expand Down Expand Up @@ -220,6 +237,28 @@ suite('Workbench - TerminalProfiles', () => {
];
profilesEqual(profiles, expected);
});

test('should preserve shellType from profile', async () => {
const config = {
profiles: {
windows: {},
linux: {
'fakeshell1': { path: '/bin/fakeshell1', shellType: PosixShellType.Sh }
},
osx: {
'fakeshell1': { path: '/bin/fakeshell1', shellType: PosixShellType.Sh }
}
},
useWslProfiles: false
} as ITestTerminalConfig as ITerminalConfiguration;
const fsProvider = createFsProvider(['/bin/fakeshell1']);
const configurationService = new TestConfigurationService({ terminal: { integrated: config } });
const profiles = await detectAvailableProfiles(undefined, undefined, false, configurationService, process.env, fsProvider, undefined, undefined, undefined);
const expected: ITerminalProfile[] = [
{ profileName: 'fakeshell1', path: '/bin/fakeshell1', isDefault: true, shellType: PosixShellType.Sh }
];
profilesEqual(profiles, expected);
});
}
});

Expand Down