Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -899,6 +899,7 @@ export default tseslint.config(
],
'verbs': [
'accept',
'archive',
'change',
'close',
'collapse',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ const _allApiProposals = {
},
chatSessionsProvider: {
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatSessionsProvider.d.ts',
version: 3
version: 4
},
chatStatusItem: {
proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatStatusItem.d.ts',
Expand Down
2 changes: 1 addition & 1 deletion src/vs/workbench/api/browser/mainThreadChatSessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -382,7 +382,6 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat
));
}


$onDidChangeChatSessionItems(handle: number): void {
this._itemProvidersRegistrations.get(handle)?.onDidChangeItems.fire();
}
Expand Down Expand Up @@ -491,6 +490,7 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat
resource: uri,
iconPath: session.iconPath,
tooltip: session.tooltip ? this._reviveTooltip(session.tooltip) : undefined,
archived: session.archived,
} satisfies IChatSessionItem;
}));
} catch (error) {
Expand Down
4 changes: 4 additions & 0 deletions src/vs/workbench/api/common/extHost.api.impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1529,6 +1529,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
checkProposedApiEnabled(extension, 'chatSessionsProvider');
return extHostChatSessions.registerChatSessionItemProvider(extension, chatSessionType, provider);
},
createChatSessionItemController: (chatSessionType: string, refreshHandler: () => Thenable<void>) => {
checkProposedApiEnabled(extension, 'chatSessionsProvider');
return extHostChatSessions.createChatSessionItemController(extension, chatSessionType, refreshHandler);
},
registerChatSessionContentProvider(scheme: string, provider: vscode.ChatSessionContentProvider, chatParticipant: vscode.ChatParticipant, capabilities?: vscode.ChatSessionCapabilities) {
checkProposedApiEnabled(extension, 'chatSessionsProvider');
return extHostChatSessions.registerChatSessionContentProvider(extension, scheme, chatParticipant, provider, capabilities);
Expand Down
265 changes: 252 additions & 13 deletions src/vs/workbench/api/common/extHostChatSessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,14 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
/* eslint-disable local/code-no-native-private */

import type * as vscode from 'vscode';
import { coalesce } from '../../../base/common/arrays.js';
import { CancellationToken, CancellationTokenSource } from '../../../base/common/cancellation.js';
import { CancellationError } from '../../../base/common/errors.js';
import { Disposable, DisposableStore } from '../../../base/common/lifecycle.js';
import { Emitter } from '../../../base/common/event.js';
import { Disposable, DisposableStore, toDisposable } from '../../../base/common/lifecycle.js';
import { ResourceMap } from '../../../base/common/map.js';
import { MarshalledId } from '../../../base/common/marshallingIds.js';
import { URI, UriComponents } from '../../../base/common/uri.js';
Expand All @@ -29,6 +31,175 @@ import { basename } from '../../../base/common/resources.js';
import { Diagnostic } from './extHostTypeConverters.js';
import { SymbolKind, SymbolKinds } from '../../../editor/common/languages.js';

// #region Chat Session Item Controller

class ChatSessionItemImpl implements vscode.ChatSessionItem {
#label: string;
#iconPath?: vscode.IconPath;
#description?: string | vscode.MarkdownString;
#badge?: string | vscode.MarkdownString;
#status?: vscode.ChatSessionStatus;
#archived?: boolean;
#tooltip?: string | vscode.MarkdownString;
#timing?: { startTime: number; endTime?: number };
#changes?: readonly vscode.ChatSessionChangedFile[] | { files: number; insertions: number; deletions: number };
#onChanged: () => void;

readonly resource: vscode.Uri;

constructor(resource: vscode.Uri, label: string, onChanged: () => void) {
this.resource = resource;
this.#label = label;
this.#onChanged = onChanged;
}

get label(): string {
return this.#label;
}

set label(value: string) {
if (this.#label !== value) {
this.#label = value;
this.#onChanged();
}
}

get iconPath(): vscode.IconPath | undefined {
return this.#iconPath;
}

set iconPath(value: vscode.IconPath | undefined) {
if (this.#iconPath !== value) {
this.#iconPath = value;
this.#onChanged();
}
}

get description(): string | vscode.MarkdownString | undefined {
return this.#description;
}

set description(value: string | vscode.MarkdownString | undefined) {
if (this.#description !== value) {
this.#description = value;
this.#onChanged();
}
}

get badge(): string | vscode.MarkdownString | undefined {
return this.#badge;
}

set badge(value: string | vscode.MarkdownString | undefined) {
if (this.#badge !== value) {
this.#badge = value;
this.#onChanged();
}
}

get status(): vscode.ChatSessionStatus | undefined {
return this.#status;
}

set status(value: vscode.ChatSessionStatus | undefined) {
if (this.#status !== value) {
this.#status = value;
this.#onChanged();
}
}

get archived(): boolean | undefined {
return this.#archived;
}

set archived(value: boolean | undefined) {
if (this.#archived !== value) {
this.#archived = value;
this.#onChanged();
}
}

get tooltip(): string | vscode.MarkdownString | undefined {
return this.#tooltip;
}

set tooltip(value: string | vscode.MarkdownString | undefined) {
if (this.#tooltip !== value) {
this.#tooltip = value;
this.#onChanged();
}
}

get timing(): { startTime: number; endTime?: number } | undefined {
return this.#timing;
}

set timing(value: { startTime: number; endTime?: number } | undefined) {
if (this.#timing !== value) {
this.#timing = value;
this.#onChanged();
}
}

get changes(): readonly vscode.ChatSessionChangedFile[] | { files: number; insertions: number; deletions: number } | undefined {
return this.#changes;
}

set changes(value: readonly vscode.ChatSessionChangedFile[] | { files: number; insertions: number; deletions: number } | undefined) {
if (this.#changes !== value) {
this.#changes = value;
this.#onChanged();
}
}
}

class ChatSessionItemCollectionImpl implements vscode.ChatSessionItemCollection {
readonly #items = new ResourceMap<vscode.ChatSessionItem>();
#onItemsChanged: () => void;

constructor(onItemsChanged: () => void) {
this.#onItemsChanged = onItemsChanged;
}

get size(): number {
return this.#items.size;
}

replace(items: readonly vscode.ChatSessionItem[]): void {
this.#items.clear();
for (const item of items) {
this.#items.set(item.resource, item);
}
this.#onItemsChanged();
}

forEach(callback: (item: vscode.ChatSessionItem, collection: vscode.ChatSessionItemCollection) => unknown, thisArg?: any): void {
for (const [_, item] of this.#items) {
callback.call(thisArg, item, this);
}
}

add(item: vscode.ChatSessionItem): void {
this.#items.set(item.resource, item);
this.#onItemsChanged();
}

delete(resource: vscode.Uri): void {
this.#items.delete(resource);
this.#onItemsChanged();
}

get(resource: vscode.Uri): vscode.ChatSessionItem | undefined {
return this.#items.get(resource);
}

[Symbol.iterator](): Iterator<readonly [id: URI, chatSessionItem: vscode.ChatSessionItem]> {
return this.#items.entries();
}
}

// #endregion

class ExtHostChatSession {
private _stream: ChatAgentResponseStream;

Expand Down Expand Up @@ -62,13 +233,20 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio
readonly extension: IExtensionDescription;
readonly disposable: DisposableStore;
}>();
private readonly _chatSessionItemControllers = new Map<number, {
readonly sessionType: string;
readonly controller: vscode.ChatSessionItemController;
readonly extension: IExtensionDescription;
readonly disposable: DisposableStore;
}>();
private _nextChatSessionItemProviderHandle = 0;
private readonly _chatSessionContentProviders = new Map<number, {
readonly provider: vscode.ChatSessionContentProvider;
readonly extension: IExtensionDescription;
readonly capabilities?: vscode.ChatSessionCapabilities;
readonly disposable: DisposableStore;
}>();
private _nextChatSessionItemProviderHandle = 0;
private _nextChatSessionItemControllerHandle = 0;
private _nextChatSessionContentProviderHandle = 0;

/**
Expand Down Expand Up @@ -137,6 +315,52 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio
};
}


createChatSessionItemController(extension: IExtensionDescription, id: string, refreshHandler: () => Thenable<void>): vscode.ChatSessionItemController {
const controllerHandle = this._nextChatSessionItemControllerHandle++;
const disposables = new DisposableStore();

// TODO: Currently not hooked up
const onDidArchiveChatSessionItem = disposables.add(new Emitter<vscode.ChatSessionItem>());

const collection = new ChatSessionItemCollectionImpl(() => {
this._proxy.$onDidChangeChatSessionItems(controllerHandle);
});

let isDisposed = false;

const controller: vscode.ChatSessionItemController = {
id,
refreshHandler,
items: collection,
onDidArchiveChatSessionItem: onDidArchiveChatSessionItem.event,
createChatSessionItem: (resource: vscode.Uri, label: string) => {
if (isDisposed) {
throw new Error('ChatSessionItemController has been disposed');
}

return new ChatSessionItemImpl(resource, label, () => {
// TODO: Optimize to only update the specific item
this._proxy.$onDidChangeChatSessionItems(controllerHandle);
});
},
dispose: () => {
isDisposed = true;
disposables.dispose();
},
};

this._chatSessionItemControllers.set(controllerHandle, { controller, extension, disposable: disposables, sessionType: id });
this._proxy.$registerChatSessionItemProvider(controllerHandle, id);

disposables.add(toDisposable(() => {
this._chatSessionItemControllers.delete(controllerHandle);
this._proxy.$unregisterChatSessionItemProvider(controllerHandle);
}));

return controller;
}

registerChatSessionContentProvider(extension: IExtensionDescription, chatSessionScheme: string, chatParticipant: vscode.ChatParticipant, provider: vscode.ChatSessionContentProvider, capabilities?: vscode.ChatSessionCapabilities): vscode.Disposable {
const handle = this._nextChatSessionContentProviderHandle++;
const disposables = new DisposableStore();
Expand Down Expand Up @@ -181,13 +405,14 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio
}
}

private convertChatSessionItem(sessionType: string, sessionContent: vscode.ChatSessionItem): IChatSessionItem {
private convertChatSessionItem(sessionContent: vscode.ChatSessionItem): IChatSessionItem {
return {
resource: sessionContent.resource,
label: sessionContent.label,
description: sessionContent.description ? typeConvert.MarkdownString.from(sessionContent.description) : undefined,
badge: sessionContent.badge ? typeConvert.MarkdownString.from(sessionContent.badge) : undefined,
status: this.convertChatSessionStatus(sessionContent.status),
archived: sessionContent.archived,
tooltip: typeConvert.MarkdownString.fromStrict(sessionContent.tooltip),
timing: {
startTime: sessionContent.timing?.startTime ?? 0,
Expand All @@ -204,21 +429,35 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio
}

async $provideChatSessionItems(handle: number, token: vscode.CancellationToken): Promise<IChatSessionItem[]> {
const entry = this._chatSessionItemProviders.get(handle);
if (!entry) {
this._logService.error(`No provider registered for handle ${handle}`);
return [];
}
let items: vscode.ChatSessionItem[];

const controller = this._chatSessionItemControllers.get(handle);
if (controller) {
// Call the refresh handler to populate items
await controller.controller.refreshHandler();
if (token.isCancellationRequested) {
return [];
}

items = Array.from(controller.controller.items, x => x[1]);
} else {

const sessions = await entry.provider.provideChatSessionItems(token);
if (!sessions) {
return [];
const itemProvider = this._chatSessionItemProviders.get(handle);
if (!itemProvider) {
this._logService.error(`No provider registered for handle ${handle}`);
return [];
}

items = await itemProvider.provider.provideChatSessionItems(token) ?? [];
if (token.isCancellationRequested) {
return [];
}
}

const response: IChatSessionItem[] = [];
for (const sessionContent of sessions) {
for (const sessionContent of items) {
this._sessionItems.set(sessionContent.resource, sessionContent);
response.push(this.convertChatSessionItem(entry.sessionType, sessionContent));
response.push(this.convertChatSessionItem(sessionContent));
}
return response;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ export interface IChatSessionsExtensionPoint {
readonly commands?: IChatSessionCommandContribution[];
readonly canDelegate?: boolean;
}

export interface IChatSessionItem {
resource: URI;
label: string;
Expand Down
Loading
Loading