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
44 changes: 40 additions & 4 deletions src/vs/workbench/api/browser/mainThreadChatSessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import { ChatEditorInput } from '../../contrib/chat/browser/widgetHosts/editor/c
import { IChatRequestVariableEntry } from '../../contrib/chat/common/attachments/chatVariableEntries.js';
import { IChatDebugService } from '../../contrib/chat/common/chatDebugService.js';
import { IChatContentInlineReference, IChatDetail, IChatProgress, IChatService, IChatSessionTiming } from '../../contrib/chat/common/chatService/chatService.js';
import { ChatSessionOptionsMap, ChatSessionStatus, IChatNewSessionRequest, IChatSession, IChatSessionContentProvider, IChatSessionHistoryItem, IChatSessionItem, IChatSessionItemController, IChatSessionItemsDelta, IChatSessionProviderOptionItem, IChatSessionRequestHistoryItem, IChatSessionsService, ReadonlyChatSessionOptionsMap } from '../../contrib/chat/common/chatSessionsService.js';
import { ChatSessionOptionsMap, ChatSessionStatus, IChatNewSessionRequest, IChatSession, IChatSessionContentProvider, IChatSessionHistoryItem, IChatSessionItem, IChatSessionItemController, IChatSessionItemsDelta, IChatSessionProviderOptionGroup, IChatSessionProviderOptionItem, IChatSessionRequestHistoryItem, IChatSessionsService, ReadonlyChatSessionOptionsMap } from '../../contrib/chat/common/chatSessionsService.js';
import { ChatAgentLocation } from '../../contrib/chat/common/constants.js';
import { IChatModel } from '../../contrib/chat/common/model/chatModel.js';
import { getChatSessionType, isUntitledChatSession } from '../../contrib/chat/common/model/chatUri.js';
Expand Down Expand Up @@ -491,6 +491,14 @@ class MainThreadChatSessionItemController extends Disposable implements IChatSes
this.addOrUpdateItem(existing);
}
}

async getNewChatSessionInputState(token: CancellationToken): Promise<readonly IChatSessionProviderOptionGroup[] | undefined> {
const optionGroups = await this._proxy.$provideChatSessionInputState(this._handle, undefined, token);
if (!optionGroups?.length) {
return undefined;
}
return optionGroups;
}
}

class MainThreadChatSessionItem implements IChatSessionItem {
Expand Down Expand Up @@ -622,6 +630,27 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat
controller,
dispose: () => disposables.dispose(),
});

// Fetch initial input state for new/untitled sessions
this._refreshControllerInputState(handle, chatSessionType);
}

private _refreshControllerInputState(handle: number, chatSessionType: string): void {
this._proxy.$provideChatSessionInputState(handle, undefined, CancellationToken.None).then(optionGroups => {
if (optionGroups?.length) {
this._applyOptionGroups(handle, chatSessionType, optionGroups);
}
}).catch(err => this._logService.error('Error fetching chat session input state', err));
}

private _applyOptionGroups(handle: number, chatSessionType: string, optionGroups: readonly IChatSessionProviderOptionGroup[]): void {
const groupsWithCallbacks = optionGroups.map(group => ({
...group,
onSearch: group.searchable ? async (query: string, token: CancellationToken) => {
return await this._proxy.$invokeOptionGroupSearch(handle, group.id, query, token);
} : undefined,
}));
this._chatSessionsService.setOptionGroupsForSessionType(chatSessionType, handle, groupsWithCallbacks);
}

private getController(handle: number): MainThreadChatSessionItemController {
Expand Down Expand Up @@ -884,6 +913,16 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat
this._refreshProviderOptions(handle, sessionType);
}

$updateChatSessionInputState(controllerHandle: number, optionGroups: readonly IChatSessionProviderOptionGroup[]): void {
const registration = this._itemControllerRegistrations.get(controllerHandle);
if (!registration) {
this._logService.warn(`No controller found for handle ${controllerHandle} when updating input state`);
return;
}

this._applyOptionGroups(controllerHandle, registration.chatSessionType, optionGroups);
}

private _refreshProviderOptions(handle: number, chatSessionScheme: string): void {
this._proxy.$provideChatSessionProviderOptions(handle, CancellationToken.None).then(options => {
if (options?.optionGroups && options.optionGroups.length) {
Expand All @@ -895,9 +934,6 @@ export class MainThreadChatSessions extends Disposable implements MainThreadChat
}));
this._chatSessionsService.setOptionGroupsForSessionType(chatSessionScheme, handle, groupsWithCallbacks);
}
if (options?.newSessionOptions) {
this._chatSessionsService.setNewSessionOptionsForSessionType(chatSessionScheme, ChatSessionOptionsMap.fromRecord(options.newSessionOptions));
}
}).catch(err => this._logService.error('Error fetching chat session options', err));
}

Expand Down
3 changes: 3 additions & 0 deletions src/vs/workbench/api/common/extHost.protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3659,6 +3659,8 @@ export interface MainThreadChatSessionsShape extends IDisposable {
$onDidChangeChatSessionOptions(handle: number, sessionResource: UriComponents, updates: Record<string, string | IChatSessionProviderOptionItem>): void;
$onDidChangeChatSessionProviderOptions(handle: number): void;

$updateChatSessionInputState(controllerHandle: number, optionGroups: readonly IChatSessionProviderOptionGroup[]): void;

$handleProgressChunk(handle: number, sessionResource: UriComponents, requestId: string, chunks: (IChatProgressDto | [IChatProgressDto, number])[]): Promise<void>;
$handleAnchorResolve(handle: number, sessionResource: UriComponents, requestId: string, requestHandle: string, anchor: Dto<IChatContentInlineReference>): void;
$handleProgressComplete(handle: number, sessionResource: UriComponents, requestId: string): void;
Expand All @@ -3677,6 +3679,7 @@ export interface ExtHostChatSessionsShape {
$invokeOptionGroupSearch(providerHandle: number, optionGroupId: string, query: string, token: CancellationToken): Promise<IChatSessionProviderOptionItem[]>;
$provideHandleOptionsChange(providerHandle: number, sessionResource: UriComponents, updates: Record<string, string | IChatSessionProviderOptionItem | undefined>, token: CancellationToken): Promise<void>;
$forkChatSession(providerHandle: number, sessionResource: UriComponents, request: IChatSessionRequestHistoryItemDto | undefined, token: CancellationToken): Promise<Dto<IChatSessionItem>>;
$provideChatSessionInputState(controllerHandle: number, sessionResource: UriComponents | undefined, token: CancellationToken): Promise<IChatSessionProviderOptionGroup[] | undefined>;
}

export interface GitRefQueryDto {
Expand Down
170 changes: 160 additions & 10 deletions src/vs/workbench/api/common/extHostChatSessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,40 @@ import { IExtHostRpcService } from './extHostRpcService.js';
import * as typeConvert from './extHostTypeConverters.js';
import { Diagnostic } from './extHostTypeConverters.js';
import * as extHostTypes from './extHostTypes.js';
import { isUntitledChatSession } from '../../contrib/chat/common/model/chatUri.js';

type ChatSessionTiming = vscode.ChatSessionItem['timing'];

// #region Chat Session Input State

class ChatSessionInputStateImpl implements vscode.ChatSessionInputState {
#groups: readonly vscode.ChatSessionProviderOptionGroup[];
readonly #onChangedDelegate: (() => void) | undefined;

readonly #onDidChangeEmitter = new Emitter<void>();
readonly onDidChange = this.#onDidChangeEmitter.event;

constructor(groups: readonly vscode.ChatSessionProviderOptionGroup[], onChangedDelegate?: () => void) {
this.#groups = groups;
this.#onChangedDelegate = onChangedDelegate;
}

get groups(): readonly vscode.ChatSessionProviderOptionGroup[] {
return this.#groups;
}

set groups(value: readonly vscode.ChatSessionProviderOptionGroup[]) {
this.#groups = value;
this.#onChangedDelegate?.();
}

_fireDidChange(): void {
this.#onDidChangeEmitter.fire();
}
}

// #endregion

// #region Chat Session Item Controller

class ChatSessionItemImpl implements vscode.ChatSessionItem {
Expand Down Expand Up @@ -312,10 +343,12 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio
readonly extension: IExtensionDescription;
readonly disposable: DisposableStore;
readonly onDidChangeChatSessionItemStateEmitter: Emitter<vscode.ChatSessionItem>;
optionGroups?: readonly vscode.ChatSessionProviderOptionGroup[];
}>();

private _contentProviderHandlePool = 0;
private readonly _chatSessionContentProviders = new Map</* handle */ number, {
readonly chatSessionScheme: string;
readonly provider: vscode.ChatSessionContentProvider;
readonly extension: IExtensionDescription;
readonly capabilities?: vscode.ChatSessionCapabilities;
Expand All @@ -326,10 +359,6 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio
* Map of uri -> chat sessions infos
*/
private readonly _extHostChatSessions = new ResourceMap<{ readonly sessionObj: ExtHostChatSession; readonly disposeCts: CancellationTokenSource }>();
/**
* Store option groups with onSearch callbacks per provider handle
*/
private readonly _providerOptionGroups = new Map<number, readonly vscode.ChatSessionProviderOptionGroup[]>();

constructor(
private readonly commands: ExtHostCommands,
Expand Down Expand Up @@ -375,6 +404,9 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio
createChatSessionItem: (_resource: vscode.Uri, _label: string) => {
throw new Error('Not implemented for providers');
},
createChatSessionInputState: (_options: vscode.ChatSessionProviderOptionItem[]) => {
return new ChatSessionInputStateImpl([]);
},
onDidChangeChatSessionItemState: onDidChangeChatSessionItemStateEmitter.event,
newChatSessionItemHandler: undefined,
dispose: () => {
Expand Down Expand Up @@ -422,6 +454,7 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio
let isDisposed = false;
let newChatSessionItemHandler: vscode.ChatSessionItemController['newChatSessionItemHandler'];
let forkHandler: vscode.ChatSessionItemController['forkHandler'];
let provideChatSessionInputStateHandler: vscode.ChatSessionItemController['getChatSessionInputState'];
const onDidChangeChatSessionItemStateEmitter = disposables.add(new Emitter<vscode.ChatSessionItem>());

const collection = new ChatSessionItemCollectionImpl(controllerHandle, this._proxy);
Expand Down Expand Up @@ -455,6 +488,34 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio
set newChatSessionItemHandler(handler: vscode.ChatSessionItemController['newChatSessionItemHandler']) { newChatSessionItemHandler = handler; },
get forkHandler() { return forkHandler; },
set forkHandler(handler: vscode.ChatSessionItemController['forkHandler']) { forkHandler = handler; },
get getChatSessionInputState() { return provideChatSessionInputStateHandler; },
set getChatSessionInputState(handler: vscode.ChatSessionItemController['getChatSessionInputState']) { provideChatSessionInputStateHandler = handler; },
createChatSessionInputState: (groups: vscode.ChatSessionProviderOptionGroup[]) => {
if (isDisposed) {
throw new Error('ChatSessionItemController has been disposed');
}

const inputState = new ChatSessionInputStateImpl(groups, () => {
// Store updated option groups on the controller entry
const entry = this._chatSessionItemControllers.get(controllerHandle);
if (entry) {
entry.optionGroups = inputState.groups;
}
const serializableGroups = inputState.groups.map(g => ({
id: g.id,
name: g.name,
description: g.description,
items: g.items,
selected: g.selected,
when: g.when,
searchable: g.searchable,
icon: g.icon,
commands: g.commands,
}));
void this._proxy.$updateChatSessionInputState(controllerHandle, serializableGroups);
});
return inputState;
},
dispose: () => {
isDisposed = true;
disposables.dispose();
Expand All @@ -478,7 +539,7 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio
const handle = this._contentProviderHandlePool++;
const disposables = new DisposableStore();

this._chatSessionContentProviders.set(handle, { provider, extension, capabilities, disposable: disposables });
this._chatSessionContentProviders.set(handle, { chatSessionScheme, provider, extension, capabilities, disposable: disposables });
this._proxy.$registerChatSessionContentProvider(handle, chatSessionScheme);

if (provider.onDidChangeChatSessionOptions) {
Expand Down Expand Up @@ -512,15 +573,28 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio

const sessionResource = URI.revive(sessionResourceComponents);

const controllerData = this.getChatSessionItemController(sessionResource.scheme);
let inputState: vscode.ChatSessionInputState;
if (controllerData?.controller.getChatSessionInputState) {
const result = await controllerData.controller.getChatSessionInputState(isUntitledChatSession(sessionResource) ? undefined : sessionResource, {
previousInputState: this._createInputStateFromOptions(controllerData.optionGroups ?? [], context.initialSessionOptions),
}, token);
if (result) {
inputState = result;
}
}
inputState ??= this._createInputStateFromOptions(
controllerData?.optionGroups ?? [], context.initialSessionOptions
);

const session = await provider.provider.provideChatSessionContent(sessionResource, token, {
sessionOptions: context?.initialSessionOptions ?? []
sessionOptions: context?.initialSessionOptions ?? [],
inputState,
});
if (token.isCancellationRequested) {
throw new CancellationError();
}

const controllerData = this.getChatSessionItemController(sessionResource.scheme);

const sessionDisposables = new DisposableStore();
const id = sessionResource.toString();
const chatSession = new ExtHostChatSession(session, provider.extension, {
Expand Down Expand Up @@ -611,7 +685,10 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio
}
const { optionGroups, newSessionOptions } = result;
if (optionGroups) {
this._providerOptionGroups.set(handle, optionGroups);
const controllerData = this.getChatSessionItemController(entry.chatSessionScheme);
if (controllerData) {
controllerData.optionGroups = optionGroups;
}
}
return {
optionGroups,
Expand Down Expand Up @@ -706,6 +783,36 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio
return undefined;
}

private _getControllerForContentProviderHandle(handle: number) {
const entry = this._chatSessionContentProviders.get(handle);
if (!entry) {
return undefined;
}
return this.getChatSessionItemController(entry.chatSessionScheme);
}

private _createInputStateFromOptions(
groups: readonly vscode.ChatSessionProviderOptionGroup[],
sessionOptions?: ReadonlyArray<{ optionId: string; value: string }>,
): ChatSessionInputStateImpl {
if (!sessionOptions?.length) {
return new ChatSessionInputStateImpl(groups);
}

const resolvedGroups = groups.map(group => {
const match = sessionOptions.find(o => o.optionId === group.id);
if (!match) {
return group;
}
const selectedItem = group.items.find(item => item.id === match.value);
if (!selectedItem) {
return group;
}
return { ...group, selected: selectedItem };
});
return new ChatSessionInputStateImpl(resolvedGroups);
}

private async getModelForRequest(request: IChatAgentRequest, extension: IExtensionDescription): Promise<vscode.LanguageModelChat> {
let model: vscode.LanguageModelChat | undefined;
if (request.userSelectedModelId) {
Expand Down Expand Up @@ -802,7 +909,8 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio
}

async $invokeOptionGroupSearch(providerHandle: number, optionGroupId: string, query: string, token: CancellationToken): Promise<IChatSessionProviderOptionItem[]> {
const optionGroups = this._providerOptionGroups.get(providerHandle);
const optionGroups = this._chatSessionItemControllers.get(providerHandle)?.optionGroups
?? this._getControllerForContentProviderHandle(providerHandle)?.optionGroups;
if (!optionGroups) {
this._logService.warn(`No option groups found for provider handle ${providerHandle}`);
return [];
Expand Down Expand Up @@ -845,12 +953,20 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio
return undefined;
}

let inputState: vscode.ChatSessionInputState;
if (controllerData.controller.getChatSessionInputState) {
inputState = await controllerData.controller.getChatSessionInputState(undefined, { previousInputState: this._createInputStateFromOptions(controllerData.optionGroups ?? [], request.initialSessionOptions) }, token);
} else {
inputState = new ChatSessionInputStateImpl([]);
}

const item = await handler({
request: {
prompt: request.prompt,
command: request.command
},
sessionOptions: request.initialSessionOptions ?? [],
inputState,
}, token);
if (!item) {
return undefined;
Expand All @@ -877,4 +993,38 @@ export class ExtHostChatSessions extends Disposable implements ExtHostChatSessio
controllerData.onDidChangeChatSessionItemStateEmitter.fire(item);
}

async $provideChatSessionInputState(controllerHandle: number, sessionResourceComponents: UriComponents | undefined, token: CancellationToken): Promise<vscode.ChatSessionProviderOptionGroup[] | undefined> {
const controllerData = this._chatSessionItemControllers.get(controllerHandle);
if (!controllerData) {
this._logService.warn(`No controller found for handle ${controllerHandle}`);
return undefined;
}

const handler = controllerData.controller.getChatSessionInputState;
if (!handler) {
return undefined;
}

const sessionResource = sessionResourceComponents ? URI.revive(sessionResourceComponents) : undefined;
const inputState = await handler(sessionResource, { previousInputState: undefined }, token);
if (!inputState) {
return undefined;
}

// Store the option groups for onSearch callbacks
controllerData.optionGroups = inputState.groups;

// Strip non-serializable fields (onSearch) before returning over the protocol
return inputState.groups.map(g => ({
id: g.id,
name: g.name,
description: g.description,
items: g.items,
selected: g.selected,
when: g.when,
searchable: g.searchable,
icon: g.icon,
commands: g.commands,
}));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ suite('ObservableChatSession', function () {
$onDidChangeChatSessionItemState: sinon.stub(),
$newChatSessionItem: sinon.stub().resolves(undefined),
$forkChatSession: sinon.stub().resolves(undefined),
$provideChatSessionInputState: sinon.stub().resolves(undefined),
};
});

Expand Down Expand Up @@ -524,6 +525,7 @@ suite('MainThreadChatSessions', function () {
$onDidChangeChatSessionItemState: sinon.stub(),
$newChatSessionItem: sinon.stub().resolves(undefined),
$forkChatSession: sinon.stub().resolves(undefined),
$provideChatSessionInputState: sinon.stub().resolves(undefined),
};

const extHostContext = new class implements IExtHostContext {
Expand Down
Loading
Loading