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",
"tasks": [
{
"label": "Testing Task Name",
"type": "shell",
"command": "sleep 3s && echo 123",
"presentation": {
"close": false,
"panel": "dedicated",
"showReuseMessage": false,
},
"problemMatcher": [],
},
{
"type": "npm",
"script": "watch-clientd",

View File

@@ -657,6 +657,12 @@ export interface IShellLaunchConfig {
* This allows extensions to control shell integration for terminals they create.
*/
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 {

View File

@@ -1162,7 +1162,7 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem {
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;
const isShellCommand = task.command.runtime === RuntimeType.Shell;
const needsFolderQualification = this._contextService.getWorkbenchState() === WorkbenchState.WORKSPACE;
@@ -1365,6 +1365,7 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem {
shellLaunchConfig.isFeatureTerminal = true;
shellLaunchConfig.useShellEnvironment = true;
shellLaunchConfig.tabActions = this._terminalTabActions;
shellLaunchConfig.preserveTaskName = presentationOptions.preserveTerminalName;
return shellLaunchConfig;
}
@@ -1495,14 +1496,15 @@ export class TerminalTaskSystem extends Disposable implements ITaskSystem {
}, 'Executing task: {0}', task._label), { excludeLeadingNewLine: true }) : undefined,
isFeatureTerminal: true,
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 {
const resolvedResult: { command: CommandString; args: CommandString[] } = await this._resolveCommandAndArgs(resolver, task.command);
command = resolvedResult.command;
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) {
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: {
type: 'boolean',
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.
*/
close?: boolean;
/**
* Controls whether to preserve the task name in the terminal after task completion.
*/
preserveTerminalName?: boolean;
}
export interface IRunOptionsConfig {
@@ -879,7 +884,7 @@ namespace CommandOptions {
namespace CommandConfiguration {
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 {
presentation?: IPresentationOptionsConfig;
@@ -895,6 +900,7 @@ namespace CommandConfiguration {
let clear: boolean;
let group: string | undefined;
let close: boolean | undefined;
let preserveTerminalName: boolean | undefined;
let hasProps = false;
if (Types.isBoolean(config.echoCommand)) {
echo = config.echoCommand;
@@ -933,12 +939,15 @@ namespace CommandConfiguration {
if (Types.isBoolean(presentation.close)) {
close = presentation.close;
}
if (Types.isBoolean(presentation.preserveTerminalName)) {
preserveTerminalName = presentation.preserveTerminalName;
}
hasProps = true;
}
if (!hasProps) {
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 {
@@ -951,7 +960,7 @@ namespace CommandConfiguration {
export function fillDefaults(value: Tasks.IPresentationOptions, context: IParseContext): Tasks.IPresentationOptions | undefined {
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 {

View File

@@ -279,11 +279,16 @@ export interface IPresentationOptions {
* Controls whether the terminal that the task runs in is closed when the task completes.
*/
close?: boolean;
/**
* Controls whether to preserve the task name in the terminal after task completion.
*/
preserveTerminalName?: boolean;
}
export namespace PresentationOptions {
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 {
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;
title = this._updateTitleProperties(title, eventSource);
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 { TerminalCapability } from '../../../../../platform/terminal/common/capabilities/capabilities.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 { IViewDescriptorService } from '../../../../common/views.js';
import { ITerminalConfigurationService, ITerminalInstance, ITerminalInstanceService } from '../../browser/terminal.js';
@@ -150,6 +150,92 @@ suite('Workbench - TerminalInstance', () => {
await new Promise(resolve => setTimeout(resolve, 100));
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', () => {
test('should return no message for exit code = undefined', () => {