Merge pull request #244302 from mjbvz/chat-status

Add chat status API proposal
This commit is contained in:
Matt Bierner
2025-03-25 01:05:44 -07:00
committed by GitHub
10 changed files with 371 additions and 20 deletions
@@ -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',
},
@@ -89,6 +89,7 @@ import './mainThreadProfileContentHandlers.js';
import './mainThreadAiRelatedInformation.js';
import './mainThreadAiEmbeddingVector.js';
import './mainThreadMcp.js';
import './mainThreadChatStatus.js';
export class ExtensionPoints implements IWorkbenchContribution {
@@ -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,
detail: entry.detail,
});
}
$disposeEntry(id: string): void {
this._chatStatusItemService.deleteEntry(id);
}
}
+16 -10
View File
@@ -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);
@@ -929,7 +931,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
@@ -3059,6 +3059,18 @@ export interface MainThreadTestingShape {
$markTestRetired(testIds: string[] | undefined): void;
}
export type ChatStatusItemDto = {
id: string;
title: string;
description: string;
detail: string | undefined;
};
export interface MainThreadChatStatusShape {
$setEntry(id: string, entry: ChatStatusItemDto): void;
$disposeEntry(id: string): void;
}
// --- proxy identifiers
export const MainContext = {
@@ -3132,7 +3144,8 @@ export const MainContext = {
MainThreadLocalization: createProxyIdentifier<MainThreadLocalizationShape>('MainThreadLocalizationShape'),
MainThreadMcp: createProxyIdentifier<MainThreadMcpShape>('MainThreadMcpShape'),
MainThreadAiRelatedInformation: createProxyIdentifier<MainThreadAiRelatedInformationShape>('MainThreadAiRelatedInformation'),
MainThreadAiEmbeddingVector: createProxyIdentifier<MainThreadAiEmbeddingVectorShape>('MainThreadAiEmbeddingVector')
MainThreadAiEmbeddingVector: createProxyIdentifier<MainThreadAiEmbeddingVectorShape>('MainThreadAiEmbeddingVector'),
MainThreadChatStatus: createProxyIdentifier<MainThreadChatStatusShape>('MainThreadChatStatus'),
};
export const ExtHostContext = {
@@ -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<string, vscode.ChatStatusItem>();
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: '',
detail: '',
};
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<vscode.ChatStatusItem>({
id: id,
get title(): string {
return state.title;
},
set title(value: string) {
state.title = value;
syncState();
},
get description(): string {
return state.description;
},
set description(value: string) {
state.description = value;
syncState();
},
get detail(): string | undefined {
return state.detail;
},
set detail(value: string | undefined) {
state.detail = 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}`;
}
@@ -28,10 +28,13 @@ import { isObject } from '../../../../base/common/types.js';
import { ILanguageService } from '../../../../editor/common/languages/language.js';
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
import { Button } from '../../../../base/browser/ui/button/button.js';
import { renderLabelWithIcons } from '../../../../base/browser/ui/iconLabel/iconLabels.js';
import { WorkbenchActionExecutedEvent, WorkbenchActionExecutedClassification } from '../../../../base/common/actions.js';
import { parseLinkedText } from '../../../../base/common/linkedText.js';
import { Link } from '../../../../platform/opener/browser/link.js';
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
import { WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from '../../../../base/common/actions.js';
//#region --- colors
import { IChatStatusItemService, ChatStatusEntry } from './chatStatusItemService.js';
const gaugeBackground = registerColor('gauge.background', {
dark: inputValidationInfoBorder,
@@ -99,9 +102,9 @@ export class ChatStatusBarEntry extends Disposable implements IWorkbenchContribu
private dashboard = new Lazy<ChatStatusDashboard>(() => 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();
@@ -211,11 +214,13 @@ class ChatStatusDashboard extends Disposable {
constructor(
@IChatEntitlementService private readonly chatEntitlementService: ChatEntitlementService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IHoverService private readonly hoverService: IHoverService,
@IEditorService private readonly editorService: IEditorService,
@ILanguageService private readonly languageService: ILanguageService,
@IChatStatusItemService private readonly chatStatusItemService: IChatStatusItemService,
@ICommandService private readonly commandService: ICommandService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IEditorService private readonly editorService: IEditorService,
@IHoverService private readonly hoverService: IHoverService,
@ILanguageService private readonly languageService: ILanguageService,
@IOpenerService private readonly openerService: IOpenerService,
@ITelemetryService private readonly telemetryService: ITelemetryService,
) {
super();
@@ -271,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"));
@@ -296,6 +324,37 @@ class ChatStatusDashboard extends Disposable {
return this.element;
}
private renderContributedChatStatusItem(item: ChatStatusEntry): { element: HTMLElement; disposables: DisposableStore } {
const disposables = new DisposableStore();
const entryEl = $('div.contribution');
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);
}
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);
target.append(...parts);
} else {
store.add(new Link(target, node, undefined, this.hoverService, this.openerService));
}
}
}
private runCommandAndClose(commandOrFn: string | Function): void {
if (typeof commandOrFn === 'function') {
commandOrFn();
@@ -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>('IChatStatusItemService');
export interface IChatStatusItemService {
readonly _serviceBrand: undefined;
readonly onDidChange: Event<IChatStatusItemChangeEvent>;
setOrUpdateEntry(entry: ChatStatusEntry): void;
deleteEntry(id: string): void;
getEntries(): Iterable<ChatStatusEntry>;
}
export interface IChatStatusItemChangeEvent {
readonly entry: ChatStatusEntry;
}
export type ChatStatusEntry = {
id: string;
label: string;
description: string;
detail: string | undefined;
};
class ChatStatusItemService implements IChatStatusItemService {
readonly _serviceBrand: undefined;
private readonly _entries = new Map<string, ChatStatusEntry>();
private readonly _onDidChange = new Emitter<IChatStatusItemChangeEvent>();
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<ChatStatusEntry> {
return this._entries.values();
}
}
registerSingleton(IChatStatusItemService, ChatStatusItemService, InstantiationType.Delayed);
@@ -111,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;
}
}
+61
View File
@@ -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 after the title. Supports Markdown style links (`[text](http://example.com)`) and rendering of
* {@link ThemeIcon theme icons} via the `$(<name>)`-syntax.
*/
description: string;
/**
* Optional additional details of the entry.
*
* 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 `$(<name>)`-syntax.
*/
detail: 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;
}
}