From dd50d09337e4b2902e58f4cdb7a646f65eb3071e Mon Sep 17 00:00:00 2001 From: Connor Peet Date: Mon, 20 Apr 2026 14:17:05 -0700 Subject: [PATCH] Merge pull request #311473 from microsoft/connor4312/terminal-nointeract agentHost: set non-interactive env vars for tool-triggered terminals --- .../agentHost/node/agentHostTerminalManager.ts | 16 ++++++++++++++-- .../agentHost/node/copilot/copilotShellTools.ts | 2 +- .../test/node/copilotShellTools.test.ts | 7 ++++--- 3 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/vs/platform/agentHost/node/agentHostTerminalManager.ts b/src/vs/platform/agentHost/node/agentHostTerminalManager.ts index 3193302c672..f0a77734e40 100644 --- a/src/vs/platform/agentHost/node/agentHostTerminalManager.ts +++ b/src/vs/platform/agentHost/node/agentHostTerminalManager.ts @@ -38,7 +38,7 @@ export interface ICommandFinishedEvent { */ export interface IAgentHostTerminalManager { readonly _serviceBrand: undefined; - createTerminal(params: ICreateTerminalParams, options?: { shell?: string; preventShellHistory?: boolean }): Promise; + createTerminal(params: ICreateTerminalParams, options?: { shell?: string; preventShellHistory?: boolean; nonInteractive?: boolean }): Promise; writeInput(uri: string, data: string): void; onData(uri: string, cb: (data: string) => void): IDisposable; onExit(uri: string, cb: (exitCode: number) => void): IDisposable; @@ -172,7 +172,7 @@ export class AgentHostTerminalManager extends Disposable implements IAgentHostTe * Create a new terminal backed by node-pty. * Spawns the user's default shell. */ - async createTerminal(params: ICreateTerminalParams, options?: { shell?: string; preventShellHistory?: boolean }): Promise { + async createTerminal(params: ICreateTerminalParams, options?: { shell?: string; preventShellHistory?: boolean; nonInteractive?: boolean }): Promise { const uri = params.terminal; if (this._terminals.has(uri)) { throw new Error(`Terminal already exists: ${uri}`); @@ -199,6 +199,18 @@ export class AgentHostTerminalManager extends Disposable implements IAgentHostTe // prevents agent-executed commands from polluting the user's shell history. env['VSCODE_PREVENT_SHELL_HISTORY'] = '1'; } + if (options?.nonInteractive) { + // Suppress paging and interactive prompts so that tool-spawned + // terminals produce clean, machine-friendly output. An empty + // string disables paging in git, less, and most CLI tools and + // is safe on all platforms (unlike 'cat' which isn't on Windows PATH). + env['LC_ALL'] = 'C.UTF-8'; + env['PAGER'] = ''; + env['GIT_PAGER'] = ''; + env['GH_PAGER'] = ''; + env['GIT_TERMINAL_PROMPT'] = '0'; + env['DEBIAN_FRONTEND'] = 'noninteractive'; + } let shellArgs: string[] = []; const injection = await getShellIntegrationInjection( diff --git a/src/vs/platform/agentHost/node/copilot/copilotShellTools.ts b/src/vs/platform/agentHost/node/copilot/copilotShellTools.ts index c944e862b44..9282fbd2ef5 100644 --- a/src/vs/platform/agentHost/node/copilot/copilotShellTools.ts +++ b/src/vs/platform/agentHost/node/copilot/copilotShellTools.ts @@ -109,7 +109,7 @@ export class ShellManager { claim, name: shellDisplayName, cwd: cwd ?? this._workingDirectory?.fsPath, - }, { shell: getShellExecutable(shellType), preventShellHistory: true }); + }, { shell: getShellExecutable(shellType), preventShellHistory: true, nonInteractive: true }); const shell: IManagedShell = { id, terminalUri, shellType }; this._shells.set(id, shell); diff --git a/src/vs/platform/agentHost/test/node/copilotShellTools.test.ts b/src/vs/platform/agentHost/test/node/copilotShellTools.test.ts index 59386ce3b3f..ef56bf3e74f 100644 --- a/src/vs/platform/agentHost/test/node/copilotShellTools.test.ts +++ b/src/vs/platform/agentHost/test/node/copilotShellTools.test.ts @@ -19,9 +19,9 @@ import { ShellManager, prefixForHistorySuppression } from '../../node/copilot/co class TestAgentHostTerminalManager implements IAgentHostTerminalManager { declare readonly _serviceBrand: undefined; - readonly created: { params: ICreateTerminalParams; options?: { shell?: string; preventShellHistory?: boolean } }[] = []; + readonly created: { params: ICreateTerminalParams; options?: { shell?: string; preventShellHistory?: boolean; nonInteractive?: boolean } }[] = []; - async createTerminal(params: ICreateTerminalParams, options?: { shell?: string; preventShellHistory?: boolean }): Promise { + async createTerminal(params: ICreateTerminalParams, options?: { shell?: string; preventShellHistory?: boolean; nonInteractive?: boolean }): Promise { this.created.push({ params, options }); } writeInput(): void { } @@ -66,7 +66,7 @@ suite('CopilotShellTools', () => { ]); }); - test('opts every managed shell into shell-history suppression', async () => { + test('opts every managed shell into shell-history suppression and non-interactive mode', async () => { const terminalManager = new TestAgentHostTerminalManager(); const services = new ServiceCollection(); services.set(ILogService, new NullLogService()); @@ -79,6 +79,7 @@ suite('CopilotShellTools', () => { assert.strictEqual(terminalManager.created.length, 1); assert.strictEqual(terminalManager.created[0].options?.preventShellHistory, true); + assert.strictEqual(terminalManager.created[0].options?.nonInteractive, true); }); test('prefixForHistorySuppression prepends a space for POSIX shells, no-op for PowerShell', () => {