From a98ee7a6239b20cc1c74b27d5d620081e6c9e83e Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Thu, 19 Feb 2026 15:36:45 -0800 Subject: [PATCH 1/4] terminal: support tab title templates for extension terminals --- .../common/extensionsApiProposals.ts | 3 ++ src/vs/platform/terminal/common/terminal.ts | 10 +++++ .../api/browser/mainThreadTerminalService.ts | 6 ++- .../workbench/api/common/extHost.protocol.ts | 1 + .../api/common/extHostTerminalService.ts | 19 +++++++--- .../terminal/browser/terminalInstance.ts | 15 ++++++-- .../browser/terminalProfileQuickpick.ts | 1 + .../browser/terminalProfileService.ts | 6 ++- .../terminal/browser/terminalService.ts | 4 +- .../contrib/terminal/common/terminal.ts | 6 ++- .../test/browser/terminalInstance.test.ts | 6 +++ .../vscode.proposed.terminalTitle.d.ts | 37 +++++++++++++++++++ 12 files changed, 98 insertions(+), 16 deletions(-) create mode 100644 src/vscode-dts/vscode.proposed.terminalTitle.d.ts diff --git a/src/vs/platform/extensions/common/extensionsApiProposals.ts b/src/vs/platform/extensions/common/extensionsApiProposals.ts index c4cc744d7e4..1cbbd458e6d 100644 --- a/src/vs/platform/extensions/common/extensionsApiProposals.ts +++ b/src/vs/platform/extensions/common/extensionsApiProposals.ts @@ -449,6 +449,9 @@ const _allApiProposals = { terminalShellEnv: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalShellEnv.d.ts', }, + terminalTitle: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.terminalTitle.d.ts', + }, testObserver: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.testObserver.d.ts', }, diff --git a/src/vs/platform/terminal/common/terminal.ts b/src/vs/platform/terminal/common/terminal.ts index 18c7915e8f2..e061c47650b 100644 --- a/src/vs/platform/terminal/common/terminal.ts +++ b/src/vs/platform/terminal/common/terminal.ts @@ -671,6 +671,13 @@ export interface IShellLaunchConfig { * This allows extensions to control shell integration for terminals they create. */ shellIntegrationNonce?: string; + + /** + * A title template string that supports the same variables as the + * `terminal.integrated.tabs.title` setting. When set, this overrides the config-based + * title template for this terminal instance. + */ + title?: string; } export interface ITerminalTabAction { @@ -686,6 +693,7 @@ export interface ICreateContributedTerminalProfileOptions { color?: string; location?: TerminalLocation | { viewColumn: number; preserveState?: boolean } | { splitActiveTerminal: boolean }; cwd?: string | URI; + title?: string; } export enum TerminalLocation { @@ -713,6 +721,7 @@ export interface IShellLaunchConfigDto { isFeatureTerminal?: boolean; tabActions?: ITerminalTabAction[]; shellIntegrationEnvironmentReporting?: boolean; + title?: string; } /** @@ -962,6 +971,7 @@ export interface ITerminalProfileContribution { id: string; icon?: URI | { light: URI; dark: URI } | string; color?: string; + tabTitle?: string; } export interface IExtensionTerminalProfile extends ITerminalProfileContribution { diff --git a/src/vs/workbench/api/browser/mainThreadTerminalService.ts b/src/vs/workbench/api/browser/mainThreadTerminalService.ts index 8e85ad2cac5..a83feafd3ea 100644 --- a/src/vs/workbench/api/browser/mainThreadTerminalService.ts +++ b/src/vs/workbench/api/browser/mainThreadTerminalService.ts @@ -173,7 +173,8 @@ export class MainThreadTerminalService extends Disposable implements MainThreadT isExtensionOwnedTerminal: launchConfig.isExtensionOwnedTerminal, useShellEnvironment: launchConfig.useShellEnvironment, isTransient: launchConfig.isTransient, - shellIntegrationNonce: launchConfig.shellIntegrationNonce + shellIntegrationNonce: launchConfig.shellIntegrationNonce, + title: launchConfig.title, }; const terminal = Promises.withAsyncBody(async r => { const terminal = await this._terminalService.createTerminal({ @@ -415,7 +416,8 @@ export class MainThreadTerminalService extends Disposable implements MainThreadT cwd: terminalInstance.shellLaunchConfig.cwd, env: terminalInstance.shellLaunchConfig.env, hideFromUser: terminalInstance.shellLaunchConfig.hideFromUser, - tabActions: terminalInstance.shellLaunchConfig.tabActions + tabActions: terminalInstance.shellLaunchConfig.tabActions, + title: terminalInstance.shellLaunchConfig.title }; this._proxy.$acceptTerminalOpened(terminalInstance.instanceId, extHostTerminalId, terminalInstance.title, shellLaunchConfigDto); } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index d10a973d8bc..7e5b2e972b8 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -625,6 +625,7 @@ export interface TerminalLaunchConfig { location?: TerminalLocation | { viewColumn: number; preserveFocus?: boolean } | { parentTerminal: ExtHostTerminalIdentifier } | { splitActiveTerminal: boolean }; isTransient?: boolean; shellIntegrationNonce?: string; + title?: string; } diff --git a/src/vs/workbench/api/common/extHostTerminalService.ts b/src/vs/workbench/api/common/extHostTerminalService.ts index 225ed9c2231..f0469e81929 100644 --- a/src/vs/workbench/api/common/extHostTerminalService.ts +++ b/src/vs/workbench/api/common/extHostTerminalService.ts @@ -193,11 +193,12 @@ export class ExtHostTerminal extends Disposable { location: internalOptions?.location || this._serializeParentTerminal(options.location, internalOptions?.resolvedExtHostIdentifier), isTransient: options.isTransient ?? undefined, shellIntegrationNonce: options.shellIntegrationNonce ?? undefined, + title: options.title ?? undefined, }); } - public async createExtensionTerminal(location?: TerminalLocation | vscode.TerminalEditorLocationOptions | vscode.TerminalSplitLocationOptions, internalOptions?: ITerminalInternalOptions, parentTerminal?: ExtHostTerminalIdentifier, iconPath?: TerminalIcon, color?: ThemeColor, shellIntegrationNonce?: string): Promise { + public async createExtensionTerminal(location?: TerminalLocation | vscode.TerminalEditorLocationOptions | vscode.TerminalSplitLocationOptions, internalOptions?: ITerminalInternalOptions, parentTerminal?: ExtHostTerminalIdentifier, iconPath?: TerminalIcon, color?: ThemeColor, shellIntegrationNonce?: string, title?: string): Promise { if (typeof this._id !== 'string') { throw new Error('Terminal has already been created'); } @@ -209,6 +210,7 @@ export class ExtHostTerminal extends Disposable { location: internalOptions?.location || this._serializeParentTerminal(location, parentTerminal), isTransient: true, shellIntegrationNonce: shellIntegrationNonce ?? undefined, + title: title ?? undefined, }); // At this point, the id has been set via `$acceptTerminalOpened` if (typeof this._id === 'string') { @@ -513,7 +515,7 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I public createExtensionTerminal(options: vscode.ExtensionTerminalOptions, internalOptions?: ITerminalInternalOptions): vscode.Terminal { const terminal = new ExtHostTerminal(this._proxy, generateUuid(), options, options.name); const p = new ExtHostPseudoterminal(options.pty); - terminal.createExtensionTerminal(options.location, internalOptions, this._serializeParentTerminal(options, internalOptions).resolvedExtHostIdentifier, asTerminalIcon(options.iconPath), asTerminalColor(options.color), options.shellIntegrationNonce).then(id => { + terminal.createExtensionTerminal(options.location, internalOptions, this._serializeParentTerminal(options, internalOptions).resolvedExtHostIdentifier, asTerminalIcon(options.iconPath), asTerminalColor(options.color), options.shellIntegrationNonce, options.title).then(id => { const disposable = this._setupExtHostProcessListeners(id, p); this._terminalProcessDisposables[id] = disposable; }); @@ -634,7 +636,8 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I shellArgs: shellLaunchConfigDto.args, cwd: typeof shellLaunchConfigDto.cwd === 'string' ? shellLaunchConfigDto.cwd : URI.revive(shellLaunchConfigDto.cwd), env: shellLaunchConfigDto.env, - hideFromUser: shellLaunchConfigDto.hideFromUser + hideFromUser: shellLaunchConfigDto.hideFromUser, + title: shellLaunchConfigDto.title }; const terminal = new ExtHostTerminal(this._proxy, id, creationOptions, name); this._terminals.push(terminal); @@ -854,11 +857,15 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I throw new Error(`No terminal profile options provided for id "${id}"`); } - if (hasKey(profile.options, { pty: true })) { - this.createExtensionTerminal(profile.options, options); + const profileOptions = options.title && !profile.options.title + ? { ...profile.options, title: options.title } + : profile.options; + + if (hasKey(profileOptions, { pty: true })) { + this.createExtensionTerminal(profileOptions, options); return; } - this.createTerminalFromOptions(profile.options, options); + this.createTerminalFromOptions(profileOptions, options); } public registerLinkProvider(provider: vscode.TerminalLinkProvider): vscode.Disposable { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index a9fd028211a..9e405b875d1 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -518,7 +518,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { // When a custom pty is used set the name immediately so it gets passed over to the exthost // and is available when Pseudoterminal.open fires. - if (this.shellLaunchConfig.customPtyImplementation) { + if (this.shellLaunchConfig.customPtyImplementation && !this._shellLaunchConfig.title) { this._setTitle(this._shellLaunchConfig.name, TitleEventSource.Api); } @@ -1471,7 +1471,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } })); } - if (this._shellLaunchConfig.name) { + if (this._shellLaunchConfig.name && !this._shellLaunchConfig.title) { this._setTitle(this._shellLaunchConfig.name, TitleEventSource.Api); } else { // Listen to xterm.js' sequence title change event, trigger this async to ensure @@ -1483,7 +1483,13 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } }); }); - this._setTitle(this._shellLaunchConfig.executable, TitleEventSource.Process); + // When a title template is provided, use the name as the initial process name + // so it can be referenced via ${process} in the template + if (this._shellLaunchConfig.title && this._shellLaunchConfig.name) { + this._setTitle(this._shellLaunchConfig.name, TitleEventSource.Process); + } else { + this._setTitle(this._shellLaunchConfig.executable, TitleEventSource.Process); + } } })); this._register(processManager.onProcessExit(exitCode => this._onProcessExit(exitCode))); @@ -2633,7 +2639,8 @@ export class TerminalLabelComputer extends Disposable { } refreshLabel(instance: Pick, reset?: boolean): void { - this._title = this.computeLabel(instance, this._terminalConfigurationService.config.tabs.title, TerminalLabelType.Title, reset); + const titleTemplate = instance.shellLaunchConfig.title ?? this._terminalConfigurationService.config.tabs.title; + this._title = this.computeLabel(instance, titleTemplate, TerminalLabelType.Title, reset); this._description = this.computeLabel(instance, this._terminalConfigurationService.config.tabs.description, TerminalLabelType.Description); if (this._title !== instance.title || this._description !== instance.description || reset) { this._onDidChangeLabel.fire({ title: this._title, description: this._description }); diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProfileQuickpick.ts b/src/vs/workbench/contrib/terminal/browser/terminalProfileQuickpick.ts index 1f06f04d3f0..6c7ab9e8457 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProfileQuickpick.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProfileQuickpick.ts @@ -82,6 +82,7 @@ export class TerminalProfileQuickpick { extensionIdentifier: result.profile.extensionIdentifier, id: result.profile.id, title: result.profile.title, + tabTitle: result.profile.tabTitle, options: { icon: result.profile.icon, color: result.profile.color, diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProfileService.ts b/src/vs/workbench/contrib/terminal/browser/terminalProfileService.ts index 0cbef05b922..7415ad5794f 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProfileService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProfileService.ts @@ -233,7 +233,8 @@ export class TerminalProfileService extends Disposable implements ITerminalProfi icon: args.options.icon, id: args.id, title: args.title, - color: args.options.color + color: args.options.color, + tabTitle: args.tabTitle }; (profilesConfig as { [key: string]: ITerminalProfileObject })[args.title] = newProfile; @@ -271,5 +272,6 @@ function contributedProfilesEqual(one: IExtensionTerminalProfile, other: IExtens one.color === other.color && one.icon === other.icon && one.id === other.id && - one.title === other.title; + one.title === other.title && + one.tabTitle === other.tabTitle; } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index a01da1a710c..ddfae73b743 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -256,7 +256,8 @@ export class TerminalService extends Disposable implements ITerminalService { await this.createContributedTerminalProfile(result.config.extensionIdentifier, result.config.id, { icon: result.config.options?.icon, color: result.config.options?.color, - location: !!(keyMods?.alt && activeInstance) ? { splitActiveTerminal: true } : defaultLocation + location: !!(keyMods?.alt && activeInstance) ? { splitActiveTerminal: true } : defaultLocation, + title: result.config.tabTitle, }); return; } else if (result.config && hasKey(result.config, { profileName: true })) { @@ -1032,6 +1033,7 @@ export class TerminalService extends Disposable implements ITerminalService { color: contributedProfile.color, location, cwd: shellLaunchConfig.cwd, + title: contributedProfile.tabTitle, }); const instanceHost = resolvedLocation === TerminalLocation.Editor ? this._terminalEditorService : this._terminalGroupService; // TODO@meganrogge: This returns undefined in the remote & web smoke tests but the function diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index 12fa676c314..f442f17698d 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -67,7 +67,7 @@ export interface ITerminalProfileResolverService { export const ShellIntegrationExitCode = 633; export interface IRegisterContributedProfileArgs { - extensionIdentifier: string; id: string; title: string; options: ICreateContributedTerminalProfileOptions; + extensionIdentifier: string; id: string; title: string; options: ICreateContributedTerminalProfileOptions; tabTitle?: string; } export const ITerminalProfileService = createDecorator('terminalProfileService'); @@ -693,6 +693,10 @@ export const terminalContributionsDescriptor: IExtensionPointDescriptor { strictEqual(terminalLabelComputer.title, 'my-title'); strictEqual(terminalLabelComputer.description, 'folder'); }); + test('should use shellLaunchConfig.title as template when set', () => { + const terminalLabelComputer = createLabelComputer({ terminal: { integrated: { tabs: { separator: ' - ', title: '${process}', description: '${cwd}' } } } }); + terminalLabelComputer.refreshLabel(createInstance({ capabilities, sequence: 'my-sequence', processName: 'zsh', shellLaunchConfig: { title: '${sequence}' } })); + strictEqual(terminalLabelComputer.title, 'my-sequence'); + strictEqual(terminalLabelComputer.description, 'cwd'); + }); test('should provide cwdFolder for all cwds only when in multi-root', () => { const terminalLabelComputer = createLabelComputer({ terminal: { integrated: { tabs: { separator: ' ~ ', title: '${process}${separator}${cwdFolder}', description: '${cwdFolder}' } } } }); terminalLabelComputer.refreshLabel(createInstance({ capabilities, processName: 'process', workspaceFolder: { uri: URI.from({ scheme: Schemas.file, path: ROOT_1 }) } as IWorkspaceFolder, cwd: ROOT_1 })); diff --git a/src/vscode-dts/vscode.proposed.terminalTitle.d.ts b/src/vscode-dts/vscode.proposed.terminalTitle.d.ts new file mode 100644 index 00000000000..584b2d29cf4 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.terminalTitle.d.ts @@ -0,0 +1,37 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + // https://github.com/microsoft/vscode/issues/xyz + + export interface TerminalOptions { + /** + * A title template string for the terminal tab. This supports the same variables as the + * `terminal.integrated.tabs.title` setting, such as `${sequence}`, `${process}`, `${cwd}`, + * `${cwdFolder}`, `${workspaceFolderName}`, etc. When set, this overrides the default title + * behavior (which uses the `name` as a static title) and instead uses the template for + * dynamic title resolution. + * + * For example, setting `title` to `"${sequence}"` allows the terminal's escape sequence + * title to be used as the tab title. + */ + title?: string; + } + + export interface ExtensionTerminalOptions { + /** + * A title template string for the terminal tab. This supports the same variables as the + * `terminal.integrated.tabs.title` setting, such as `${sequence}`, `${process}`, `${cwd}`, + * `${cwdFolder}`, `${workspaceFolderName}`, etc. When set, this overrides the default title + * behavior (which uses the `name` as a static title) and instead uses the template for + * dynamic title resolution. + * + * For example, setting `title` to `"${sequence}"` allows the terminal's escape sequence + * title to be used as the tab title. + */ + title?: string; + } +} From 1b8971bb128b2c83c5ee155d8c42b00b7742407d Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Thu, 19 Feb 2026 15:48:44 -0800 Subject: [PATCH 2/4] terminal: omit undefined tabTitle from profile quick pick --- .../browser/terminalProfileQuickpick.ts | 35 +++++++++++++------ 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProfileQuickpick.ts b/src/vs/workbench/contrib/terminal/browser/terminalProfileQuickpick.ts index 6c7ab9e8457..569f84b4357 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProfileQuickpick.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProfileQuickpick.ts @@ -77,17 +77,29 @@ export class TerminalProfileQuickpick { await this._configurationService.updateValue(defaultProfileKey, result.profileName, ConfigurationTarget.USER); } else if (type === 'createInstance') { if (hasKey(result.profile, { id: true })) { + const config: { + extensionIdentifier: string; + id: string; + title: string; + tabTitle?: string; + options: { + icon: IExtensionTerminalProfile['icon']; + color: IExtensionTerminalProfile['color']; + }; + } = { + extensionIdentifier: result.profile.extensionIdentifier, + id: result.profile.id, + title: result.profile.title, + options: { + icon: result.profile.icon, + color: result.profile.color, + } + }; + if (result.profile.tabTitle !== undefined) { + config.tabTitle = result.profile.tabTitle; + } return { - config: { - extensionIdentifier: result.profile.extensionIdentifier, - id: result.profile.id, - title: result.profile.title, - tabTitle: result.profile.tabTitle, - options: { - icon: result.profile.icon, - color: result.profile.color, - } - }, + config, keyMods: result.keyMods }; } else { @@ -178,7 +190,8 @@ export class TerminalProfileQuickpick { title: contributed.title, icon: contributed.icon, id: contributed.id, - color: contributed.color + color: contributed.color, + tabTitle: contributed.tabTitle }, profileName: contributed.title, iconClasses From 746774994933c275c09ec208ea8fb4cd28afe014 Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Fri, 20 Feb 2026 13:47:17 -0800 Subject: [PATCH 3/4] refactor: rename terminal title properties to titleTemplate for consistency --- src/vs/platform/terminal/common/terminal.ts | 8 ++++---- .../api/browser/mainThreadTerminalService.ts | 4 ++-- src/vs/workbench/api/common/extHost.protocol.ts | 2 +- .../workbench/api/common/extHostTerminalService.ts | 14 +++++++------- .../contrib/terminal/browser/terminalInstance.ts | 8 ++++---- .../terminal/browser/terminalProfileQuickpick.ts | 8 ++++---- .../terminal/browser/terminalProfileService.ts | 4 ++-- .../contrib/terminal/browser/terminalService.ts | 4 ++-- .../workbench/contrib/terminal/common/terminal.ts | 6 +++--- .../terminal/test/browser/terminalInstance.test.ts | 4 ++-- src/vscode-dts/vscode.proposed.terminalTitle.d.ts | 8 ++++---- 11 files changed, 35 insertions(+), 35 deletions(-) diff --git a/src/vs/platform/terminal/common/terminal.ts b/src/vs/platform/terminal/common/terminal.ts index e061c47650b..dc7c596c346 100644 --- a/src/vs/platform/terminal/common/terminal.ts +++ b/src/vs/platform/terminal/common/terminal.ts @@ -677,7 +677,7 @@ export interface IShellLaunchConfig { * `terminal.integrated.tabs.title` setting. When set, this overrides the config-based * title template for this terminal instance. */ - title?: string; + titleTemplate?: string; } export interface ITerminalTabAction { @@ -693,7 +693,7 @@ export interface ICreateContributedTerminalProfileOptions { color?: string; location?: TerminalLocation | { viewColumn: number; preserveState?: boolean } | { splitActiveTerminal: boolean }; cwd?: string | URI; - title?: string; + titleTemplate?: string; } export enum TerminalLocation { @@ -721,7 +721,7 @@ export interface IShellLaunchConfigDto { isFeatureTerminal?: boolean; tabActions?: ITerminalTabAction[]; shellIntegrationEnvironmentReporting?: boolean; - title?: string; + titleTemplate?: string; } /** @@ -971,7 +971,7 @@ export interface ITerminalProfileContribution { id: string; icon?: URI | { light: URI; dark: URI } | string; color?: string; - tabTitle?: string; + titleTemplate?: string; } export interface IExtensionTerminalProfile extends ITerminalProfileContribution { diff --git a/src/vs/workbench/api/browser/mainThreadTerminalService.ts b/src/vs/workbench/api/browser/mainThreadTerminalService.ts index a83feafd3ea..ccf282f9cc8 100644 --- a/src/vs/workbench/api/browser/mainThreadTerminalService.ts +++ b/src/vs/workbench/api/browser/mainThreadTerminalService.ts @@ -174,7 +174,7 @@ export class MainThreadTerminalService extends Disposable implements MainThreadT useShellEnvironment: launchConfig.useShellEnvironment, isTransient: launchConfig.isTransient, shellIntegrationNonce: launchConfig.shellIntegrationNonce, - title: launchConfig.title, + titleTemplate: launchConfig.titleTemplate, }; const terminal = Promises.withAsyncBody(async r => { const terminal = await this._terminalService.createTerminal({ @@ -417,7 +417,7 @@ export class MainThreadTerminalService extends Disposable implements MainThreadT env: terminalInstance.shellLaunchConfig.env, hideFromUser: terminalInstance.shellLaunchConfig.hideFromUser, tabActions: terminalInstance.shellLaunchConfig.tabActions, - title: terminalInstance.shellLaunchConfig.title + titleTemplate: terminalInstance.shellLaunchConfig.titleTemplate }; this._proxy.$acceptTerminalOpened(terminalInstance.instanceId, extHostTerminalId, terminalInstance.title, shellLaunchConfigDto); } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 7e5b2e972b8..5ddc2106e84 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -625,7 +625,7 @@ export interface TerminalLaunchConfig { location?: TerminalLocation | { viewColumn: number; preserveFocus?: boolean } | { parentTerminal: ExtHostTerminalIdentifier } | { splitActiveTerminal: boolean }; isTransient?: boolean; shellIntegrationNonce?: string; - title?: string; + titleTemplate?: string; } diff --git a/src/vs/workbench/api/common/extHostTerminalService.ts b/src/vs/workbench/api/common/extHostTerminalService.ts index f0469e81929..4dcec3f6681 100644 --- a/src/vs/workbench/api/common/extHostTerminalService.ts +++ b/src/vs/workbench/api/common/extHostTerminalService.ts @@ -193,12 +193,12 @@ export class ExtHostTerminal extends Disposable { location: internalOptions?.location || this._serializeParentTerminal(options.location, internalOptions?.resolvedExtHostIdentifier), isTransient: options.isTransient ?? undefined, shellIntegrationNonce: options.shellIntegrationNonce ?? undefined, - title: options.title ?? undefined, + titleTemplate: options.titleTemplate ?? undefined, }); } - public async createExtensionTerminal(location?: TerminalLocation | vscode.TerminalEditorLocationOptions | vscode.TerminalSplitLocationOptions, internalOptions?: ITerminalInternalOptions, parentTerminal?: ExtHostTerminalIdentifier, iconPath?: TerminalIcon, color?: ThemeColor, shellIntegrationNonce?: string, title?: string): Promise { + public async createExtensionTerminal(location?: TerminalLocation | vscode.TerminalEditorLocationOptions | vscode.TerminalSplitLocationOptions, internalOptions?: ITerminalInternalOptions, parentTerminal?: ExtHostTerminalIdentifier, iconPath?: TerminalIcon, color?: ThemeColor, shellIntegrationNonce?: string, titleTemplate?: string): Promise { if (typeof this._id !== 'string') { throw new Error('Terminal has already been created'); } @@ -210,7 +210,7 @@ export class ExtHostTerminal extends Disposable { location: internalOptions?.location || this._serializeParentTerminal(location, parentTerminal), isTransient: true, shellIntegrationNonce: shellIntegrationNonce ?? undefined, - title: title ?? undefined, + titleTemplate: titleTemplate ?? undefined, }); // At this point, the id has been set via `$acceptTerminalOpened` if (typeof this._id === 'string') { @@ -515,7 +515,7 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I public createExtensionTerminal(options: vscode.ExtensionTerminalOptions, internalOptions?: ITerminalInternalOptions): vscode.Terminal { const terminal = new ExtHostTerminal(this._proxy, generateUuid(), options, options.name); const p = new ExtHostPseudoterminal(options.pty); - terminal.createExtensionTerminal(options.location, internalOptions, this._serializeParentTerminal(options, internalOptions).resolvedExtHostIdentifier, asTerminalIcon(options.iconPath), asTerminalColor(options.color), options.shellIntegrationNonce, options.title).then(id => { + terminal.createExtensionTerminal(options.location, internalOptions, this._serializeParentTerminal(options, internalOptions).resolvedExtHostIdentifier, asTerminalIcon(options.iconPath), asTerminalColor(options.color), options.shellIntegrationNonce, options.titleTemplate).then(id => { const disposable = this._setupExtHostProcessListeners(id, p); this._terminalProcessDisposables[id] = disposable; }); @@ -637,7 +637,7 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I cwd: typeof shellLaunchConfigDto.cwd === 'string' ? shellLaunchConfigDto.cwd : URI.revive(shellLaunchConfigDto.cwd), env: shellLaunchConfigDto.env, hideFromUser: shellLaunchConfigDto.hideFromUser, - title: shellLaunchConfigDto.title + titleTemplate: shellLaunchConfigDto.titleTemplate }; const terminal = new ExtHostTerminal(this._proxy, id, creationOptions, name); this._terminals.push(terminal); @@ -857,8 +857,8 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I throw new Error(`No terminal profile options provided for id "${id}"`); } - const profileOptions = options.title && !profile.options.title - ? { ...profile.options, title: options.title } + const profileOptions = options.titleTemplate && !profile.options.titleTemplate + ? { ...profile.options, titleTemplate: options.titleTemplate } : profile.options; if (hasKey(profileOptions, { pty: true })) { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts index 9e405b875d1..f78a087fbd8 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalInstance.ts @@ -518,7 +518,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { // When a custom pty is used set the name immediately so it gets passed over to the exthost // and is available when Pseudoterminal.open fires. - if (this.shellLaunchConfig.customPtyImplementation && !this._shellLaunchConfig.title) { + if (this.shellLaunchConfig.customPtyImplementation && !this._shellLaunchConfig.titleTemplate) { this._setTitle(this._shellLaunchConfig.name, TitleEventSource.Api); } @@ -1471,7 +1471,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { } })); } - if (this._shellLaunchConfig.name && !this._shellLaunchConfig.title) { + if (this._shellLaunchConfig.name && !this._shellLaunchConfig.titleTemplate) { this._setTitle(this._shellLaunchConfig.name, TitleEventSource.Api); } else { // Listen to xterm.js' sequence title change event, trigger this async to ensure @@ -1485,7 +1485,7 @@ export class TerminalInstance extends Disposable implements ITerminalInstance { }); // When a title template is provided, use the name as the initial process name // so it can be referenced via ${process} in the template - if (this._shellLaunchConfig.title && this._shellLaunchConfig.name) { + if (this._shellLaunchConfig.titleTemplate && this._shellLaunchConfig.name) { this._setTitle(this._shellLaunchConfig.name, TitleEventSource.Process); } else { this._setTitle(this._shellLaunchConfig.executable, TitleEventSource.Process); @@ -2639,7 +2639,7 @@ export class TerminalLabelComputer extends Disposable { } refreshLabel(instance: Pick, reset?: boolean): void { - const titleTemplate = instance.shellLaunchConfig.title ?? this._terminalConfigurationService.config.tabs.title; + const titleTemplate = instance.shellLaunchConfig.titleTemplate ?? this._terminalConfigurationService.config.tabs.title; this._title = this.computeLabel(instance, titleTemplate, TerminalLabelType.Title, reset); this._description = this.computeLabel(instance, this._terminalConfigurationService.config.tabs.description, TerminalLabelType.Description); if (this._title !== instance.title || this._description !== instance.description || reset) { diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProfileQuickpick.ts b/src/vs/workbench/contrib/terminal/browser/terminalProfileQuickpick.ts index 569f84b4357..28318f5403d 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProfileQuickpick.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProfileQuickpick.ts @@ -81,7 +81,7 @@ export class TerminalProfileQuickpick { extensionIdentifier: string; id: string; title: string; - tabTitle?: string; + titleTemplate?: string; options: { icon: IExtensionTerminalProfile['icon']; color: IExtensionTerminalProfile['color']; @@ -95,8 +95,8 @@ export class TerminalProfileQuickpick { color: result.profile.color, } }; - if (result.profile.tabTitle !== undefined) { - config.tabTitle = result.profile.tabTitle; + if (result.profile.titleTemplate !== undefined) { + config.titleTemplate = result.profile.titleTemplate; } return { config, @@ -191,7 +191,7 @@ export class TerminalProfileQuickpick { icon: contributed.icon, id: contributed.id, color: contributed.color, - tabTitle: contributed.tabTitle + titleTemplate: contributed.titleTemplate }, profileName: contributed.title, iconClasses diff --git a/src/vs/workbench/contrib/terminal/browser/terminalProfileService.ts b/src/vs/workbench/contrib/terminal/browser/terminalProfileService.ts index 7415ad5794f..598715341de 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalProfileService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalProfileService.ts @@ -234,7 +234,7 @@ export class TerminalProfileService extends Disposable implements ITerminalProfi id: args.id, title: args.title, color: args.options.color, - tabTitle: args.tabTitle + titleTemplate: args.titleTemplate }; (profilesConfig as { [key: string]: ITerminalProfileObject })[args.title] = newProfile; @@ -273,5 +273,5 @@ function contributedProfilesEqual(one: IExtensionTerminalProfile, other: IExtens one.icon === other.icon && one.id === other.id && one.title === other.title && - one.tabTitle === other.tabTitle; + one.titleTemplate === other.titleTemplate; } diff --git a/src/vs/workbench/contrib/terminal/browser/terminalService.ts b/src/vs/workbench/contrib/terminal/browser/terminalService.ts index ddfae73b743..6107058afad 100644 --- a/src/vs/workbench/contrib/terminal/browser/terminalService.ts +++ b/src/vs/workbench/contrib/terminal/browser/terminalService.ts @@ -257,7 +257,7 @@ export class TerminalService extends Disposable implements ITerminalService { icon: result.config.options?.icon, color: result.config.options?.color, location: !!(keyMods?.alt && activeInstance) ? { splitActiveTerminal: true } : defaultLocation, - title: result.config.tabTitle, + titleTemplate: result.config.titleTemplate, }); return; } else if (result.config && hasKey(result.config, { profileName: true })) { @@ -1033,7 +1033,7 @@ export class TerminalService extends Disposable implements ITerminalService { color: contributedProfile.color, location, cwd: shellLaunchConfig.cwd, - title: contributedProfile.tabTitle, + titleTemplate: contributedProfile.titleTemplate, }); const instanceHost = resolvedLocation === TerminalLocation.Editor ? this._terminalEditorService : this._terminalGroupService; // TODO@meganrogge: This returns undefined in the remote & web smoke tests but the function diff --git a/src/vs/workbench/contrib/terminal/common/terminal.ts b/src/vs/workbench/contrib/terminal/common/terminal.ts index f442f17698d..c5951189a80 100644 --- a/src/vs/workbench/contrib/terminal/common/terminal.ts +++ b/src/vs/workbench/contrib/terminal/common/terminal.ts @@ -67,7 +67,7 @@ export interface ITerminalProfileResolverService { export const ShellIntegrationExitCode = 633; export interface IRegisterContributedProfileArgs { - extensionIdentifier: string; id: string; title: string; options: ICreateContributedTerminalProfileOptions; tabTitle?: string; + extensionIdentifier: string; id: string; title: string; options: ICreateContributedTerminalProfileOptions; titleTemplate?: string; } export const ITerminalProfileService = createDecorator('terminalProfileService'); @@ -693,8 +693,8 @@ export const terminalContributionsDescriptor: IExtensionPointDescriptor { strictEqual(terminalLabelComputer.title, 'my-title'); strictEqual(terminalLabelComputer.description, 'folder'); }); - test('should use shellLaunchConfig.title as template when set', () => { + test('should use shellLaunchConfig.titleTemplate as template when set', () => { const terminalLabelComputer = createLabelComputer({ terminal: { integrated: { tabs: { separator: ' - ', title: '${process}', description: '${cwd}' } } } }); - terminalLabelComputer.refreshLabel(createInstance({ capabilities, sequence: 'my-sequence', processName: 'zsh', shellLaunchConfig: { title: '${sequence}' } })); + terminalLabelComputer.refreshLabel(createInstance({ capabilities, sequence: 'my-sequence', processName: 'zsh', shellLaunchConfig: { titleTemplate: '${sequence}' } })); strictEqual(terminalLabelComputer.title, 'my-sequence'); strictEqual(terminalLabelComputer.description, 'cwd'); }); diff --git a/src/vscode-dts/vscode.proposed.terminalTitle.d.ts b/src/vscode-dts/vscode.proposed.terminalTitle.d.ts index 584b2d29cf4..7833a644a49 100644 --- a/src/vscode-dts/vscode.proposed.terminalTitle.d.ts +++ b/src/vscode-dts/vscode.proposed.terminalTitle.d.ts @@ -15,10 +15,10 @@ declare module 'vscode' { * behavior (which uses the `name` as a static title) and instead uses the template for * dynamic title resolution. * - * For example, setting `title` to `"${sequence}"` allows the terminal's escape sequence + * For example, setting `titleTemplate` to `"${sequence}"` allows the terminal's escape sequence * title to be used as the tab title. */ - title?: string; + titleTemplate?: string; } export interface ExtensionTerminalOptions { @@ -29,9 +29,9 @@ declare module 'vscode' { * behavior (which uses the `name` as a static title) and instead uses the template for * dynamic title resolution. * - * For example, setting `title` to `"${sequence}"` allows the terminal's escape sequence + * For example, setting `titleTemplate` to `"${sequence}"` allows the terminal's escape sequence * title to be used as the tab title. */ - title?: string; + titleTemplate?: string; } } From 1643bdada516a99e7ebc1b9ad4e4c6cf91b8ffbc Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Fri, 20 Feb 2026 14:03:27 -0800 Subject: [PATCH 4/4] proposed api check. --- src/vs/workbench/api/common/extHost.api.impl.ts | 3 +++ .../api/common/extHostTerminalService.ts | 15 ++++++++++++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index f26f121a87d..1ed78327aa5 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -941,6 +941,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I }, createTerminal(nameOrOptions?: vscode.TerminalOptions | vscode.ExtensionTerminalOptions | string, shellPath?: string, shellArgs?: readonly string[] | string): vscode.Terminal { if (typeof nameOrOptions === 'object') { + if ('titleTemplate' in nameOrOptions && nameOrOptions.titleTemplate !== undefined) { + checkProposedApiEnabled(extension, 'terminalTitle'); + } if ('pty' in nameOrOptions) { return extHostTerminalService.createExtensionTerminal(nameOrOptions); } diff --git a/src/vs/workbench/api/common/extHostTerminalService.ts b/src/vs/workbench/api/common/extHostTerminalService.ts index 4dcec3f6681..52becdb9649 100644 --- a/src/vs/workbench/api/common/extHostTerminalService.ts +++ b/src/vs/workbench/api/common/extHostTerminalService.ts @@ -29,6 +29,7 @@ import { MarshalledId } from '../../../base/common/marshallingIds.js'; import { ISerializedTerminalInstanceContext } from '../../contrib/terminal/common/terminal.js'; import { isWindows } from '../../../base/common/platform.js'; import { hasKey } from '../../../base/common/types.js'; +import { checkProposedApiEnabled } from '../../services/extensions/common/extensions.js'; export interface IExtHostTerminalService extends ExtHostTerminalServiceShape, IDisposable { @@ -425,7 +426,7 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I private readonly _bufferer: TerminalDataBufferer; private readonly _linkProviders: Set = new Set(); private readonly _completionProviders: Map> = new Map(); - private readonly _profileProviders: Map = new Map(); + private readonly _profileProviders: Map = new Map(); private readonly _quickFixProviders: Map = new Map(); private readonly _terminalLinkCache: Map> = new Map(); private readonly _terminalLinkCancellationSource: Map = new Map(); @@ -752,7 +753,7 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I if (this._profileProviders.has(id)) { throw new Error(`Terminal profile provider "${id}" already registered`); } - this._profileProviders.set(id, provider); + this._profileProviders.set(id, { provider, extension }); this._proxy.$registerProfileProvider(id, extension.identifier.value); return new VSCodeDisposable(() => { this._profileProviders.delete(id); @@ -845,7 +846,11 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I public async $createContributedProfileTerminal(id: string, options: ICreateContributedTerminalProfileOptions): Promise { const token = new CancellationTokenSource().token; - let profile = await this._profileProviders.get(id)?.provideTerminalProfile(token); + const profileProviderData = this._profileProviders.get(id); + if (!profileProviderData) { + throw new Error(`No terminal profile provider registered for id "${id}"`); + } + let profile = await profileProviderData.provider.provideTerminalProfile(token); if (token.isCancellationRequested) { return; } @@ -857,6 +862,10 @@ export abstract class BaseExtHostTerminalService extends Disposable implements I throw new Error(`No terminal profile options provided for id "${id}"`); } + if (profile.options.titleTemplate !== undefined) { + checkProposedApiEnabled(profileProviderData.extension, 'terminalTitle'); + } + const profileOptions = options.titleTemplate && !profile.options.titleTemplate ? { ...profile.options, titleTemplate: options.titleTemplate } : profile.options;