Add preserveTerminalName task presentation option to control terminal name preservation (#268350)

This commit is contained in:
Copilot
2025-09-25 15:51:18 -04:00
committed by GitHub
parent 1ff586146b
commit 14dd6cdb57
8 changed files with 139 additions and 8 deletions

11
.vscode/tasks.json vendored
View File

@@ -1,6 +1,17 @@
{ {
"version": "2.0.0", "version": "2.0.0",
"tasks": [ "tasks": [
{
"label": "Testing Task Name",
"type": "shell",
"command": "sleep 3s && echo 123",
"presentation": {
"close": false,
"panel": "dedicated",
"showReuseMessage": false,
},
"problemMatcher": [],
},
{ {
"type": "npm", "type": "npm",
"script": "watch-clientd", "script": "watch-clientd",

View File

@@ -657,6 +657,12 @@ export interface IShellLaunchConfig {
* This allows extensions to control shell integration for terminals they create. * This allows extensions to control shell integration for terminals they create.
*/ */
shellIntegrationNonce?: string; shellIntegrationNonce?: string;
/**
* For task terminals, controls whether to preserve the task name after task completion.
* When true, prevents process title changes from overriding the task name.
*/
preserveTaskName?: boolean;
} }
export interface ITerminalTabAction { export interface ITerminalTabAction {

View File

@@ -1162,7 +1162,7 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem {
return needsFolderQualification ? task.getQualifiedLabel() : (task.configurationProperties.name || ''); return needsFolderQualification ? task.getQualifiedLabel() : (task.configurationProperties.name || '');
} }
private async _createShellLaunchConfig(task: CustomTask | ContributedTask, workspaceFolder: IWorkspaceFolder | undefined, variableResolver: VariableResolver, platform: Platform.Platform, options: CommandOptions, command: CommandString, args: CommandString[], waitOnExit: WaitOnExitValue): Promise<IShellLaunchConfig | undefined> { private async _createShellLaunchConfig(task: CustomTask | ContributedTask, workspaceFolder: IWorkspaceFolder | undefined, variableResolver: VariableResolver, platform: Platform.Platform, options: CommandOptions, command: CommandString, args: CommandString[], waitOnExit: WaitOnExitValue, presentationOptions: IPresentationOptions): Promise<IShellLaunchConfig | undefined> {
let shellLaunchConfig: IShellLaunchConfig; let shellLaunchConfig: IShellLaunchConfig;
const isShellCommand = task.command.runtime === RuntimeType.Shell; const isShellCommand = task.command.runtime === RuntimeType.Shell;
const needsFolderQualification = this._contextService.getWorkbenchState() === WorkbenchState.WORKSPACE; const needsFolderQualification = this._contextService.getWorkbenchState() === WorkbenchState.WORKSPACE;
@@ -1365,6 +1365,7 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem {
shellLaunchConfig.isFeatureTerminal = true; shellLaunchConfig.isFeatureTerminal = true;
shellLaunchConfig.useShellEnvironment = true; shellLaunchConfig.useShellEnvironment = true;
shellLaunchConfig.tabActions = this._terminalTabActions; shellLaunchConfig.tabActions = this._terminalTabActions;
shellLaunchConfig.preserveTaskName = presentationOptions.preserveTerminalName;
return shellLaunchConfig; return shellLaunchConfig;
} }
@@ -1495,14 +1496,15 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem {
}, 'Executing task: {0}', task._label), { excludeLeadingNewLine: true }) : undefined, }, 'Executing task: {0}', task._label), { excludeLeadingNewLine: true }) : undefined,
isFeatureTerminal: true, isFeatureTerminal: true,
icon: task.configurationProperties.icon?.id ? ThemeIcon.fromId(task.configurationProperties.icon.id) : undefined, icon: task.configurationProperties.icon?.id ? ThemeIcon.fromId(task.configurationProperties.icon.id) : undefined,
color: task.configurationProperties.icon?.color || undefined color: task.configurationProperties.icon?.color || undefined,
preserveTaskName: presentationOptions.preserveTerminalName
}; };
} else { } else {
const resolvedResult: { command: CommandString; args: CommandString[] } = await this._resolveCommandAndArgs(resolver, task.command); const resolvedResult: { command: CommandString; args: CommandString[] } = await this._resolveCommandAndArgs(resolver, task.command);
command = resolvedResult.command; command = resolvedResult.command;
args = resolvedResult.args; args = resolvedResult.args;
this._currentTask.shellLaunchConfig = launchConfigs = await this._createShellLaunchConfig(task, workspaceFolder, resolver, platform, options, command, args, waitOnExit); this._currentTask.shellLaunchConfig = launchConfigs = await this._createShellLaunchConfig(task, workspaceFolder, resolver, platform, options, command, args, waitOnExit, presentationOptions);
if (launchConfigs === undefined) { if (launchConfigs === undefined) {
return [undefined, new TaskError(Severity.Error, nls.localize('TerminalTaskSystem', 'Can\'t execute a shell command on an UNC drive using cmd.exe.'), TaskErrors.UnknownError)]; return [undefined, new TaskError(Severity.Error, nls.localize('TerminalTaskSystem', 'Can\'t execute a shell command on an UNC drive using cmd.exe.'), TaskErrors.UnknownError)];
} }

View File

@@ -197,6 +197,11 @@ const presentation: IJSONSchema = {
close: { close: {
type: 'boolean', type: 'boolean',
description: nls.localize('JsonSchema.tasks.presentation.close', 'Controls whether the terminal the task runs in is closed when the task exits.') description: nls.localize('JsonSchema.tasks.presentation.close', 'Controls whether the terminal the task runs in is closed when the task exits.')
},
preserveTerminalName: {
type: 'boolean',
default: false,
description: nls.localize('JsonSchema.tasks.presentation.preserveTerminalName', 'Controls whether to preserve the task name in the terminal after task completion.')
} }
} }
}; };

View File

@@ -136,6 +136,11 @@ export interface IPresentationOptionsConfig {
* Note that if the terminal process exits with a non-zero exit code, it will not close. * Note that if the terminal process exits with a non-zero exit code, it will not close.
*/ */
close?: boolean; close?: boolean;
/**
* Controls whether to preserve the task name in the terminal after task completion.
*/
preserveTerminalName?: boolean;
} }
export interface IRunOptionsConfig { export interface IRunOptionsConfig {
@@ -879,7 +884,7 @@ namespace CommandOptions {
namespace CommandConfiguration { namespace CommandConfiguration {
export namespace PresentationOptions { export namespace PresentationOptions {
const properties: IMetaData<Tasks.IPresentationOptions, void>[] = [{ property: 'echo' }, { property: 'reveal' }, { property: 'revealProblems' }, { property: 'focus' }, { property: 'panel' }, { property: 'showReuseMessage' }, { property: 'clear' }, { property: 'group' }, { property: 'close' }]; const properties: IMetaData<Tasks.IPresentationOptions, void>[] = [{ property: 'echo' }, { property: 'reveal' }, { property: 'revealProblems' }, { property: 'focus' }, { property: 'panel' }, { property: 'showReuseMessage' }, { property: 'clear' }, { property: 'group' }, { property: 'close' }, { property: 'preserveTerminalName' }];
interface IPresentationOptionsShape extends ILegacyCommandProperties { interface IPresentationOptionsShape extends ILegacyCommandProperties {
presentation?: IPresentationOptionsConfig; presentation?: IPresentationOptionsConfig;
@@ -895,6 +900,7 @@ namespace CommandConfiguration {
let clear: boolean; let clear: boolean;
let group: string | undefined; let group: string | undefined;
let close: boolean | undefined; let close: boolean | undefined;
let preserveTerminalName: boolean | undefined;
let hasProps = false; let hasProps = false;
if (Types.isBoolean(config.echoCommand)) { if (Types.isBoolean(config.echoCommand)) {
echo = config.echoCommand; echo = config.echoCommand;
@@ -933,12 +939,15 @@ namespace CommandConfiguration {
if (Types.isBoolean(presentation.close)) { if (Types.isBoolean(presentation.close)) {
close = presentation.close; close = presentation.close;
} }
if (Types.isBoolean(presentation.preserveTerminalName)) {
preserveTerminalName = presentation.preserveTerminalName;
}
hasProps = true; hasProps = true;
} }
if (!hasProps) { if (!hasProps) {
return undefined; return undefined;
} }
return { echo: echo!, reveal: reveal!, revealProblems: revealProblems!, focus: focus!, panel: panel!, showReuseMessage: showReuseMessage!, clear: clear!, group, close: close }; return { echo: echo!, reveal: reveal!, revealProblems: revealProblems!, focus: focus!, panel: panel!, showReuseMessage: showReuseMessage!, clear: clear!, group, close: close, preserveTerminalName };
} }
export function assignProperties(target: Tasks.IPresentationOptions, source: Tasks.IPresentationOptions | undefined): Tasks.IPresentationOptions | undefined { export function assignProperties(target: Tasks.IPresentationOptions, source: Tasks.IPresentationOptions | undefined): Tasks.IPresentationOptions | undefined {
@@ -951,7 +960,7 @@ namespace CommandConfiguration {
export function fillDefaults(value: Tasks.IPresentationOptions, context: IParseContext): Tasks.IPresentationOptions | undefined { export function fillDefaults(value: Tasks.IPresentationOptions, context: IParseContext): Tasks.IPresentationOptions | undefined {
const defaultEcho = context.engine === Tasks.ExecutionEngine.Terminal ? true : false; const defaultEcho = context.engine === Tasks.ExecutionEngine.Terminal ? true : false;
return _fillDefaults(value, { echo: defaultEcho, reveal: Tasks.RevealKind.Always, revealProblems: Tasks.RevealProblemKind.Never, focus: false, panel: Tasks.PanelKind.Shared, showReuseMessage: true, clear: false }, properties, context); return _fillDefaults(value, { echo: defaultEcho, reveal: Tasks.RevealKind.Always, revealProblems: Tasks.RevealProblemKind.Never, focus: false, panel: Tasks.PanelKind.Shared, showReuseMessage: true, clear: false, preserveTerminalName: false }, properties, context);
} }
export function freeze(value: Tasks.IPresentationOptions): Readonly<Tasks.IPresentationOptions> | undefined { export function freeze(value: Tasks.IPresentationOptions): Readonly<Tasks.IPresentationOptions> | undefined {

View File

@@ -279,11 +279,16 @@ export interface IPresentationOptions {
* Controls whether the terminal that the task runs in is closed when the task completes. * Controls whether the terminal that the task runs in is closed when the task completes.
*/ */
close?: boolean; close?: boolean;
/**
* Controls whether to preserve the task name in the terminal after task completion.
*/
preserveTerminalName?: boolean;
} }
export namespace PresentationOptions { export namespace PresentationOptions {
export const defaults: IPresentationOptions = { export const defaults: IPresentationOptions = {
echo: true, reveal: RevealKind.Always, revealProblems: RevealProblemKind.Never, focus: false, panel: PanelKind.Shared, showReuseMessage: true, clear: false echo: true, reveal: RevealKind.Always, revealProblems: RevealProblemKind.Never, focus: false, panel: PanelKind.Shared, showReuseMessage: true, clear: false, preserveTerminalName: false
}; };
} }

View File

@@ -2256,6 +2256,13 @@ export class TerminalInstance extends Disposable implements ITerminalInstance {
} }
private _setTitle(title: string | undefined, eventSource: TitleEventSource): void { private _setTitle(title: string | undefined, eventSource: TitleEventSource): void {
if (this._shellLaunchConfig?.type === 'Task' &&
(this._shellLaunchConfig?.preserveTaskName || this._titleSource === TitleEventSource.Api) &&
eventSource === TitleEventSource.Process) {
// For task terminals with preserveTaskName enabled or with API-set titles, preserve the title even when the process title changes
return;
}
const reset = !title; const reset = !title;
title = this._updateTitleProperties(title, eventSource); title = this._updateTitleProperties(title, eventSource);
const titleChanged = title !== this._title; const titleChanged = title !== this._title;

View File

@@ -15,7 +15,7 @@ import { TestConfigurationService } from '../../../../../platform/configuration/
import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js'; import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js';
import { TerminalCapability } from '../../../../../platform/terminal/common/capabilities/capabilities.js'; import { TerminalCapability } from '../../../../../platform/terminal/common/capabilities/capabilities.js';
import { TerminalCapabilityStore } from '../../../../../platform/terminal/common/capabilities/terminalCapabilityStore.js'; import { TerminalCapabilityStore } from '../../../../../platform/terminal/common/capabilities/terminalCapabilityStore.js';
import { GeneralShellType, ITerminalChildProcess, ITerminalProfile } from '../../../../../platform/terminal/common/terminal.js'; import { GeneralShellType, ITerminalChildProcess, ITerminalProfile, TitleEventSource } from '../../../../../platform/terminal/common/terminal.js';
import { IWorkspaceFolder } from '../../../../../platform/workspace/common/workspace.js'; import { IWorkspaceFolder } from '../../../../../platform/workspace/common/workspace.js';
import { IViewDescriptorService } from '../../../../common/views.js'; import { IViewDescriptorService } from '../../../../common/views.js';
import { ITerminalConfigurationService, ITerminalInstance, ITerminalInstanceService } from '../../browser/terminal.js'; import { ITerminalConfigurationService, ITerminalInstance, ITerminalInstanceService } from '../../browser/terminal.js';
@@ -150,6 +150,92 @@ suite('Workbench - TerminalInstance', () => {
await new Promise(resolve => setTimeout(resolve, 100)); await new Promise(resolve => setTimeout(resolve, 100));
deepStrictEqual(terminalInstance.shellLaunchConfig.env, { TEST: 'TEST' }); deepStrictEqual(terminalInstance.shellLaunchConfig.env, { TEST: 'TEST' });
}); });
test('should preserve title for task terminals when preserveTaskName is enabled', async () => {
const instantiationService = workbenchInstantiationService({
configurationService: () => new TestConfigurationService({
files: {},
terminal: {
integrated: {
fontFamily: 'monospace',
scrollback: 1000,
fastScrollSensitivity: 2,
mouseWheelScrollSensitivity: 1,
unicodeVersion: '6',
shellIntegration: {
enabled: true
}
}
},
})
}, store);
instantiationService.set(ITerminalProfileResolverService, new MockTerminalProfileResolverService());
instantiationService.stub(IViewDescriptorService, new TestViewDescriptorService());
instantiationService.stub(IEnvironmentVariableService, store.add(instantiationService.createInstance(EnvironmentVariableService)));
instantiationService.stub(ITerminalInstanceService, store.add(new TestTerminalInstanceService()));
// Create a task terminal with type 'Task' and preserveTaskName enabled
const taskTerminal = store.add(instantiationService.createInstance(TerminalInstance, terminalShellTypeContextKey, {
type: 'Task',
name: 'Test Task Name',
preserveTaskName: true
}));
// Wait for initialization
await new Promise(resolve => setTimeout(resolve, 100));
// Simulate setting the title via API (as the task system would do)
await taskTerminal.rename('Test Task Name');
strictEqual(taskTerminal.title, 'Test Task Name');
// Simulate a process title change (which happens when task completes)
(taskTerminal as any)._setTitle('some-process-name', TitleEventSource.Process);
// Verify that the task name is preserved
strictEqual(taskTerminal.title, 'Test Task Name', 'Task terminal should preserve API-set title when preserveTaskName is enabled');
});
test('should allow process title changes for non-task terminals', async () => {
const instantiationService = workbenchInstantiationService({
configurationService: () => new TestConfigurationService({
files: {},
terminal: {
integrated: {
fontFamily: 'monospace',
scrollback: 1000,
fastScrollSensitivity: 2,
mouseWheelScrollSensitivity: 1,
unicodeVersion: '6',
shellIntegration: {
enabled: true
}
}
},
})
}, store);
instantiationService.set(ITerminalProfileResolverService, new MockTerminalProfileResolverService());
instantiationService.stub(IViewDescriptorService, new TestViewDescriptorService());
instantiationService.stub(IEnvironmentVariableService, store.add(instantiationService.createInstance(EnvironmentVariableService)));
instantiationService.stub(ITerminalInstanceService, store.add(new TestTerminalInstanceService()));
// Create a regular terminal (not a task terminal)
const regularTerminal = store.add(instantiationService.createInstance(TerminalInstance, terminalShellTypeContextKey, {
name: 'Regular Terminal'
}));
// Wait for initialization
await new Promise(resolve => setTimeout(resolve, 100));
// Simulate setting the title via API
await regularTerminal.rename('Regular Terminal');
strictEqual(regularTerminal.title, 'Regular Terminal');
// Simulate a process title change
(regularTerminal as any)._setTitle('bash', TitleEventSource.Process);
// Verify that the title was changed (regular terminals should allow process title changes)
strictEqual(regularTerminal.title, 'bash', 'Regular terminal should allow process title changes to override API-set title');
});
}); });
suite('parseExitResult', () => { suite('parseExitResult', () => {
test('should return no message for exit code = undefined', () => { test('should return no message for exit code = undefined', () => {