diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 3e4844db52f..a8a857c8da2 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -89,6 +89,7 @@ import './agentSessions/agentSessionsView.js'; import { IChatAccessibilityService, IChatCodeBlockContextProviderService, IChatWidgetService, IQuickChatService } from './chat.js'; import { ChatAccessibilityService } from './chatAccessibilityService.js'; import './chatAttachmentModel.js'; +import './chatStatusWidget.js'; import { ChatAttachmentResolveService, IChatAttachmentResolveService } from './chatAttachmentResolveService.js'; import { ChatMarkdownAnchorService, IChatMarkdownAnchorService } from './chatContentParts/chatMarkdownAnchorService.js'; import { ChatContextPickService, IChatContextPickService } from './chatContextPickService.js'; @@ -535,6 +536,16 @@ configurationRegistry.registerConfiguration({ default: true, tags: ['experimental'], }, + ['chat.statusWidget.enabled']: { + type: 'boolean', + description: nls.localize('chat.statusWidget.enabled.description', "Show the status widget in new chat sessions when quota is exceeded."), + default: false, + tags: ['experimental'], + included: false, + experiment: { + mode: 'auto' + } + }, [ChatConfiguration.AgentSessionsViewLocation]: { type: 'string', enum: ['disabled', 'view', 'single-view'], // TODO@bpasero remove this setting eventually diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index 651e5c0475b..6f5b56da3be 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -97,6 +97,7 @@ import { DefaultChatAttachmentWidget, ElementChatAttachmentWidget, FileAttachmen import { IDisposableReference } from './chatContentParts/chatCollections.js'; import { CollapsibleListPool, IChatCollapsibleListItem } from './chatContentParts/chatReferencesContentPart.js'; import { ChatTodoListWidget } from './chatContentParts/chatTodoListWidget.js'; +import { ChatInputPartWidgetController } from './chatInputPartWidgets.js'; import { IChatContextService } from './chatContextService.js'; import { ChatDragAndDrop } from './chatDragAndDrop.js'; import { ChatEditingShowChangesAction, ViewPreviousEditsAction } from './chatEditing/chatEditingActions.js'; @@ -234,6 +235,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private chatEditingSessionWidgetContainer!: HTMLElement; private chatInputTodoListWidgetContainer!: HTMLElement; + private chatInputWidgetsContainer!: HTMLElement; + private readonly _widgetController = this._register(new MutableDisposable()); private _inputPartHeight: number = 0; get inputPartHeight() { @@ -254,6 +257,10 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge return this.chatInputTodoListWidgetContainer.offsetHeight; } + get inputWidgetsHeight() { + return this.chatInputWidgetsContainer?.offsetHeight ?? 0; + } + get attachmentsHeight() { return this.attachmentsContainer.offsetHeight + (this.attachmentsContainer.checkVisibility() ? 6 : 0); } @@ -1312,6 +1319,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge if (this.options.renderStyle === 'compact') { elements = dom.h('.interactive-input-part', [ dom.h('.interactive-input-and-edit-session', [ + dom.h('.chat-input-widgets-container@chatInputWidgetsContainer'), dom.h('.chat-todo-list-widget-container@chatInputTodoListWidgetContainer'), dom.h('.chat-editing-session@chatEditingSessionWidgetContainer'), dom.h('.interactive-input-and-side-toolbar@inputAndSideToolbar', [ @@ -1331,6 +1339,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } else { elements = dom.h('.interactive-input-part', [ dom.h('.interactive-input-followups@followupsContainer'), + dom.h('.chat-input-widgets-container@chatInputWidgetsContainer'), dom.h('.chat-todo-list-widget-container@chatInputTodoListWidgetContainer'), dom.h('.chat-editing-session@chatEditingSessionWidgetContainer'), dom.h('.interactive-input-and-side-toolbar@inputAndSideToolbar', [ @@ -1362,6 +1371,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const attachmentToolbarContainer = elements.attachmentToolbar; this.chatEditingSessionWidgetContainer = elements.chatEditingSessionWidgetContainer; this.chatInputTodoListWidgetContainer = elements.chatInputTodoListWidgetContainer; + this.chatInputWidgetsContainer = elements.chatInputWidgetsContainer; if (this.options.enableImplicitContext) { this._implicitContext = this._register( @@ -1374,6 +1384,9 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge })); } + this._widgetController.value = this.instantiationService.createInstance(ChatInputPartWidgetController, this.chatInputWidgetsContainer); + this._register(this._widgetController.value.onDidChangeHeight(() => this._onDidChangeHeight.fire())); + this.renderAttachedContext(); this._register(this._attachmentModel.onDidChange((e) => { if (e.added.length > 0) { @@ -2197,7 +2210,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge get contentHeight(): number { const data = this.getLayoutData(); - return data.followupsHeight + data.inputPartEditorHeight + data.inputPartVerticalPadding + data.inputEditorBorder + data.attachmentsHeight + data.toolbarsHeight + data.chatEditingStateHeight + data.todoListWidgetContainerHeight; + return data.followupsHeight + data.inputPartEditorHeight + data.inputPartVerticalPadding + data.inputEditorBorder + data.attachmentsHeight + data.toolbarsHeight + data.chatEditingStateHeight + data.todoListWidgetContainerHeight + data.inputWidgetsContainerHeight; } layout(height: number, width: number) { @@ -2209,12 +2222,12 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private previousInputEditorDimension: IDimension | undefined; private _layout(height: number, width: number, allowRecurse = true): void { const data = this.getLayoutData(); - const inputEditorHeight = Math.min(data.inputPartEditorHeight, height - data.followupsHeight - data.attachmentsHeight - data.inputPartVerticalPadding - data.toolbarsHeight - data.chatEditingStateHeight - data.todoListWidgetContainerHeight); + const inputEditorHeight = Math.min(data.inputPartEditorHeight, height - data.followupsHeight - data.attachmentsHeight - data.inputPartVerticalPadding - data.toolbarsHeight - data.chatEditingStateHeight - data.todoListWidgetContainerHeight - data.inputWidgetsContainerHeight); const followupsWidth = width - data.inputPartHorizontalPadding; this.followupsContainer.style.width = `${followupsWidth}px`; - this._inputPartHeight = data.inputPartVerticalPadding + data.followupsHeight + inputEditorHeight + data.inputEditorBorder + data.attachmentsHeight + data.toolbarsHeight + data.chatEditingStateHeight + data.todoListWidgetContainerHeight; + this._inputPartHeight = data.inputPartVerticalPadding + data.followupsHeight + inputEditorHeight + data.inputEditorBorder + data.attachmentsHeight + data.toolbarsHeight + data.chatEditingStateHeight + data.todoListWidgetContainerHeight + data.inputWidgetsContainerHeight; this._followupsHeight = data.followupsHeight; this._editSessionWidgetHeight = data.chatEditingStateHeight; @@ -2265,6 +2278,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge chatEditingStateHeight: this.chatEditingSessionWidgetContainer.offsetHeight, sideToolbarWidth: inputSideToolbarWidth > 0 ? inputSideToolbarWidth + 4 /*gap*/ : 0, todoListWidgetContainerHeight: this.chatInputTodoListWidgetContainer.offsetHeight, + inputWidgetsContainerHeight: this.inputWidgetsHeight, }; } } diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPartWidgets.ts b/src/vs/workbench/contrib/chat/browser/chatInputPartWidgets.ts new file mode 100644 index 00000000000..56f96455ccf --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatInputPartWidgets.ts @@ -0,0 +1,144 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Emitter, Event } from '../../../../base/common/event.js'; +import { Disposable, DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js'; +import { ContextKeyExpression, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; +import { BrandedService, IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; + +/** + * A widget that can be rendered on top of the chat input part. + */ +export interface IChatInputPartWidget extends IDisposable { + /** + * The DOM node of the widget. + */ + readonly domNode: HTMLElement; + + /** + * Fired when the height of the widget changes. + */ + readonly onDidChangeHeight: Event; + + /** + * The current height of the widget in pixels. + */ + readonly height: number; +} + +export interface IChatInputPartWidgetDescriptor { + readonly id: string; + readonly when?: ContextKeyExpression; + readonly ctor: new (...services: Services) => IChatInputPartWidget; +} + +/** + * Registry for chat input part widgets. + * Widgets register themselves and are instantiated by the controller based on context key conditions. + */ +export const ChatInputPartWidgetsRegistry = new class { + readonly widgets: IChatInputPartWidgetDescriptor[] = []; + + register(id: string, ctor: new (...services: Services) => IChatInputPartWidget, when?: ContextKeyExpression): void { + this.widgets.push({ id, ctor: ctor as IChatInputPartWidgetDescriptor['ctor'], when }); + } + + getWidgets(): readonly IChatInputPartWidgetDescriptor[] { + return this.widgets; + } +}(); + +interface IRenderedWidget { + readonly descriptor: IChatInputPartWidgetDescriptor; + readonly widget: IChatInputPartWidget; + readonly disposables: DisposableStore; +} + +/** + * Controller that manages the rendering of widgets in the chat input part. + * Widgets are shown/hidden based on context key conditions. + */ +export class ChatInputPartWidgetController extends Disposable { + + private readonly _onDidChangeHeight = this._register(new Emitter()); + readonly onDidChangeHeight: Event = this._onDidChangeHeight.event; + + private readonly renderedWidgets = new Map(); + + constructor( + private readonly container: HTMLElement, + @IContextKeyService private readonly contextKeyService: IContextKeyService, + @IInstantiationService private readonly instantiationService: IInstantiationService, + ) { + super(); + + this.update(); + + this._register(this.contextKeyService.onDidChangeContext(e => { + const relevantKeys = new Set(); + for (const descriptor of ChatInputPartWidgetsRegistry.getWidgets()) { + if (descriptor.when) { + for (const key of descriptor.when.keys()) { + relevantKeys.add(key); + } + } + } + if (e.affectsSome(relevantKeys)) { + this.update(); + } + })); + } + + private update(): void { + const visibleIds = new Set(); + for (const descriptor of ChatInputPartWidgetsRegistry.getWidgets()) { + if (this.contextKeyService.contextMatchesRules(descriptor.when)) { + visibleIds.add(descriptor.id); + } + } + + for (const [id, rendered] of this.renderedWidgets) { + if (!visibleIds.has(id)) { + rendered.widget.domNode.remove(); + rendered.disposables.dispose(); + this.renderedWidgets.delete(id); + } + } + + for (const descriptor of ChatInputPartWidgetsRegistry.getWidgets()) { + if (!visibleIds.has(descriptor.id)) { + continue; + } + + if (!this.renderedWidgets.has(descriptor.id)) { + const disposables = new DisposableStore(); + const widget = this.instantiationService.createInstance(descriptor.ctor); + disposables.add(widget); + disposables.add(widget.onDidChangeHeight(() => this._onDidChangeHeight.fire())); + + this.renderedWidgets.set(descriptor.id, { descriptor, widget, disposables }); + this.container.appendChild(widget.domNode); + } + } + + this._onDidChangeHeight.fire(); + } + + get height(): number { + let total = 0; + for (const rendered of this.renderedWidgets.values()) { + total += rendered.widget.height; + } + return total; + } + + override dispose(): void { + for (const rendered of this.renderedWidgets.values()) { + rendered.disposables.dispose(); + } + this.renderedWidgets.clear(); + super.dispose(); + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chatStatusWidget.ts b/src/vs/workbench/contrib/chat/browser/chatStatusWidget.ts new file mode 100644 index 00000000000..2e21cd6cfea --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatStatusWidget.ts @@ -0,0 +1,138 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import './media/chatStatusWidget.css'; +import * as dom from '../../../../base/browser/dom.js'; +import { Button } from '../../../../base/browser/ui/button/button.js'; +import { Emitter, Event } from '../../../../base/common/event.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { localize, localize2 } from '../../../../nls.js'; +import { Categories } from '../../../../platform/action/common/actionCommonCategories.js'; +import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js'; +import { ICommandService } from '../../../../platform/commands/common/commands.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { IContextKeyService, ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; +import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; +import { defaultButtonStyles } from '../../../../platform/theme/browser/defaultStyles.js'; +import { ChatEntitlement, ChatEntitlementContextKeys, IChatEntitlementService } from '../../../services/chat/common/chatEntitlementService.js'; +import { ChatInputPartWidgetsRegistry, IChatInputPartWidget } from './chatInputPartWidgets.js'; +import { ChatContextKeys } from '../common/chatContextKeys.js'; + +const $ = dom.$; + +/** + * Widget that displays a status message with an optional action button. + * Only shown for free tier users when the setting is enabled (experiment controlled via onExP tag). + */ +export class ChatStatusWidget extends Disposable implements IChatInputPartWidget { + + static readonly ID = 'chatStatusWidget'; + + readonly domNode: HTMLElement; + + private readonly _onDidChangeHeight = this._register(new Emitter()); + readonly onDidChangeHeight: Event = this._onDidChangeHeight.event; + + private messageElement: HTMLElement | undefined; + private actionButton: Button | undefined; + private _isEnabled = false; + + constructor( + @IChatEntitlementService private readonly chatEntitlementService: IChatEntitlementService, + @ICommandService private readonly commandService: ICommandService, + @IConfigurationService private readonly configurationService: IConfigurationService, + ) { + super(); + + this.domNode = $('.chat-status-widget'); + this.domNode.style.display = 'none'; + this.initializeIfEnabled(); + } + + private initializeIfEnabled(): void { + const isEnabled = this.configurationService.getValue('chat.statusWidget.enabled'); + if (!isEnabled) { + return; + } + + this._isEnabled = true; + if (!this.chatEntitlementService.isInternal) { + return; + } + + this.createWidgetContent(); + this.updateContent(); + this.domNode.style.display = ''; + + this._register(this.chatEntitlementService.onDidChangeEntitlement(() => { + this.updateContent(); + })); + + this._onDidChangeHeight.fire(); + } + + get height(): number { + return this._isEnabled ? this.domNode.offsetHeight : 0; + } + + private createWidgetContent(): void { + const contentContainer = $('.chat-status-content'); + this.messageElement = $('.chat-status-message'); + contentContainer.appendChild(this.messageElement); + + const actionContainer = $('.chat-status-action'); + this.actionButton = this._register(new Button(actionContainer, { + ...defaultButtonStyles, + supportIcons: true + })); + this.actionButton.element.classList.add('chat-status-button'); + + this._register(this.actionButton.onDidClick(async () => { + const commandId = this.chatEntitlementService.entitlement === ChatEntitlement.Free + ? 'workbench.action.chat.upgradePlan' + : 'workbench.action.chat.manageOverages'; + await this.commandService.executeCommand(commandId); + })); + + this.domNode.appendChild(contentContainer); + this.domNode.appendChild(actionContainer); + } + + private updateContent(): void { + if (!this.messageElement || !this.actionButton) { + return; + } + + this.messageElement.textContent = localize('chat.quotaExceeded.message', "Free tier chat message limit reached."); + this.actionButton.label = localize('chat.quotaExceeded.increaseLimit', "Increase Limit"); + + this._onDidChangeHeight.fire(); + } +} + +// TODO@bhavyaus remove this command after testing complete with team +registerAction2(class ToggleChatQuotaExceededAction extends Action2 { + constructor() { + super({ + id: 'workbench.action.chat.toggleStatusWidget', + title: localize2('chat.toggleStatusWidget.label', "Toggle Chat Status Widget State"), + f1: true, + category: Categories.Developer, + precondition: ContextKeyExpr.and(ChatContextKeys.enabled, ChatEntitlementContextKeys.Entitlement.internal), + }); + } + + run(accessor: ServicesAccessor): void { + const contextKeyService = accessor.get(IContextKeyService); + const currentValue = ChatEntitlementContextKeys.chatQuotaExceeded.getValue(contextKeyService) ?? false; + ChatEntitlementContextKeys.chatQuotaExceeded.bindTo(contextKeyService).set(!currentValue); + } +}); + +ChatInputPartWidgetsRegistry.register( + ChatStatusWidget.ID, + ChatStatusWidget, + ContextKeyExpr.and(ChatContextKeys.chatQuotaExceeded, ChatContextKeys.chatSessionIsEmpty) +); diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index ee174b026cf..3b216929a49 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -274,6 +274,7 @@ export class ChatWidget extends Disposable implements IChatWidget { }; private readonly _lockedToCodingAgentContextKey: IContextKey; private readonly _agentSupportsAttachmentsContextKey: IContextKey; + private readonly _sessionIsEmptyContextKey: IContextKey; private _attachmentCapabilities: IChatAgentAttachmentCapabilities = supportsAllAttachments; // Cache for prompt file descriptions to avoid async calls during rendering @@ -382,6 +383,7 @@ export class ChatWidget extends Disposable implements IChatWidget { this._lockedToCodingAgentContextKey = ChatContextKeys.lockedToCodingAgent.bindTo(this.contextKeyService); this._agentSupportsAttachmentsContextKey = ChatContextKeys.agentSupportsAttachments.bindTo(this.contextKeyService); + this._sessionIsEmptyContextKey = ChatContextKeys.chatSessionIsEmpty.bindTo(this.contextKeyService); this.viewContext = viewContext ?? {}; @@ -1983,6 +1985,7 @@ export class ChatWidget extends Disposable implements IChatWidget { })); const inputState = model.inputModel.state.get(); this.input.initForNewChatModel(inputState, model.getRequests().length === 0); + this._sessionIsEmptyContextKey.set(model.getRequests().length === 0); this.refreshParsedInput(); this.viewModelDisposables.add(model.onDidChange((e) => { @@ -1993,11 +1996,13 @@ export class ChatWidget extends Disposable implements IChatWidget { } if (e.kind === 'addRequest') { this.inputPart.clearTodoListWidget(this.viewModel?.sessionResource, false); + this._sessionIsEmptyContextKey.set(false); } // Hide widget on request removal if (e.kind === 'removeRequest') { this.inputPart.clearTodoListWidget(this.viewModel?.sessionResource, true); this.chatSuggestNextWidget.hide(); + this._sessionIsEmptyContextKey.set((this.viewModel?.model.getRequests().length ?? 0) === 0); } // Show next steps widget when response completes (not when request starts) if (e.kind === 'completedRequest') { diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index dca62b1f784..23e0d88e6e2 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -759,8 +759,9 @@ have to be updated for changes to the rules above, or to support more deeply nes } .interactive-input-part:has(.chat-editing-session > .chat-editing-session-container) .chat-input-container, -.interactive-input-part:has(.chat-todo-list-widget-container > .chat-todo-list-widget.has-todos) .chat-input-container { - /* Remove top border radius when editing session or todo list is present */ +.interactive-input-part:has(.chat-todo-list-widget-container > .chat-todo-list-widget.has-todos) .chat-input-container, +.interactive-input-part:has(.chat-input-widgets-container > .chat-status-widget:not([style*="display: none"])) .chat-input-container { + /* Remove top border radius when editing session, todo list, or status widget is present */ border-top-left-radius: 0; border-top-right-radius: 0; } @@ -1088,6 +1089,12 @@ have to be updated for changes to the rules above, or to support more deeply nes background-color: var(--vscode-toolbar-hoverBackground); } +.interactive-session .interactive-input-part > .chat-input-widgets-container { + margin-bottom: -4px; + width: 100%; + position: relative; +} + /* Chat Todo List Widget Container - mirrors chat-editing-session styling */ .interactive-session .interactive-input-part > .chat-todo-list-widget-container { margin-bottom: -4px; diff --git a/src/vs/workbench/contrib/chat/browser/media/chatStatusWidget.css b/src/vs/workbench/contrib/chat/browser/media/chatStatusWidget.css new file mode 100644 index 00000000000..1dac7ac8072 --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/media/chatStatusWidget.css @@ -0,0 +1,57 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +.interactive-session .interactive-input-part > .chat-input-widgets-container .chat-status-widget { + padding: 6px 3px 6px 3px; + box-sizing: border-box; + border: 1px solid var(--vscode-input-border, transparent); + background-color: var(--vscode-editor-background); + border-bottom: none; + border-top-left-radius: 4px; + border-top-right-radius: 4px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; +} + +.interactive-session .interactive-input-part > .chat-input-widgets-container .chat-status-widget .chat-status-content { + display: flex; + align-items: center; + flex: 1; + min-width: 0; + padding-left: 8px; +} + +.interactive-session .interactive-input-part > .chat-input-widgets-container .chat-status-widget .chat-status-message { + font-size: 11px; + line-height: 16px; + color: var(--vscode-foreground); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.interactive-session .interactive-input-part > .chat-input-widgets-container .chat-status-widget .chat-status-action { + flex-shrink: 0; + padding-right: 4px; +} + +.interactive-session .interactive-input-part > .chat-input-widgets-container .chat-status-widget .chat-status-button { + font-size: 11px; + padding: 2px 8px; + min-width: unset; + height: 22px; +} + +.interactive-session .interactive-input-part > .chat-input-widgets-container:has(.chat-status-widget:not([style*="display: none"])) + .chat-todo-list-widget-container .chat-todo-list-widget { + border-top-left-radius: 0; + border-top-right-radius: 0; +} + +.interactive-session .interactive-input-part > .chat-input-widgets-container:has(.chat-status-widget:not([style*="display: none"])) + .chat-todo-list-widget-container:not(:has(.chat-todo-list-widget.has-todos)) + .chat-editing-session .chat-editing-session-container { + border-top-left-radius: 0; + border-top-right-radius: 0; +} diff --git a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts index 6e0ffdf9db6..7f8b5e6cae2 100644 --- a/src/vs/workbench/contrib/chat/common/chatContextKeys.ts +++ b/src/vs/workbench/contrib/chat/common/chatContextKeys.ts @@ -61,6 +61,7 @@ export namespace ChatContextKeys { export const location = new RawContextKey('chatLocation', undefined); export const inQuickChat = new RawContextKey('quickChatHasFocus', false, { type: 'boolean', description: localize('inQuickChat', "True when the quick chat UI has focus, false otherwise.") }); export const hasFileAttachments = new RawContextKey('chatHasFileAttachments', false, { type: 'boolean', description: localize('chatHasFileAttachments', "True when the chat has file attachments.") }); + export const chatSessionIsEmpty = new RawContextKey('chatSessionIsEmpty', true, { type: 'boolean', description: localize('chatSessionIsEmpty', "True when the current chat session has no requests.") }); export const remoteJobCreating = new RawContextKey('chatRemoteJobCreating', false, { type: 'boolean', description: localize('chatRemoteJobCreating', "True when a remote coding agent job is being created.") }); export const hasRemoteCodingAgent = new RawContextKey('hasRemoteCodingAgent', false, localize('hasRemoteCodingAgent', "Whether any remote coding agent is available"));