From cb6b626928779e191da0fb80eb3797115b544c7d Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Fri, 21 Mar 2025 13:28:13 -0700 Subject: [PATCH 1/4] Add chat status API proposal Adds a new proposal that lets extensions contribute to a basic status item for chat --- .../common/extensionsApiProposals.ts | 3 + .../api/browser/extensionHost.contribution.ts | 1 + .../api/browser/mainThreadChatStatus.ts | 33 ++++++ .../api/browser/statusBarExtensionPoint.ts | 2 +- .../workbench/api/common/extHost.api.impl.ts | 26 +++-- .../workbench/api/common/extHost.protocol.ts | 15 ++- .../workbench/api/common/extHostChatStatus.ts | 99 ++++++++++++++++ .../browser/parts/statusbar/statusbarItem.ts | 1 + .../contrib/chat/browser/chatStatus.ts | 108 ++++++++++++++---- .../chat/browser/chatStatusItemService.ts | 62 ++++++++++ .../contrib/chat/browser/media/chatStatus.css | 14 +++ .../vscode.proposed.chatStatusItem.d.ts | 61 ++++++++++ 12 files changed, 389 insertions(+), 36 deletions(-) create mode 100644 src/vs/workbench/api/browser/mainThreadChatStatus.ts create mode 100644 src/vs/workbench/api/common/extHostChatStatus.ts create mode 100644 src/vs/workbench/contrib/chat/browser/chatStatusItemService.ts create mode 100644 src/vscode-dts/vscode.proposed.chatStatusItem.d.ts diff --git a/src/vs/platform/extensions/common/extensionsApiProposals.ts b/src/vs/platform/extensions/common/extensionsApiProposals.ts index 03510fbfd14..51799185777 100644 --- a/src/vs/platform/extensions/common/extensionsApiProposals.ts +++ b/src/vs/platform/extensions/common/extensionsApiProposals.ts @@ -44,6 +44,9 @@ const _allApiProposals = { chatReferenceDiagnostic: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatReferenceDiagnostic.d.ts', }, + chatStatusItem: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatStatusItem.d.ts', + }, chatTab: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatTab.d.ts', }, diff --git a/src/vs/workbench/api/browser/extensionHost.contribution.ts b/src/vs/workbench/api/browser/extensionHost.contribution.ts index f0bf62c67ff..7a778aa030b 100644 --- a/src/vs/workbench/api/browser/extensionHost.contribution.ts +++ b/src/vs/workbench/api/browser/extensionHost.contribution.ts @@ -89,6 +89,7 @@ import './mainThreadProfileContentHandlers.js'; import './mainThreadAiRelatedInformation.js'; import './mainThreadAiEmbeddingVector.js'; import './mainThreadMcp.js'; +import './mainThreadChatStatus.js'; export class ExtensionPoints implements IWorkbenchContribution { diff --git a/src/vs/workbench/api/browser/mainThreadChatStatus.ts b/src/vs/workbench/api/browser/mainThreadChatStatus.ts new file mode 100644 index 00000000000..dcf37738f44 --- /dev/null +++ b/src/vs/workbench/api/browser/mainThreadChatStatus.ts @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable } from '../../../base/common/lifecycle.js'; +import { IChatStatusItemService } from '../../contrib/chat/browser/chatStatusItemService.js'; +import { IExtHostContext, extHostNamedCustomer } from '../../services/extensions/common/extHostCustomers.js'; +import { ChatStatusItemDto, MainContext, MainThreadChatStatusShape } from '../common/extHost.protocol.js'; + +@extHostNamedCustomer(MainContext.MainThreadChatStatus) +export class MainThreadChatStatus extends Disposable implements MainThreadChatStatusShape { + + constructor( + _extHostContext: IExtHostContext, + @IChatStatusItemService private readonly _chatStatusItemService: IChatStatusItemService, + ) { + super(); + } + + $setEntry(id: string, entry: ChatStatusItemDto): void { + this._chatStatusItemService.setOrUpdateEntry({ + id, + label: entry.title, + description: entry.description, + details: entry.details, + }); + } + + $disposeEntry(id: string): void { + this._chatStatusItemService.deleteEntry(id); + } +} diff --git a/src/vs/workbench/api/browser/statusBarExtensionPoint.ts b/src/vs/workbench/api/browser/statusBarExtensionPoint.ts index fa0d7fd2307..46a8443e46a 100644 --- a/src/vs/workbench/api/browser/statusBarExtensionPoint.ts +++ b/src/vs/workbench/api/browser/statusBarExtensionPoint.ts @@ -58,7 +58,7 @@ export interface IExtensionStatusBarItemService { } -class ExtensionStatusBarItemService implements IExtensionStatusBarItemService { +export class ExtensionStatusBarItemService implements IExtensionStatusBarItemService { declare readonly _serviceBrand: undefined; diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index e6d5b024f66..3549f85ab6e 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import type * as vscode from 'vscode'; import { CancellationTokenSource } from '../../../base/common/cancellation.js'; import * as errors from '../../../base/common/errors.js'; import { Emitter, Event } from '../../../base/common/event.js'; @@ -21,6 +22,12 @@ import { ILogService, ILoggerService, LogLevel } from '../../../platform/log/com import { getRemoteName } from '../../../platform/remote/common/remoteHosts.js'; import { TelemetryTrustedValue } from '../../../platform/telemetry/common/telemetryUtils.js'; import { EditSessionIdentityMatch } from '../../../platform/workspace/common/editSessions.js'; +import { DebugConfigurationProviderTriggerKind } from '../../contrib/debug/common/debug.js'; +import { ExtensionDescriptionRegistry } from '../../services/extensions/common/extensionDescriptionRegistry.js'; +import { UIKind } from '../../services/extensions/common/extensionHostProtocol.js'; +import { checkProposedApiEnabled, isProposedApiEnabled } from '../../services/extensions/common/extensions.js'; +import { ProxyIdentifier } from '../../services/extensions/common/proxyIdentifier.js'; +import { ExcludeSettingOptions, TextSearchCompleteMessageType, TextSearchContext2, TextSearchMatch2 } from '../../services/search/common/searchExtTypes.js'; import { CandidatePortSource, ExtHostContext, ExtHostLogLevelServiceShape, MainContext } from './extHost.protocol.js'; import { ExtHostRelatedInformation } from './extHostAiRelatedInformation.js'; import { ExtHostApiCommands } from './extHostApiCommands.js'; @@ -28,8 +35,10 @@ import { IExtHostApiDeprecationService } from './extHostApiDeprecationService.js import { IExtHostAuthentication } from './extHostAuthentication.js'; import { ExtHostBulkEdits } from './extHostBulkEdits.js'; import { ExtHostChatAgents2 } from './extHostChatAgents2.js'; +import { ExtHostChatStatus } from './extHostChatStatus.js'; import { ExtHostClipboard } from './extHostClipboard.js'; import { ExtHostEditorInsets } from './extHostCodeInsets.js'; +import { ExtHostCodeMapper } from './extHostCodeMapper.js'; import { IExtHostCommands } from './extHostCommands.js'; import { createExtHostComments } from './extHostComments.js'; import { ExtHostConfigProvider, IExtHostConfiguration } from './extHostConfiguration.js'; @@ -59,6 +68,7 @@ import { IExtHostLanguageModels } from './extHostLanguageModels.js'; import { ExtHostLanguages } from './extHostLanguages.js'; import { IExtHostLocalizationService } from './extHostLocalizationService.js'; import { IExtHostManagedSockets } from './extHostManagedSockets.js'; +import { IExtHostMpcService } from './extHostMcp.js'; import { ExtHostMessageService } from './extHostMessageService.js'; import { ExtHostNotebookController } from './extHostNotebook.js'; import { ExtHostNotebookDocumentSaveParticipant } from './extHostNotebookDocumentSaveParticipant.js'; @@ -100,15 +110,6 @@ import { ExtHostWebviewPanels } from './extHostWebviewPanels.js'; import { ExtHostWebviewViews } from './extHostWebviewView.js'; import { IExtHostWindow } from './extHostWindow.js'; import { IExtHostWorkspace } from './extHostWorkspace.js'; -import { DebugConfigurationProviderTriggerKind } from '../../contrib/debug/common/debug.js'; -import { ExtensionDescriptionRegistry } from '../../services/extensions/common/extensionDescriptionRegistry.js'; -import { UIKind } from '../../services/extensions/common/extensionHostProtocol.js'; -import { checkProposedApiEnabled, isProposedApiEnabled } from '../../services/extensions/common/extensions.js'; -import { ProxyIdentifier } from '../../services/extensions/common/proxyIdentifier.js'; -import { ExcludeSettingOptions, TextSearchCompleteMessageType, TextSearchContext2, TextSearchMatch2 } from '../../services/search/common/searchExtTypes.js'; -import type * as vscode from 'vscode'; -import { ExtHostCodeMapper } from './extHostCodeMapper.js'; -import { IExtHostMpcService } from './extHostMcp.js'; export interface IExtensionRegistries { mine: ExtensionDescriptionRegistry; @@ -231,6 +232,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostClipboard = new ExtHostClipboard(rpcProtocol); const extHostMessageService = new ExtHostMessageService(rpcProtocol, extHostLogService); const extHostDialogs = new ExtHostDialogs(rpcProtocol); + const extHostChatStatus = new ExtHostChatStatus(rpcProtocol); // Register API-ish commands ExtHostApiCommands.register(extHostCommands); @@ -937,7 +939,11 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I get nativeHandle(): Uint8Array | undefined { checkProposedApiEnabled(extension, 'nativeWindowHandle'); return extHostWindow.nativeHandle; - } + }, + createChatStatusItem: (id: string) => { + checkProposedApiEnabled(extension, 'chatStatusItem'); + return extHostChatStatus.createChatStatusItem(extension, id); + }, }; // namespace: workspace diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 35f04460d7c..ba304fcf60a 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -3061,6 +3061,18 @@ export interface MainThreadTestingShape { $markTestRetired(testIds: string[] | undefined): void; } +export type ChatStatusItemDto = { + id: string; + title: string; + description: string | undefined; + details: string | undefined; +}; + +export interface MainThreadChatStatusShape { + $setEntry(id: string, entry: ChatStatusItemDto): void; + $disposeEntry(id: string): void; +} + // --- proxy identifiers export const MainContext = { @@ -3134,7 +3146,8 @@ export const MainContext = { MainThreadLocalization: createProxyIdentifier('MainThreadLocalizationShape'), MainThreadMcp: createProxyIdentifier('MainThreadMcpShape'), MainThreadAiRelatedInformation: createProxyIdentifier('MainThreadAiRelatedInformation'), - MainThreadAiEmbeddingVector: createProxyIdentifier('MainThreadAiEmbeddingVector') + MainThreadAiEmbeddingVector: createProxyIdentifier('MainThreadAiEmbeddingVector'), + MainThreadChatStatus: createProxyIdentifier('MainThreadChatStatus'), }; export const ExtHostContext = { diff --git a/src/vs/workbench/api/common/extHostChatStatus.ts b/src/vs/workbench/api/common/extHostChatStatus.ts new file mode 100644 index 00000000000..5dc2e4bb0df --- /dev/null +++ b/src/vs/workbench/api/common/extHostChatStatus.ts @@ -0,0 +1,99 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type * as vscode from 'vscode'; +import * as extHostProtocol from './extHost.protocol.js'; +import { ExtensionIdentifier, IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; + +export class ExtHostChatStatus { + + private readonly _proxy: extHostProtocol.MainThreadChatStatusShape; + + private readonly _items = new Map(); + + constructor( + mainContext: extHostProtocol.IMainContext + ) { + this._proxy = mainContext.getProxy(extHostProtocol.MainContext.MainThreadChatStatus); + } + + createChatStatusItem(extension: IExtensionDescription, id: string): vscode.ChatStatusItem { + const internalId = asChatItemIdentifier(extension.identifier, id); + if (this._items.has(internalId)) { + throw new Error(`Chat status item '${id}' already exists`); + } + + const state: extHostProtocol.ChatStatusItemDto = { + id: internalId, + title: '', + description: '', + details: '', + }; + + let disposed = false; + let visible = false; + const syncState = () => { + if (disposed) { + throw new Error('Chat status item is disposed'); + } + + if (!visible) { + return; + } + + this._proxy.$setEntry(id, state); + }; + + const item = Object.freeze({ + id: id, + + get title(): string { + return state.title; + }, + set title(value: string) { + state.title = value; + syncState(); + }, + + get details(): string | undefined { + return state.details; + }, + set details(value: string | undefined) { + state.details = value; + syncState(); + }, + + get description(): string | undefined { + return state.description; + }, + set description(value: string | undefined) { + state.description = value; + syncState(); + }, + + show: () => { + visible = true; + syncState(); + }, + hide: () => { + visible = false; + this._proxy.$disposeEntry(id); + }, + dispose: () => { + disposed = true; + this._proxy.$disposeEntry(id); + this._items.delete(internalId); + }, + }); + + this._items.set(internalId, item); + return item; + } +} + +function asChatItemIdentifier(extension: ExtensionIdentifier, id: string): string { + return `${ExtensionIdentifier.toKey(extension)}.${id}`; +} + diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarItem.ts b/src/vs/workbench/browser/parts/statusbar/statusbarItem.ts index 241526727af..ce79ac6dfb0 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarItem.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarItem.ts @@ -120,6 +120,7 @@ export class StatusbarEntryItem extends Disposable { if (isTooltipWithCommands(entry.tooltip)) { hoverTooltip = entry.tooltip.content; hoverOptions = { + trapFocus: true, actions: entry.tooltip.commands.map(command => ({ commandId: command.id, label: command.title, diff --git a/src/vs/workbench/contrib/chat/browser/chatStatus.ts b/src/vs/workbench/contrib/chat/browser/chatStatus.ts index b0f6a1e507d..740196908b2 100644 --- a/src/vs/workbench/contrib/chat/browser/chatStatus.ts +++ b/src/vs/workbench/contrib/chat/browser/chatStatus.ts @@ -3,31 +3,37 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import './media/chatStatus.css'; -import { safeIntl } from '../../../../base/common/date.js'; -import { Disposable, DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js'; -import { language } from '../../../../base/common/platform.js'; -import { localize } from '../../../../nls.js'; -import { IWorkbenchContribution } from '../../../common/contributions.js'; -import { IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarService, ShowTooltipCommand, StatusbarAlignment, StatusbarEntryKind } from '../../../services/statusbar/browser/statusbar.js'; +import * as dom from '../../../../base/browser/dom.js'; import { $, addDisposableListener, append, clearNode, EventHelper, EventType } from '../../../../base/browser/dom.js'; -import { ChatEntitlement, ChatEntitlementService, ChatSentiment, IChatEntitlementService } from '../common/chatEntitlementService.js'; -import { CancellationToken } from '../../../../base/common/cancellation.js'; -import { defaultButtonStyles, defaultCheckboxStyles } from '../../../../platform/theme/browser/defaultStyles.js'; -import { Checkbox } from '../../../../base/browser/ui/toggle/toggle.js'; -import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { ICommandService } from '../../../../platform/commands/common/commands.js'; -import { Lazy } from '../../../../base/common/lazy.js'; -import { contrastBorder, inputValidationErrorBorder, inputValidationInfoBorder, inputValidationWarningBorder, registerColor, transparent } from '../../../../platform/theme/common/colorRegistry.js'; -import { IHoverService } from '../../../../platform/hover/browser/hover.js'; -import { Color } from '../../../../base/common/color.js'; import { Gesture, EventType as TouchEventType } from '../../../../base/browser/touch.js'; -import { IEditorService } from '../../../services/editor/common/editorService.js'; -import product from '../../../../platform/product/common/product.js'; +import { Button } from '../../../../base/browser/ui/button/button.js'; +import { renderLabelWithIcons } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; +import { Checkbox } from '../../../../base/browser/ui/toggle/toggle.js'; +import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { Color } from '../../../../base/common/color.js'; +import { safeIntl } from '../../../../base/common/date.js'; +import { Lazy } from '../../../../base/common/lazy.js'; +import { Disposable, DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js'; +import { parseLinkedText } from '../../../../base/common/linkedText.js'; +import { language } from '../../../../base/common/platform.js'; import { isObject } from '../../../../base/common/types.js'; import { ILanguageService } from '../../../../editor/common/languages/language.js'; +import { localize } from '../../../../nls.js'; +import { ICommandService } from '../../../../platform/commands/common/commands.js'; +import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; +import { IHoverService } from '../../../../platform/hover/browser/hover.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { Button } from '../../../../base/browser/ui/button/button.js'; +import { Link } from '../../../../platform/opener/browser/link.js'; +import { IOpenerService } from '../../../../platform/opener/common/opener.js'; +import product from '../../../../platform/product/common/product.js'; +import { defaultButtonStyles, defaultCheckboxStyles } from '../../../../platform/theme/browser/defaultStyles.js'; +import { contrastBorder, inputValidationErrorBorder, inputValidationInfoBorder, inputValidationWarningBorder, registerColor, transparent } from '../../../../platform/theme/common/colorRegistry.js'; +import { IWorkbenchContribution } from '../../../common/contributions.js'; +import { IEditorService } from '../../../services/editor/common/editorService.js'; +import { IStatusbarEntry, IStatusbarEntryAccessor, IStatusbarService, ShowTooltipCommand, StatusbarAlignment, StatusbarEntryKind } from '../../../services/statusbar/browser/statusbar.js'; +import { ChatEntitlement, ChatEntitlementService, ChatSentiment, IChatEntitlementService } from '../common/chatEntitlementService.js'; +import { ChatStatusEntry, IChatStatusItemService } from './chatStatusItemService.js'; +import './media/chatStatus.css'; //#region --- colors @@ -97,9 +103,9 @@ export class ChatStatusBarEntry extends Disposable implements IWorkbenchContribu private dashboard = new Lazy(() => this.instantiationService.createInstance(ChatStatusDashboard)); constructor( - @IStatusbarService private readonly statusbarService: IStatusbarService, @IChatEntitlementService private readonly chatEntitlementService: ChatEntitlementService, - @IInstantiationService private readonly instantiationService: IInstantiationService + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IStatusbarService private readonly statusbarService: IStatusbarService, ) { super(); @@ -209,11 +215,13 @@ class ChatStatusDashboard extends Disposable { constructor( @IChatEntitlementService private readonly chatEntitlementService: ChatEntitlementService, + @IChatStatusItemService private readonly chatStatusItemService: IChatStatusItemService, + @ICommandService private readonly commandService: ICommandService, @IConfigurationService private readonly configurationService: IConfigurationService, - @IHoverService private readonly hoverService: IHoverService, @IEditorService private readonly editorService: IEditorService, + @IHoverService private readonly hoverService: IHoverService, @ILanguageService private readonly languageService: ILanguageService, - @ICommandService private readonly commandService: ICommandService + @IOpenerService private readonly openerService: IOpenerService, ) { super(); } @@ -268,6 +276,29 @@ class ChatStatusDashboard extends Disposable { })(); } + // Contributions + { + for (const item of this.chatStatusItemService.getEntries()) { + addSeparator(undefined); + const chatItemDisposables = disposables.add(new MutableDisposable()); + + let rendered = this.renderContributedChatStatusItem(item); + chatItemDisposables.value = rendered.disposables; + this.element.appendChild(rendered.element); + + disposables.add(this.chatStatusItemService.onDidChange(e => { + if (e.entry.id === item.id) { + const oldEl = rendered.element; + + rendered = this.renderContributedChatStatusItem(e.entry); + chatItemDisposables.value = rendered.disposables; + + oldEl.replaceWith(rendered.element); + } + })); + } + } + // Settings { addSeparator(localize('settingsTitle', "Settings")); @@ -293,6 +324,35 @@ class ChatStatusDashboard extends Disposable { return this.element; } + private renderContributedChatStatusItem(item: ChatStatusEntry): { element: HTMLElement; disposables: DisposableStore } { + const disposables = new DisposableStore(); + + const entryEl = $('div.contribution'); + + const headerEl = entryEl.appendChild($('div.header', undefined, item.label)); + if (item.description) { + const descriptionEl = headerEl.appendChild($('span.description')); + this._renderTextPlus(descriptionEl, item.description, disposables); + } + + if (item.details) { + const itemElement = entryEl.appendChild($('div.detail-item')); + this._renderTextPlus(itemElement, item.details, disposables); + } + return { element: entryEl, disposables }; + } + + private _renderTextPlus(target: HTMLElement, text: string, store: DisposableStore): void { + for (const node of parseLinkedText(text).nodes) { + if (typeof node === 'string') { + const parts = renderLabelWithIcons(node); + dom.append(target, ...parts); + } else { + store.add(new Link(target, node, undefined, this.hoverService, this.openerService)); + } + } + } + private runCommandAndClose(command: { id: string; args?: unknown[] } | Function): void { if (typeof command === 'function') { command(); diff --git a/src/vs/workbench/contrib/chat/browser/chatStatusItemService.ts b/src/vs/workbench/contrib/chat/browser/chatStatusItemService.ts new file mode 100644 index 00000000000..31e08bbaf2a --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatStatusItemService.ts @@ -0,0 +1,62 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; +import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; + +export const IChatStatusItemService = createDecorator('IChatStatusItemService'); + +export interface IChatStatusItemService { + readonly _serviceBrand: undefined; + + readonly onDidChange: Event; + + setOrUpdateEntry(entry: ChatStatusEntry): void; + + deleteEntry(id: string): void; + + getEntries(): Iterable; +} + + +export interface IChatStatusItemChangeEvent { + readonly entry: ChatStatusEntry; +} + +export type ChatStatusEntry = { + id: string; + label: string; + description: string | undefined; + details: string | undefined; +}; + + +class ChatStatusItemService implements IChatStatusItemService { + readonly _serviceBrand: undefined; + + private readonly _entries = new Map(); + + private readonly _onDidChange = new Emitter(); + readonly onDidChange = this._onDidChange.event; + + setOrUpdateEntry(entry: ChatStatusEntry): void { + const isUpdate = this._entries.has(entry.id); + this._entries.set(entry.id, entry); + if (isUpdate) { + this._onDidChange.fire({ entry }); + } + } + + deleteEntry(id: string): void { + this._entries.delete(id); + } + + getEntries(): Iterable { + return this._entries.values(); + } +} + +registerSingleton(IChatStatusItemService, ChatStatusItemService, InstantiationType.Delayed); diff --git a/src/vs/workbench/contrib/chat/browser/media/chatStatus.css b/src/vs/workbench/contrib/chat/browser/media/chatStatus.css index 82f35aebb20..2f7675e558a 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chatStatus.css +++ b/src/vs/workbench/contrib/chat/browser/media/chatStatus.css @@ -19,6 +19,14 @@ color: var(--vscode-descriptionForeground); margin-bottom: 4px; font-weight: 600; + display: flex; + flex-direction: row; + gap: 6px; + + .description { + margin-left: auto; + font-weight: normal; + } } .chat-status-bar-entry-tooltip div.description { @@ -111,3 +119,9 @@ .chat-status-bar-entry-tooltip .settings .setting.disabled .setting-label { color: var(--vscode-disabledForeground); } + +/* Contributions */ + +.chat-status-bar-entry-tooltip .detail-item { + +} diff --git a/src/vscode-dts/vscode.proposed.chatStatusItem.d.ts b/src/vscode-dts/vscode.proposed.chatStatusItem.d.ts new file mode 100644 index 00000000000..6b3c3dc6baf --- /dev/null +++ b/src/vscode-dts/vscode.proposed.chatStatusItem.d.ts @@ -0,0 +1,61 @@ +/*--------------------------------------------------------------------------------------------- + * 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' { + + export interface ChatStatusItem { + /** + * The identifier of this item. + */ + readonly id: string; + + /** + * The main name of the entry, like 'Indexing Status' + */ + title: string; + + /** + * Optional additional description of the entry. + * + * This is rendered less prominent on the same line as the title. Supports Markdown style links and rendering of + * {@link ThemeIcon theme icons} via the `$()`-syntax. + */ + description: string | undefined; + + /** + * Optional additional details of the entry. + * + * This is rendered less prominent in a separate line. Supports Markdown style links and rendering of + * {@link ThemeIcon theme icons} via the `$()`-syntax. + */ + details: string | undefined; + + /** + * Shows the entry in the chat status. + */ + show(): void; + + /** + * Hide the entry in the chat status. + */ + hide(): void; + + /** + * Dispose and free associated resources + */ + dispose(): void; + } + + namespace window { + /** + * Create a new chat status item. + * + * @param id The unique identifier of the status bar item. + * + * @returns A new chat status item. + */ + export function createChatStatusItem(id: string): ChatStatusItem; + } +} From 63dc0b70c53f80bfcc6b4d1479e1ae86a9b0cc9a Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Mon, 24 Mar 2025 08:22:52 -0700 Subject: [PATCH 2/4] Remove extra edits --- src/vs/workbench/api/browser/statusBarExtensionPoint.ts | 2 +- src/vs/workbench/browser/parts/statusbar/statusbarItem.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/vs/workbench/api/browser/statusBarExtensionPoint.ts b/src/vs/workbench/api/browser/statusBarExtensionPoint.ts index 46a8443e46a..fa0d7fd2307 100644 --- a/src/vs/workbench/api/browser/statusBarExtensionPoint.ts +++ b/src/vs/workbench/api/browser/statusBarExtensionPoint.ts @@ -58,7 +58,7 @@ export interface IExtensionStatusBarItemService { } -export class ExtensionStatusBarItemService implements IExtensionStatusBarItemService { +class ExtensionStatusBarItemService implements IExtensionStatusBarItemService { declare readonly _serviceBrand: undefined; diff --git a/src/vs/workbench/browser/parts/statusbar/statusbarItem.ts b/src/vs/workbench/browser/parts/statusbar/statusbarItem.ts index ce79ac6dfb0..241526727af 100644 --- a/src/vs/workbench/browser/parts/statusbar/statusbarItem.ts +++ b/src/vs/workbench/browser/parts/statusbar/statusbarItem.ts @@ -120,7 +120,6 @@ export class StatusbarEntryItem extends Disposable { if (isTooltipWithCommands(entry.tooltip)) { hoverTooltip = entry.tooltip.content; hoverOptions = { - trapFocus: true, actions: entry.tooltip.commands.map(command => ({ commandId: command.id, label: command.title, From 0c9c6e6cb12460e37f3e6be639db03a344122cb5 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Mon, 24 Mar 2025 08:29:37 -0700 Subject: [PATCH 3/4] Remove empty section --- src/vs/workbench/contrib/chat/browser/media/chatStatus.css | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/media/chatStatus.css b/src/vs/workbench/contrib/chat/browser/media/chatStatus.css index 2f7675e558a..d6dc87fd6ec 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chatStatus.css +++ b/src/vs/workbench/contrib/chat/browser/media/chatStatus.css @@ -119,9 +119,3 @@ .chat-status-bar-entry-tooltip .settings .setting.disabled .setting-label { color: var(--vscode-disabledForeground); } - -/* Contributions */ - -.chat-status-bar-entry-tooltip .detail-item { - -} From cadb511d3639af147f2aeef4dbe64b8728f48d69 Mon Sep 17 00:00:00 2001 From: Matt Bierner Date: Mon, 24 Mar 2025 14:28:30 -0700 Subject: [PATCH 4/4] Tweaks to UI --- .../api/browser/mainThreadChatStatus.ts | 2 +- .../workbench/api/common/extHost.protocol.ts | 4 ++-- .../workbench/api/common/extHostChatStatus.ts | 18 +++++++-------- .../contrib/chat/browser/chatStatus.ts | 18 ++++++++------- .../chat/browser/chatStatusItemService.ts | 4 ++-- .../contrib/chat/browser/media/chatStatus.css | 22 ++++++++++++------- .../vscode.proposed.chatStatusItem.d.ts | 8 +++---- 7 files changed, 42 insertions(+), 34 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadChatStatus.ts b/src/vs/workbench/api/browser/mainThreadChatStatus.ts index dcf37738f44..e2b8cd24fc6 100644 --- a/src/vs/workbench/api/browser/mainThreadChatStatus.ts +++ b/src/vs/workbench/api/browser/mainThreadChatStatus.ts @@ -23,7 +23,7 @@ export class MainThreadChatStatus extends Disposable implements MainThreadChatSt id, label: entry.title, description: entry.description, - details: entry.details, + detail: entry.detail, }); } diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 91739cd5810..f21aa17dc0d 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -3064,8 +3064,8 @@ export interface MainThreadTestingShape { export type ChatStatusItemDto = { id: string; title: string; - description: string | undefined; - details: string | undefined; + description: string; + detail: string | undefined; }; export interface MainThreadChatStatusShape { diff --git a/src/vs/workbench/api/common/extHostChatStatus.ts b/src/vs/workbench/api/common/extHostChatStatus.ts index 5dc2e4bb0df..38f22b171d7 100644 --- a/src/vs/workbench/api/common/extHostChatStatus.ts +++ b/src/vs/workbench/api/common/extHostChatStatus.ts @@ -29,7 +29,7 @@ export class ExtHostChatStatus { id: internalId, title: '', description: '', - details: '', + detail: '', }; let disposed = false; @@ -57,19 +57,19 @@ export class ExtHostChatStatus { syncState(); }, - get details(): string | undefined { - return state.details; + get description(): string { + return state.description; }, - set details(value: string | undefined) { - state.details = value; + set description(value: string) { + state.description = value; syncState(); }, - get description(): string | undefined { - return state.description; + get detail(): string | undefined { + return state.detail; }, - set description(value: string | undefined) { - state.description = value; + set detail(value: string | undefined) { + state.detail = value; syncState(); }, diff --git a/src/vs/workbench/contrib/chat/browser/chatStatus.ts b/src/vs/workbench/contrib/chat/browser/chatStatus.ts index cb87911c900..d9f21bc1ff4 100644 --- a/src/vs/workbench/contrib/chat/browser/chatStatus.ts +++ b/src/vs/workbench/contrib/chat/browser/chatStatus.ts @@ -329,16 +329,18 @@ class ChatStatusDashboard extends Disposable { const entryEl = $('div.contribution'); - const headerEl = entryEl.appendChild($('div.header', undefined, item.label)); - if (item.description) { - const descriptionEl = headerEl.appendChild($('span.description')); - this._renderTextPlus(descriptionEl, item.description, disposables); + entryEl.appendChild($('div.header', undefined, item.label)); + + const bodyEl = entryEl.appendChild($('div.body')); + + const descriptionEl = bodyEl.appendChild($('span.description')); + this._renderTextPlus(descriptionEl, item.description, disposables); + + if (item.detail) { + const itemElement = bodyEl.appendChild($('div.detail-item')); + this._renderTextPlus(itemElement, item.detail, disposables); } - if (item.details) { - const itemElement = entryEl.appendChild($('div.detail-item')); - this._renderTextPlus(itemElement, item.details, disposables); - } return { element: entryEl, disposables }; } diff --git a/src/vs/workbench/contrib/chat/browser/chatStatusItemService.ts b/src/vs/workbench/contrib/chat/browser/chatStatusItemService.ts index 31e08bbaf2a..3f009e58eb0 100644 --- a/src/vs/workbench/contrib/chat/browser/chatStatusItemService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatStatusItemService.ts @@ -29,8 +29,8 @@ export interface IChatStatusItemChangeEvent { export type ChatStatusEntry = { id: string; label: string; - description: string | undefined; - details: string | undefined; + description: string; + detail: string | undefined; }; diff --git a/src/vs/workbench/contrib/chat/browser/media/chatStatus.css b/src/vs/workbench/contrib/chat/browser/media/chatStatus.css index d6dc87fd6ec..081aea5ed2c 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chatStatus.css +++ b/src/vs/workbench/contrib/chat/browser/media/chatStatus.css @@ -19,14 +19,6 @@ color: var(--vscode-descriptionForeground); margin-bottom: 4px; font-weight: 600; - display: flex; - flex-direction: row; - gap: 6px; - - .description { - margin-left: auto; - font-weight: normal; - } } .chat-status-bar-entry-tooltip div.description { @@ -119,3 +111,17 @@ .chat-status-bar-entry-tooltip .settings .setting.disabled .setting-label { color: var(--vscode-disabledForeground); } + +/* Contributions */ + +.chat-status-bar-entry-tooltip .contribution .body { + display: flex; + flex-direction: row; + gap: 6px; + color: var(--vscode-descriptionForeground); + + .detail-item { + margin-left: auto; + font-weight: normal; + } +} diff --git a/src/vscode-dts/vscode.proposed.chatStatusItem.d.ts b/src/vscode-dts/vscode.proposed.chatStatusItem.d.ts index 6b3c3dc6baf..28ba4157291 100644 --- a/src/vscode-dts/vscode.proposed.chatStatusItem.d.ts +++ b/src/vscode-dts/vscode.proposed.chatStatusItem.d.ts @@ -19,18 +19,18 @@ declare module 'vscode' { /** * Optional additional description of the entry. * - * This is rendered less prominent on the same line as the title. Supports Markdown style links and rendering of + * This is rendered after the title. Supports Markdown style links (`[text](http://example.com)`) and rendering of * {@link ThemeIcon theme icons} via the `$()`-syntax. */ - description: string | undefined; + description: string; /** * Optional additional details of the entry. * - * This is rendered less prominent in a separate line. Supports Markdown style links and rendering of + * This is rendered less prominently after the title. Supports Markdown style links (`[text](http://example.com)`) and rendering of * {@link ThemeIcon theme icons} via the `$()`-syntax. */ - details: string | undefined; + detail: string | undefined; /** * Shows the entry in the chat status.