From 302c91406087e6e4bdecef584019c38f2ce22f59 Mon Sep 17 00:00:00 2001 From: Hawk Ticehurst <39639992+hawkticehurst@users.noreply.github.com> Date: Wed, 3 Dec 2025 06:27:51 -0500 Subject: [PATCH] Chat view: introduce session title and back button (fix #277537) (#278874) --- .github/CODENOTIFY | 3 + src/vs/platform/actions/common/actions.ts | 1 + .../chat/browser/actions/chatActions.ts | 25 ++- .../chat/browser/actions/chatNewActions.ts | 9 + .../contrib/chat/browser/chat.contribution.ts | 9 + .../browser/chatParticipant.contribution.ts | 2 +- .../contrib/chat/browser/chatViewPane.ts | 93 ++++++--- .../chat/browser/chatViewTitleControl.ts | 180 ++++++++++++++++++ .../browser/media/chatViewTitleControl.css | 28 +++ .../contrib/chat/common/constants.ts | 1 + 10 files changed, 321 insertions(+), 30 deletions(-) create mode 100644 src/vs/workbench/contrib/chat/browser/chatViewTitleControl.ts create mode 100644 src/vs/workbench/contrib/chat/browser/media/chatViewTitleControl.css diff --git a/.github/CODENOTIFY b/.github/CODENOTIFY index 6b84383e51f..ac22ac40d26 100644 --- a/.github/CODENOTIFY +++ b/.github/CODENOTIFY @@ -104,6 +104,9 @@ src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @roblourens src/vs/workbench/contrib/chat/browser/chatSetup/** @bpasero src/vs/workbench/contrib/chat/browser/chatStatus/** @bpasero src/vs/workbench/contrib/chat/browser/chatViewPane.ts @bpasero +src/vs/workbench/contrib/chat/browser/media/chatViewPane.css @bpasero +src/vs/workbench/contrib/chat/browser/chatViewTitleControl.ts @bpasero +src/vs/workbench/contrib/chat/browser/media/chatViewTitleControl.css @bpasero src/vs/workbench/contrib/chat/browser/chatManagement/chatUsageWidget.ts @bpasero src/vs/workbench/contrib/chat/browser/chatManagement/media/chatUsageWidget.css @bpasero src/vs/workbench/contrib/chat/browser/agentSessions/** @bpasero diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index 1da4aa0b405..b9b5cef814a 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -281,6 +281,7 @@ export class MenuId { static readonly ChatSessionsMenu = new MenuId('ChatSessionsMenu'); static readonly ChatSessionsCreateSubMenu = new MenuId('ChatSessionsCreateSubMenu'); static readonly ChatRecentSessionsToolbar = new MenuId('ChatRecentSessionsToolbar'); + static readonly ChatViewSessionTitleToolbar = new MenuId('ChatViewSessionTitleToolbar'); static readonly ChatConfirmationMenu = new MenuId('ChatConfirmationMenu'); static readonly ChatEditorInlineExecute = new MenuId('ChatEditorInputExecute'); static readonly ChatEditorInlineInputSide = new MenuId('ChatEditorInputSide'); diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts index 31adfedf89b..43eea5141ce 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatActions.ts @@ -1836,8 +1836,6 @@ registerAction2(class ToggleChatViewRecentSessionsAction extends Action2 { super({ id: 'workbench.action.chat.toggleChatViewRecentSessions', title: localize2('chat.toggleChatViewRecentSessions.label', "Show Recent Sessions"), - category: CHAT_CATEGORY, - precondition: ChatContextKeys.enabled, toggled: ContextKeyExpr.equals(`config.${ChatConfiguration.ChatViewRecentSessionsEnabled}`, true), menu: { id: MenuId.ChatWelcomeContext, @@ -1855,3 +1853,26 @@ registerAction2(class ToggleChatViewRecentSessionsAction extends Action2 { await configurationService.updateValue(ChatConfiguration.ChatViewRecentSessionsEnabled, !chatViewRecentSessionsEnabled); } }); + +registerAction2(class ToggleChatViewTitleAction extends Action2 { + constructor() { + super({ + id: 'workbench.action.chat.toggleChatViewTitle', + title: localize2('chat.toggleChatViewTitle.label', "Show Chat Title"), + toggled: ContextKeyExpr.equals(`config.${ChatConfiguration.ChatViewTitleEnabled}`, true), + menu: { + id: MenuId.ChatWelcomeContext, + group: '1_modify', + order: 2, + when: ChatContextKeys.inChatEditor.negate() + } + }); + } + + async run(accessor: ServicesAccessor): Promise { + const configurationService = accessor.get(IConfigurationService); + + const chatViewTitleEnabled = configurationService.getValue(ChatConfiguration.ChatViewTitleEnabled); + await configurationService.updateValue(ChatConfiguration.ChatViewTitleEnabled, !chatViewTitleEnabled); + } +}); diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatNewActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatNewActions.ts index 4d8c98998a4..cc39ad13d52 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatNewActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatNewActions.ts @@ -160,6 +160,15 @@ export function registerNewChatActions() { }); CommandsRegistry.registerCommandAlias(ACTION_ID_NEW_EDIT_SESSION, ACTION_ID_NEW_CHAT); + MenuRegistry.appendMenuItem(MenuId.ChatViewSessionTitleToolbar, { + command: { + id: ACTION_ID_NEW_CHAT, + title: localize2('chat.goBack', "Go Back"), + icon: Codicon.arrowLeft, + }, + group: 'navigation' + }); + registerAction2(class UndoChatEditInteractionAction extends EditingSessionAction { constructor() { super({ diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 636008564dc..e51424cb887 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -373,6 +373,15 @@ configurationRegistry.registerConfiguration({ mode: 'auto' } }, + [ChatConfiguration.ChatViewTitleEnabled]: { // TODO@bpasero decide on a default + type: 'boolean', + default: product.quality !== 'stable', + description: nls.localize('chat.viewTitle.enabled', "Show the title of the chat above the chat in the chat view."), + tags: ['preview', 'experimental'], + experiment: { + mode: 'auto' + } + }, [ChatConfiguration.NotifyWindowOnResponseReceived]: { type: 'boolean', default: true, diff --git a/src/vs/workbench/contrib/chat/browser/chatParticipant.contribution.ts b/src/vs/workbench/contrib/chat/browser/chatParticipant.contribution.ts index a0494482d5a..9573667588a 100644 --- a/src/vs/workbench/contrib/chat/browser/chatParticipant.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chatParticipant.contribution.ts @@ -64,7 +64,7 @@ const chatViewDescriptor: IViewDescriptor = { }, order: 1 }, - ctorDescriptor: new SyncDescriptor(ChatViewPane, [{ location: ChatAgentLocation.Chat }]), + ctorDescriptor: new SyncDescriptor(ChatViewPane), when: ContextKeyExpr.or( ContextKeyExpr.or( ChatContextKeys.Setup.hidden, diff --git a/src/vs/workbench/contrib/chat/browser/chatViewPane.ts b/src/vs/workbench/contrib/chat/browser/chatViewPane.ts index 7d0c5715133..8152cef0b0b 100644 --- a/src/vs/workbench/contrib/chat/browser/chatViewPane.ts +++ b/src/vs/workbench/contrib/chat/browser/chatViewPane.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import './media/chatViewPane.css'; import { $, addDisposableListener, append, EventHelper, EventType, getWindow, setVisibility } from '../../../../base/browser/dom.js'; import { StandardMouseEvent } from '../../../../base/browser/mouseEvent.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; @@ -46,7 +47,7 @@ import { showCloseActiveChatNotification } from './actions/chatCloseNotification import { AgentSessionsControl } from './agentSessions/agentSessionsControl.js'; import { AgentSessionsListDelegate } from './agentSessions/agentSessionsViewer.js'; import { ChatWidget } from './chatWidget.js'; -import './media/chatViewPane.css'; +import { ChatViewTitleControl } from './chatViewTitleControl.js'; import { ChatViewWelcomeController, IViewWelcomeDelegate } from './viewsWelcome/chatViewWelcomeController.js'; interface IChatViewPaneState extends Partial { @@ -65,8 +66,6 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { private _widget!: ChatWidget; get widget(): ChatWidget { return this._widget; } - private readonly modelRef = this._register(new MutableDisposable()); - private readonly memento: Memento; private readonly viewState: IChatViewPaneState; @@ -77,14 +76,16 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { private sessionsControl: AgentSessionsControl | undefined; private sessionsCount: number = 0; - private welcomeController: ChatViewWelcomeController | undefined; + private titleControl: ChatViewTitleControl | undefined; - private restoringSession: Promise | undefined; + private welcomeController: ChatViewWelcomeController | undefined; private lastDimensions: { height: number; width: number } | undefined; + private restoringSession: Promise | undefined; + private readonly modelRef = this._register(new MutableDisposable()); + constructor( - private readonly chatOptions: { location: ChatAgentLocation.Chat }, options: IViewPaneOptions, @IKeybindingService keybindingService: IKeybindingService, @IContextMenuService contextMenuService: IContextMenuService, @@ -125,7 +126,7 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { private registerListeners(): void { this._register(this.chatAgentService.onDidChangeAgents(() => { - if (this.chatAgentService.getDefaultAgent(this.chatOptions?.location)) { + if (this.chatAgentService.getDefaultAgent(ChatAgentLocation.Chat)) { if (!this._widget?.viewModel && !this.restoringSession) { const info = this.getTransferredOrPersistedSessionInfo(); this.restoringSession = @@ -144,11 +145,13 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { if (info.inputState && modelRef) { modelRef.object.inputModel.setState(info.inputState); } - await this.updateModel(modelRef); + + await this.showModel(modelRef); } finally { this._widget.setVisible(wasVisible); } }); + this.restoringSession.finally(() => this.restoringSession = undefined); } } @@ -158,7 +161,7 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { } private getTransferredOrPersistedSessionInfo(): { sessionId?: string; inputState?: IChatModelInputState; mode?: ChatModeKind } { - if (this.chatService.transferredSessionData?.location === this.chatOptions.location) { + if (this.chatService.transferredSessionData?.location === ChatAgentLocation.Chat) { const sessionId = this.chatService.transferredSessionData.sessionId; return { sessionId, @@ -176,7 +179,7 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { } : undefined; } - private async updateModel(modelRef?: IChatModelReference | undefined) { + private async showModel(modelRef?: IChatModelReference | undefined): Promise { // Check if we're disposing a model with an active request if (this.modelRef.value?.object.requestInProgress.get()) { @@ -186,20 +189,24 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { this.modelRef.value = undefined; - const ref = modelRef ?? (this.chatService.transferredSessionData?.sessionId && this.chatService.transferredSessionData?.location === this.chatOptions.location + const ref = modelRef ?? (this.chatService.transferredSessionData?.sessionId && this.chatService.transferredSessionData?.location === ChatAgentLocation.Chat ? await this.chatService.getOrRestoreSession(LocalChatSessionUri.forSession(this.chatService.transferredSessionData.sessionId)) - : this.chatService.startSession(this.chatOptions.location)); + : this.chatService.startSession(ChatAgentLocation.Chat)); if (!ref) { throw new Error('Could not start chat session'); } this.modelRef.value = ref; const model = ref.object; + // Update widget lock state based on session type await this.updateWidgetLockState(model.sessionResource); - this.viewState.sessionId = model.sessionId; + this.viewState.sessionId = model.sessionId; // remember as model to restore in view state this._widget.setModel(model); + // Update title control + this.titleControl?.update(model); + // Update the toolbar context with new sessionId this.updateActions(); @@ -208,11 +215,11 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { override shouldShowWelcome(): boolean { const noPersistedSessions = !this.chatService.hasSessions(); - const hasCoreAgent = this.chatAgentService.getAgents().some(agent => agent.isCore && agent.locations.includes(this.chatOptions.location)); - const hasDefaultAgent = this.chatAgentService.getDefaultAgent(this.chatOptions.location) !== undefined; // only false when Hide AI Features has run and unregistered the setup agents + const hasCoreAgent = this.chatAgentService.getAgents().some(agent => agent.isCore && agent.locations.includes(ChatAgentLocation.Chat)); + const hasDefaultAgent = this.chatAgentService.getDefaultAgent(ChatAgentLocation.Chat) !== undefined; // only false when Hide AI Features has run and unregistered the setup agents const shouldShow = !hasCoreAgent && (!hasDefaultAgent || !this._widget?.viewModel && noPersistedSessions); - this.logService.trace(`ChatViewPane#shouldShowWelcome(${this.chatOptions.location}) = ${shouldShow}: hasCoreAgent=${hasCoreAgent} hasDefaultAgent=${hasDefaultAgent} || noViewModel=${!this._widget?.viewModel} && noPersistedSessions=${noPersistedSessions}`); + this.logService.trace(`ChatViewPane#shouldShowWelcome() = ${shouldShow}: hasCoreAgent=${hasCoreAgent} hasDefaultAgent=${hasDefaultAgent} || noViewModel=${!this._widget?.viewModel} && noPersistedSessions=${noPersistedSessions}`); return !!shouldShow; } @@ -226,6 +233,7 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { this.viewPaneContainer.classList.add('chat-viewpane'); this.createControls(parent); + this.setupContextMenu(parent); this.applyModel(); @@ -236,8 +244,11 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { // Sessions Control this.createSessionsControl(parent); + // Title Control + this.createTitleControl(parent); + // Welcome Control - this.welcomeController = this._register(this.instantiationService.createInstance(ChatViewWelcomeController, parent, this, this.chatOptions.location)); + this.welcomeController = this._register(this.instantiationService.createInstance(ChatViewWelcomeController, parent, this, ChatAgentLocation.Chat)); // Chat Widget this.createChatWidget(parent); @@ -314,9 +325,9 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { const newSessionsContainerVisible = this.configurationService.getValue(ChatConfiguration.ChatViewRecentSessionsEnabled) && // enabled in settings - (!this._widget || this._widget?.isEmpty()) && // chat widget empty - !this.welcomeController?.isShowingWelcome.get() && // welcome not showing - this.sessionsCount > 0; // has sessions + (!this._widget || this._widget?.isEmpty()) && // chat widget empty + !this.welcomeController?.isShowingWelcome.get() && // welcome not showing + this.sessionsCount > 0; // has sessions this.viewPaneContainer.classList.toggle('has-sessions-control', newSessionsContainerVisible); @@ -329,6 +340,21 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { }; } + private createTitleControl(parent: HTMLElement): void { + this.titleControl = this._register(this.instantiationService.createInstance(ChatViewTitleControl, + parent, + { + updateTitle: title => this.updateTitle(title) + } + )); + + this._register(this.titleControl.onDidChangeHeight(() => { + if (this.lastDimensions) { + this.layoutBody(this.lastDimensions.height, this.lastDimensions.width); + } + })); + } + private createChatWidget(parent: HTMLElement): void { const locationBasedColors = this.getLocationBasedColors(); @@ -338,11 +364,11 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { const scopedInstantiationService = this._register(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService]))); this._widget = this._register(scopedInstantiationService.createInstance( ChatWidget, - this.chatOptions.location, + ChatAgentLocation.Chat, { viewId: this.id }, { autoScroll: mode => mode !== ChatModeKind.Ask, - renderFollowups: this.chatOptions.location === ChatAgentLocation.Chat, + renderFollowups: true, supportsFileReferences: true, clear: () => this.clear(), rendererOptions: { @@ -353,7 +379,7 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { progressMessageAtBottomOfResponse: mode => mode !== ChatModeKind.Ask, }, editorOverflowWidgetsDomNode, - enableImplicitContext: this.chatOptions.location === ChatAgentLocation.Chat, + enableImplicitContext: true, enableWorkingSet: 'explicit', supportsChangingModes: true, }, @@ -390,28 +416,27 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { modelRef.object.inputModel.setState(info.inputState); } - await this.updateModel(modelRef); + await this.showModel(modelRef); } private async clear(): Promise { // Grab the widget's latest view state because it will be loaded back into the widget this.updateViewState(); - await this.updateModel(undefined); + await this.showModel(undefined); // Update the toolbar context with new sessionId this.updateActions(); } async loadSession(sessionId: URI): Promise { - const sessionType = getChatSessionType(sessionId); if (sessionType !== localChatSessionType) { await this.chatSessionsService.canResolveChatSession(sessionId); } const newModelRef = await this.chatService.loadSessionForResource(sessionId, ChatAgentLocation.Chat, CancellationToken.None); - return this.updateModel(newModelRef); + return this.showModel(newModelRef); } focusInput(): void { @@ -440,6 +465,9 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { remainingHeight -= this.sessionsContainer.offsetHeight; } + // Title Control + remainingHeight -= this.titleControl?.getHeight() ?? 0; + // Chat Widget this._widget.layout(remainingHeight, width); } @@ -494,4 +522,15 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { this._widget.unlockFromCodingAgent(); } } + + override get singleViewPaneContainerTitle(): string | undefined { + if (this.titleControl) { + const titleControlTitle = this.titleControl.getSingleViewPaneContainerTitle(); + if (titleControlTitle) { + return titleControlTitle; + } + } + + return super.singleViewPaneContainerTitle; + } } diff --git a/src/vs/workbench/contrib/chat/browser/chatViewTitleControl.ts b/src/vs/workbench/contrib/chat/browser/chatViewTitleControl.ts new file mode 100644 index 00000000000..2a889c16909 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatViewTitleControl.ts @@ -0,0 +1,180 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import './media/chatViewTitleControl.css'; +import { h } from '../../../../base/browser/dom.js'; +import { Emitter } from '../../../../base/common/event.js'; +import { Disposable, MutableDisposable } from '../../../../base/common/lifecycle.js'; +import { localize } from '../../../../nls.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { IViewDescriptorService, IViewContainerModel } from '../../../common/views.js'; +import { ActivityBarPosition, LayoutSettings } from '../../../services/layout/browser/layoutService.js'; +import { IChatModel } from '../common/chatModel.js'; +import { ChatViewId } from './chat.js'; +import { ChatConfiguration } from '../common/constants.js'; +import { MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; +import { MenuId } from '../../../../platform/actions/common/actions.js'; +import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; + +export interface IChatViewTitleDelegate { + updateTitle(title: string): void; +} + +export class ChatViewTitleControl extends Disposable { + + private static readonly DEFAULT_TITLE = localize('chat', "Chat Session"); + + private readonly _onDidChangeHeight = this._register(new Emitter()); + readonly onDidChangeHeight = this._onDidChangeHeight.event; + + private get viewContainerModel(): IViewContainerModel | undefined { + const viewContainer = this.viewDescriptorService.getViewContainerByViewId(ChatViewId); + if (viewContainer) { + return this.viewDescriptorService.getViewContainerModel(viewContainer); + } + + return undefined; + } + + private title: string | undefined = undefined; + + private titleContainer: HTMLElement | undefined; + private titleLabel: HTMLElement | undefined; + + private model: IChatModel | undefined; + private modelDisposables = this._register(new MutableDisposable()); + + private lastKnownHeight = 0; + + constructor( + private readonly container: HTMLElement, + private readonly delegate: IChatViewTitleDelegate, + @IConfigurationService private readonly configurationService: IConfigurationService, + @IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { + super(); + + this.render(this.container); + + this.registerListeners(); + } + + private registerListeners(): void { + + // Update when views change in container + if (this.viewContainerModel) { + this._register(this.viewContainerModel.onDidAddVisibleViewDescriptors(() => this.doUpdate())); + this._register(this.viewContainerModel.onDidRemoveVisibleViewDescriptors(() => this.doUpdate())); + } + + // Update on configuration changes + this._register(this.configurationService.onDidChangeConfiguration(e => { + if ( + e.affectsConfiguration(LayoutSettings.ACTIVITY_BAR_LOCATION) || + e.affectsConfiguration(ChatConfiguration.ChatViewTitleEnabled) + ) { + this.doUpdate(); + } + })); + } + + private render(parent: HTMLElement): void { + const elements = h('div.chat-view-title-container', [ + h('div.chat-view-title-toolbar@toolbar'), + h('span.chat-view-title-label@label'), + ]); + + this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, elements.toolbar, MenuId.ChatViewSessionTitleToolbar, {})); + + this.titleContainer = elements.root; + this.titleLabel = elements.label; + + parent.appendChild(this.titleContainer); + } + + update(model: IChatModel | undefined): void { + this.model = model; + + this.modelDisposables.value = model?.onDidChange(e => { + if (e.kind === 'setCustomTitle' || e.kind === 'addRequest') { + this.doUpdate(); + } + }); + + this.doUpdate(); + } + + private doUpdate(): void { + this.title = this.model?.title; + + this.delegate.updateTitle(this.getTitleWithPrefix()); + + this.updateTitle(this.title ?? ChatViewTitleControl.DEFAULT_TITLE); + } + + private updateTitle(title: string): void { + if (!this.titleContainer || !this.titleLabel) { + return; + } + + this.titleContainer.classList.toggle('visible', this.shouldRender()); + this.titleLabel.textContent = title; + + const currentHeight = this.getHeight(); + if (currentHeight !== this.lastKnownHeight) { + this.lastKnownHeight = currentHeight; + + this._onDidChangeHeight.fire(); + } + } + + private shouldRender(): boolean { + if (!this.isEnabled()) { + return false; // title hidden via setting + } + + if (this.viewContainerModel && this.viewContainerModel.visibleViewDescriptors.length > 1) { + return false; // multiple views visible, chat view shows a title already + } + + if (this.configurationService.getValue(LayoutSettings.ACTIVITY_BAR_LOCATION) !== ActivityBarPosition.DEFAULT) { + return false; // activity bar not in default location, view title shown already + } + + return !!this.model?.title; + } + + private isEnabled(): boolean { + return this.configurationService.getValue(ChatConfiguration.ChatViewTitleEnabled) === true; + } + + getSingleViewPaneContainerTitle(): string | undefined { + if ( + !this.isEnabled() || // title disabled + this.shouldRender() // title is rendered in the view, do not repeat + ) { + return undefined; + } + + return this.getTitleWithPrefix(); + } + + private getTitleWithPrefix(): string { + if (this.title) { + return localize('chatTitleWithPrefixCustom', "Chat: {0}", this.title); + } + + return ChatViewTitleControl.DEFAULT_TITLE; + } + + getHeight(): number { + if (!this.titleContainer || this.titleContainer.style.display === 'none') { + return 0; + } + + return this.titleContainer.offsetHeight; + } +} diff --git a/src/vs/workbench/contrib/chat/browser/media/chatViewTitleControl.css b/src/vs/workbench/contrib/chat/browser/media/chatViewTitleControl.css new file mode 100644 index 00000000000..b9798760472 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/media/chatViewTitleControl.css @@ -0,0 +1,28 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.chat-viewpane { + + .chat-view-title-container { + display: none; + padding: 8px 16px; + border-bottom: 1px solid var(--vscode-sideBarSectionHeader-border); + align-items: center; + + .chat-view-title-label { + text-transform: uppercase; + font-size: 12px; + color: var(--vscode-descriptionForeground); + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + } + + .chat-view-title-container.visible { + display: flex; + gap: 4px; + } +} diff --git a/src/vs/workbench/contrib/chat/common/constants.ts b/src/vs/workbench/contrib/chat/common/constants.ts index e572b4979a9..9885a8c1f93 100644 --- a/src/vs/workbench/contrib/chat/common/constants.ts +++ b/src/vs/workbench/contrib/chat/common/constants.ts @@ -25,6 +25,7 @@ export enum ChatConfiguration { ShowAgentSessionsViewDescription = 'chat.showAgentSessionsViewDescription', NotifyWindowOnResponseReceived = 'chat.notifyWindowOnResponseReceived', ChatViewRecentSessionsEnabled = 'chat.recentSessions.enabled', + ChatViewTitleEnabled = 'chat.viewTitle.enabled', SubagentToolCustomAgents = 'chat.customAgentInSubagent.enabled', ShowCodeBlockProgressAnimation = 'chat.agent.codeBlockProgress', RestoreLastPanelSession = 'chat.restoreLastPanelSession',