mirror of
https://github.com/microsoft/vscode.git
synced 2026-05-08 17:19:48 +01:00
Merge pull request #265683 from microsoft/copilot/add-terminal-profile-for-agent
Add terminal profile configuration for chat agent tools
This commit is contained in:
@@ -33,7 +33,7 @@ export const terminalIconSchema: IJSONSchema = {
|
||||
markdownEnumDescriptions: Array.from(getAllCodicons(), icon => `$(${icon.id})`),
|
||||
};
|
||||
|
||||
const terminalProfileBaseProperties: IJSONSchemaMap = {
|
||||
export const terminalProfileBaseProperties: IJSONSchemaMap = {
|
||||
args: {
|
||||
description: localize('terminalProfile.args', 'An optional set of arguments to run the shell executable with.'),
|
||||
type: 'array',
|
||||
@@ -41,10 +41,6 @@ const terminalProfileBaseProperties: IJSONSchemaMap = {
|
||||
type: 'string'
|
||||
}
|
||||
},
|
||||
overrideName: {
|
||||
description: localize('terminalProfile.overrideName', 'Whether or not to replace the dynamic terminal title that detects what program is running with the static profile name.'),
|
||||
type: 'boolean'
|
||||
},
|
||||
icon: {
|
||||
description: localize('terminalProfile.icon', 'A codicon ID to associate with the terminal icon.'),
|
||||
...terminalIconSchema
|
||||
@@ -74,6 +70,10 @@ const terminalProfileSchema: IJSONSchema = {
|
||||
type: 'string'
|
||||
}
|
||||
},
|
||||
overrideName: {
|
||||
description: localize('terminalProfile.overrideName', 'Whether or not to replace the dynamic terminal title that detects what program is running with the static profile name.'),
|
||||
type: 'boolean'
|
||||
},
|
||||
...terminalProfileBaseProperties
|
||||
}
|
||||
};
|
||||
|
||||
+27
-14
@@ -13,7 +13,7 @@ import { ThemeIcon } from '../../../../../base/common/themables.js';
|
||||
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
|
||||
import { TerminalCapability } from '../../../../../platform/terminal/common/capabilities/capabilities.js';
|
||||
import { PromptInputState } from '../../../../../platform/terminal/common/capabilities/commandDetection/promptInputModel.js';
|
||||
import { ITerminalLogService, TerminalSettingId } from '../../../../../platform/terminal/common/terminal.js';
|
||||
import { ITerminalLogService, ITerminalProfile, TerminalSettingId, type IShellLaunchConfig } from '../../../../../platform/terminal/common/terminal.js';
|
||||
import { ITerminalService, type ITerminalInstance } from '../../../terminal/browser/terminal.js';
|
||||
import { TerminalChatAgentToolsSettingId } from '../common/terminalChatAgentToolsConfiguration.js';
|
||||
|
||||
@@ -49,8 +49,8 @@ export class ToolTerminalCreator {
|
||||
) {
|
||||
}
|
||||
|
||||
async createTerminal(shell: string, token: CancellationToken): Promise<IToolTerminal> {
|
||||
const instance = await this._createCopilotTerminal(shell);
|
||||
async createTerminal(shellOrProfile: string | ITerminalProfile, token: CancellationToken): Promise<IToolTerminal> {
|
||||
const instance = await this._createCopilotTerminal(shellOrProfile);
|
||||
const toolTerminal: IToolTerminal = {
|
||||
instance,
|
||||
shellIntegrationQuality: ShellIntegrationQuality.None,
|
||||
@@ -126,17 +126,30 @@ export class ToolTerminalCreator {
|
||||
}
|
||||
}
|
||||
|
||||
private _createCopilotTerminal(shell: string) {
|
||||
return this._terminalService.createTerminal({
|
||||
config: {
|
||||
executable: shell,
|
||||
icon: ThemeIcon.fromId(Codicon.chatSparkle.id),
|
||||
hideFromUser: true,
|
||||
env: {
|
||||
GIT_PAGER: 'cat', // avoid making `git diff` interactive when called from copilot
|
||||
},
|
||||
},
|
||||
});
|
||||
private _createCopilotTerminal(shellOrProfile: string | ITerminalProfile) {
|
||||
const config: IShellLaunchConfig = {
|
||||
icon: ThemeIcon.fromId(Codicon.chatSparkle.id),
|
||||
hideFromUser: true,
|
||||
env: {
|
||||
// Avoid making `git diff` interactive when called from copilot
|
||||
GIT_PAGER: 'cat',
|
||||
}
|
||||
};
|
||||
|
||||
if (typeof shellOrProfile === 'string') {
|
||||
config.executable = shellOrProfile;
|
||||
} else {
|
||||
config.executable = shellOrProfile.path;
|
||||
config.args = shellOrProfile.args;
|
||||
config.icon = shellOrProfile.icon ?? config.icon;
|
||||
config.color = shellOrProfile.color;
|
||||
config.env = {
|
||||
...config.env,
|
||||
...shellOrProfile.env
|
||||
};
|
||||
}
|
||||
|
||||
return this._terminalService.createTerminal({ config });
|
||||
}
|
||||
|
||||
private _waitForShellIntegration(
|
||||
|
||||
+59
-7
@@ -22,7 +22,7 @@ import { IConfigurationService } from '../../../../../../platform/configuration/
|
||||
import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IStorageService, StorageScope, StorageTarget } from '../../../../../../platform/storage/common/storage.js';
|
||||
import { TerminalCapability } from '../../../../../../platform/terminal/common/capabilities/capabilities.js';
|
||||
import { ITerminalLogService } from '../../../../../../platform/terminal/common/terminal.js';
|
||||
import { ITerminalLogService, ITerminalProfile } from '../../../../../../platform/terminal/common/terminal.js';
|
||||
import { IRemoteAgentService } from '../../../../../services/remote/common/remoteAgentService.js';
|
||||
import { TerminalToolConfirmationStorageKeys } from '../../../../chat/browser/chatContentParts/toolInvocationParts/chatTerminalToolConfirmationSubPart.js';
|
||||
import { openTerminalSettingsLinkCommandId } from '../../../../chat/browser/chatContentParts/toolInvocationParts/chatTerminalToolProgressPart.js';
|
||||
@@ -616,22 +616,74 @@ export class RunInTerminalTool extends Disposable implements IToolImpl {
|
||||
|
||||
// #region Terminal init
|
||||
|
||||
private async _getCopilotShell(): Promise<string> {
|
||||
protected async _getCopilotShellOrProfile(): Promise<string | ITerminalProfile> {
|
||||
const os = await this._osBackend;
|
||||
|
||||
// Check for chat agent terminal profile first
|
||||
const customChatAgentProfile = this._getChatTerminalProfile(os);
|
||||
if (customChatAgentProfile) {
|
||||
return customChatAgentProfile;
|
||||
}
|
||||
|
||||
// When setting is null, use the previous behavior
|
||||
const defaultShell = await this._terminalProfileResolverService.getDefaultShell({
|
||||
os: await this._osBackend,
|
||||
os,
|
||||
remoteAuthority: this._remoteAgentService.getConnection()?.remoteAuthority
|
||||
});
|
||||
|
||||
// Force pwsh over cmd as cmd doesn't have shell integration
|
||||
if (basename(defaultShell) === 'cmd.exe') {
|
||||
return 'C:\\WINDOWS\\System32\\WindowsPowerShell\\v1.0\\powershell.exe';
|
||||
}
|
||||
|
||||
return defaultShell;
|
||||
}
|
||||
|
||||
private async _getCopilotShell(): Promise<string> {
|
||||
const shellOrProfile = await this._getCopilotShellOrProfile();
|
||||
if (typeof shellOrProfile === 'string') {
|
||||
return shellOrProfile;
|
||||
}
|
||||
return shellOrProfile.path;
|
||||
}
|
||||
|
||||
private _getChatTerminalProfile(os: OperatingSystem): ITerminalProfile | undefined {
|
||||
let profileSetting: string;
|
||||
switch (os) {
|
||||
case OperatingSystem.Windows:
|
||||
profileSetting = TerminalChatAgentToolsSettingId.TerminalProfileWindows;
|
||||
break;
|
||||
case OperatingSystem.Macintosh:
|
||||
profileSetting = TerminalChatAgentToolsSettingId.TerminalProfileMacOs;
|
||||
break;
|
||||
case OperatingSystem.Linux:
|
||||
default:
|
||||
profileSetting = TerminalChatAgentToolsSettingId.TerminalProfileLinux;
|
||||
break;
|
||||
}
|
||||
|
||||
const profile = this._configurationService.getValue(profileSetting);
|
||||
if (this._isValidChatAgentTerminalProfile(profile)) {
|
||||
return profile;
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
private _isValidChatAgentTerminalProfile(profile: unknown): profile is ITerminalProfile {
|
||||
if (profile === null || profile === undefined || typeof profile !== 'object') {
|
||||
return false;
|
||||
}
|
||||
if ('path' in profile && typeof (profile as { path: unknown }).path === 'string') {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private async _initBackgroundTerminal(chatSessionId: string, termId: string, token: CancellationToken): Promise<IToolTerminal> {
|
||||
this._logService.debug(`RunInTerminalTool: Creating background terminal with ID=${termId}`);
|
||||
const shell = await this._getCopilotShell();
|
||||
const toolTerminal = await this._terminalToolCreator.createTerminal(shell, token);
|
||||
const shellOrProfile = await this._getCopilotShellOrProfile();
|
||||
const toolTerminal = await this._terminalToolCreator.createTerminal(shellOrProfile, token);
|
||||
this._registerInputListener(toolTerminal);
|
||||
this._sessionTerminalAssociations.set(chatSessionId, toolTerminal);
|
||||
if (token.isCancellationRequested) {
|
||||
@@ -649,8 +701,8 @@ export class RunInTerminalTool extends Disposable implements IToolImpl {
|
||||
this._terminalToolCreator.refreshShellIntegrationQuality(cachedTerminal);
|
||||
return cachedTerminal;
|
||||
}
|
||||
const shell = await this._getCopilotShell();
|
||||
const toolTerminal = await this._terminalToolCreator.createTerminal(shell, token);
|
||||
const shellOrProfile = await this._getCopilotShellOrProfile();
|
||||
const toolTerminal = await this._terminalToolCreator.createTerminal(shellOrProfile, token);
|
||||
this._registerInputListener(toolTerminal);
|
||||
this._sessionTerminalAssociations.set(chatSessionId, toolTerminal);
|
||||
if (token.isCancellationRequested) {
|
||||
|
||||
+68
@@ -8,6 +8,7 @@ import type { IJSONSchema } from '../../../../../base/common/jsonSchema.js';
|
||||
import { localize } from '../../../../../nls.js';
|
||||
import { type IConfigurationPropertySchema } from '../../../../../platform/configuration/common/configurationRegistry.js';
|
||||
import { TerminalSettingId } from '../../../../../platform/terminal/common/terminal.js';
|
||||
import { terminalProfileBaseProperties } from '../../../../../platform/terminal/common/terminalPlatformConfiguration.js';
|
||||
|
||||
export const enum TerminalChatAgentToolsSettingId {
|
||||
EnableAutoApprove = 'chat.tools.terminal.enableAutoApprove',
|
||||
@@ -15,6 +16,10 @@ export const enum TerminalChatAgentToolsSettingId {
|
||||
ShellIntegrationTimeout = 'chat.tools.terminal.shellIntegrationTimeout',
|
||||
AutoReplyToPrompts = 'chat.tools.terminal.autoReplyToPrompts',
|
||||
|
||||
TerminalProfileLinux = 'chat.tools.terminal.terminalProfile.linux',
|
||||
TerminalProfileMacOs = 'chat.tools.terminal.terminalProfile.osx',
|
||||
TerminalProfileWindows = 'chat.tools.terminal.terminalProfile.windows',
|
||||
|
||||
DeprecatedAutoApproveCompatible = 'chat.agent.terminal.autoApprove',
|
||||
DeprecatedAutoApprove1 = 'chat.agent.terminal.allowList',
|
||||
DeprecatedAutoApprove2 = 'chat.agent.terminal.denyList',
|
||||
@@ -41,6 +46,18 @@ const autoApproveBoolean: IJSONSchema = {
|
||||
description: localize('autoApprove.key', "The start of a command to match against. A regular expression can be provided by wrapping the string in `/` characters."),
|
||||
};
|
||||
|
||||
const terminalChatAgentProfileSchema: IJSONSchema = {
|
||||
type: 'object',
|
||||
required: ['path'],
|
||||
properties: {
|
||||
path: {
|
||||
description: localize('terminalChatAgentProfile.path', "A single path to a shell executable."),
|
||||
type: 'string',
|
||||
},
|
||||
...terminalProfileBaseProperties,
|
||||
}
|
||||
};
|
||||
|
||||
export const terminalChatAgentToolsConfiguration: IStringDictionary<IConfigurationPropertySchema> = {
|
||||
[TerminalChatAgentToolsSettingId.AutoApprove]: {
|
||||
markdownDescription: [
|
||||
@@ -312,6 +329,57 @@ export const terminalChatAgentToolsConfiguration: IStringDictionary<IConfigurati
|
||||
maximum: 60000,
|
||||
default: -1
|
||||
},
|
||||
[TerminalChatAgentToolsSettingId.TerminalProfileLinux]: {
|
||||
restricted: true,
|
||||
markdownDescription: localize('terminalChatAgentProfile.linux', "The terminal profile to use on Linux for chat agent's run in terminal tool."),
|
||||
type: ['object', 'null'],
|
||||
default: null,
|
||||
'anyOf': [
|
||||
{ type: 'null' },
|
||||
terminalChatAgentProfileSchema
|
||||
],
|
||||
defaultSnippets: [
|
||||
{
|
||||
body: {
|
||||
path: '${1}'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
[TerminalChatAgentToolsSettingId.TerminalProfileMacOs]: {
|
||||
restricted: true,
|
||||
markdownDescription: localize('terminalChatAgentProfile.osx', "The terminal profile to use on macOS for chat agent's run in terminal tool."),
|
||||
type: ['object', 'null'],
|
||||
default: null,
|
||||
'anyOf': [
|
||||
{ type: 'null' },
|
||||
terminalChatAgentProfileSchema
|
||||
],
|
||||
defaultSnippets: [
|
||||
{
|
||||
body: {
|
||||
path: '${1}'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
[TerminalChatAgentToolsSettingId.TerminalProfileWindows]: {
|
||||
restricted: true,
|
||||
markdownDescription: localize('terminalChatAgentProfile.windows', "The terminal profile to use on Windows for chat agent's run in terminal tool."),
|
||||
type: ['object', 'null'],
|
||||
default: null,
|
||||
'anyOf': [
|
||||
{ type: 'null' },
|
||||
terminalChatAgentProfileSchema
|
||||
],
|
||||
defaultSnippets: [
|
||||
{
|
||||
body: {
|
||||
path: '${1}'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
[TerminalChatAgentToolsSettingId.AutoReplyToPrompts]: {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
|
||||
+22
@@ -30,6 +30,9 @@ class TestRunInTerminalTool extends RunInTerminalTool {
|
||||
get commandLineAutoApprover() { return this._commandLineAutoApprover; }
|
||||
get sessionTerminalAssociations() { return this._sessionTerminalAssociations; }
|
||||
|
||||
getCopilotShellOrProfile() {
|
||||
return this._getCopilotShellOrProfile();
|
||||
}
|
||||
setBackendOs(os: OperatingSystem) {
|
||||
this._osBackend = Promise.resolve(os);
|
||||
}
|
||||
@@ -926,4 +929,23 @@ suite('RunInTerminalTool', () => {
|
||||
strictEqual(count(autoApproveInfo.value, 'echo'), 1);
|
||||
});
|
||||
});
|
||||
|
||||
suite('getCopilotShellOrProfile', () => {
|
||||
test('should return custom profile when configured', async () => {
|
||||
runInTerminalTool.setBackendOs(OperatingSystem.Windows);
|
||||
const customProfile = Object.freeze({ path: 'C:\\Windows\\System32\\powershell.exe', args: ['-NoProfile'] });
|
||||
setConfig(TerminalChatAgentToolsSettingId.TerminalProfileWindows, customProfile);
|
||||
|
||||
const result = await runInTerminalTool.getCopilotShellOrProfile();
|
||||
strictEqual(result, customProfile);
|
||||
});
|
||||
|
||||
test('should fall back to default shell when no custom profile is configured', async () => {
|
||||
runInTerminalTool.setBackendOs(OperatingSystem.Linux);
|
||||
setConfig(TerminalChatAgentToolsSettingId.TerminalProfileLinux, null);
|
||||
|
||||
const result = await runInTerminalTool.getCopilotShellOrProfile();
|
||||
strictEqual(result, 'pwsh'); // From the mock ITerminalProfileResolverService
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user