diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 51a34c77a57..d5ff867f89b 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -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", diff --git a/src/vs/platform/terminal/common/terminal.ts b/src/vs/platform/terminal/common/terminal.ts index 5e320a7843d..0a27cf70896 100644 --- a/src/vs/platform/terminal/common/terminal.ts +++ b/src/vs/platform/terminal/common/terminal.ts @@ -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 { diff --git a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts index 8ee6435e62a..5ad32acc292 100644 --- a/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts +++ b/src/vs/workbench/contrib/tasks/browser/terminalTaskSystem.ts @@ -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 { + 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 { 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)]; } diff --git a/src/vs/workbench/contrib/tasks/common/jsonSchema_v2.ts b/src/vs/workbench/contrib/tasks/common/jsonSchema_v2.ts index e698eb98988..d917ab2e032 100644 --- a/src/vs/workbench/contrib/tasks/common/jsonSchema_v2.ts +++ b/src/vs/workbench/contrib/tasks/common/jsonSchema_v2.ts @@ -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.') } } }; diff --git a/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts b/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts index 4a29e79efd9..2f149d1f004 100644 --- a/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts +++ b/src/vs/workbench/contrib/tasks/common/taskConfiguration.ts @@ -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[] = [{ property: 'echo' }, { property: 'reveal' }, { property: 'revealProblems' }, { property: 'focus' }, { property: 'panel' }, { property: 'showReuseMessage' }, { property: 'clear' }, { property: 'group' }, { property: 'close' }]; + const properties: IMetaData[] = [{ 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 | undefined { diff --git a/src/vs/workbench/contrib/tasks/common/tasks.ts b/src/vs/workbench/contrib/tasks/common/tasks.ts index a75a2571e0e..73cbc8ebf3d 100644 --- a/src/vs/workbench/contrib/tasks/common/tasks.ts +++ b/src/vs/workbench/contrib/tasks/common/tasks.ts @@ -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 }; } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 6f27a447dd8..72b84366b1d 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -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; diff --git a/src/vs/workbench/contrib/terminal/test/browser/terminalInstance.test.ts b/src/vs/workbench/contrib/terminal/test/browser/terminalInstance.test.ts index 21920b5f3e6..d9ea2fcf4f3 100644 --- a/src/vs/workbench/contrib/terminal/test/browser/terminalInstance.test.ts +++ b/src/vs/workbench/contrib/terminal/test/browser/terminalInstance.test.ts @@ -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', () => {