Merge branch 'main' into npm-extension-bun-package-manager

This commit is contained in:
Marvin A. Ruder 2023-11-12 11:54:50 +01:00 committed by GitHub
commit 4fd54f93f7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
77 changed files with 517 additions and 1288 deletions

View file

@ -5,7 +5,7 @@
import * as assert from 'assert';
import 'mocha';
import { CancellationToken, chat, ChatAgentRequest, ChatVariableLevel, CompletionItemKind, Disposable, interactive, InteractiveProgress, InteractiveRequest, InteractiveResponseForProgress, InteractiveSession, InteractiveSessionState, Progress, ProviderResult } from 'vscode';
import { CancellationToken, chat, ChatAgentRequest, ChatVariableLevel, Disposable, interactive, InteractiveSession, ProviderResult } from 'vscode';
import { assertNoRpc, closeAllEditors, DeferredPromise, disposeAll } from '../utils';
suite('chat', () => {
@ -22,24 +22,12 @@ suite('chat', () => {
function getDeferredForRequest(): DeferredPromise<ChatAgentRequest> {
disposables.push(interactive.registerInteractiveSessionProvider('provider', {
prepareSession: (_initialState: InteractiveSessionState | undefined, _token: CancellationToken): ProviderResult<InteractiveSession> => {
prepareSession: (_token: CancellationToken): ProviderResult<InteractiveSession> => {
return {
requester: { name: 'test' },
responder: { name: 'test' },
};
},
provideResponseWithProgress: (_request: InteractiveRequest, _progress: Progress<InteractiveProgress>, _token: CancellationToken): ProviderResult<InteractiveResponseForProgress> => {
return null;
},
provideSlashCommands: (_session, _token) => {
return [{ command: 'hello', title: 'Hello', kind: CompletionItemKind.Text }];
},
removeRequest: (_session: InteractiveSession, _requestId: string): void => {
throw new Error('Function not implemented.');
}
}));
const deferred = new DeferredPromise<ChatAgentRequest>();

View file

@ -1,62 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import 'mocha';
import { CancellationToken, CompletionItemKind, Disposable, interactive, InteractiveProgress, InteractiveRequest, InteractiveResponseForProgress, InteractiveSession, InteractiveSessionState, Progress, ProviderResult } from 'vscode';
import { assertNoRpc, closeAllEditors, DeferredPromise, disposeAll } from '../utils';
suite('InteractiveSessionProvider', () => {
let disposables: Disposable[] = [];
setup(async () => {
disposables = [];
});
teardown(async function () {
assertNoRpc();
await closeAllEditors();
disposeAll(disposables);
});
function getDeferredForRequest(): DeferredPromise<InteractiveRequest> {
const deferred = new DeferredPromise<InteractiveRequest>();
disposables.push(interactive.registerInteractiveSessionProvider('provider', {
prepareSession: (_initialState: InteractiveSessionState | undefined, _token: CancellationToken): ProviderResult<InteractiveSession> => {
return {
requester: { name: 'test' },
responder: { name: 'test' },
};
},
provideResponseWithProgress: (request: InteractiveRequest, _progress: Progress<InteractiveProgress>, _token: CancellationToken): ProviderResult<InteractiveResponseForProgress> => {
deferred.complete(request);
return null;
},
provideSlashCommands: (_session, _token) => {
return [{ command: 'hello', title: 'Hello', kind: CompletionItemKind.Text }];
},
removeRequest: (_session: InteractiveSession, _requestId: string): void => {
throw new Error('Function not implemented.');
}
}));
return deferred;
}
test('plain text query', async () => {
const deferred = getDeferredForRequest();
interactive.sendInteractiveRequestToProvider('provider', { message: 'hello' });
const lastResult = await deferred.p;
assert.strictEqual(lastResult.message, 'hello');
});
test('slash command', async () => {
const deferred = getDeferredForRequest();
interactive.sendInteractiveRequestToProvider('provider', { message: '/hello' });
const lastResult = await deferred.p;
assert.strictEqual(lastResult.message, '/hello');
});
});

View file

@ -133,9 +133,9 @@ export interface IAddStandardDisposableListenerSignature {
(node: HTMLElement, type: 'pointerup', handler: (event: PointerEvent) => void, useCapture?: boolean): IDisposable;
(node: HTMLElement, type: string, handler: (event: any) => void, useCapture?: boolean): IDisposable;
}
function _wrapAsStandardMouseEvent(handler: (e: IMouseEvent) => void): (e: MouseEvent) => void {
function _wrapAsStandardMouseEvent(targetWindow: Window, handler: (e: IMouseEvent) => void): (e: MouseEvent) => void {
return function (e: MouseEvent) {
return handler(new StandardMouseEvent(e));
return handler(new StandardMouseEvent(targetWindow, e));
};
}
function _wrapAsStandardKeyboardEvent(handler: (e: IKeyboardEvent) => void): (e: KeyboardEvent) => void {
@ -147,7 +147,7 @@ export const addStandardDisposableListener: IAddStandardDisposableListenerSignat
let wrapHandler = handler;
if (type === 'click' || type === 'mousedown') {
wrapHandler = _wrapAsStandardMouseEvent(handler);
wrapHandler = _wrapAsStandardMouseEvent(getWindow(node), handler);
} else if (type === 'keydown' || type === 'keypress' || type === 'keyup') {
wrapHandler = _wrapAsStandardKeyboardEvent(handler);
}
@ -156,13 +156,13 @@ export const addStandardDisposableListener: IAddStandardDisposableListenerSignat
};
export const addStandardDisposableGenericMouseDownListener = function addStandardDisposableListener(node: HTMLElement, handler: (event: any) => void, useCapture?: boolean): IDisposable {
const wrapHandler = _wrapAsStandardMouseEvent(handler);
const wrapHandler = _wrapAsStandardMouseEvent(getWindow(node), handler);
return addDisposableGenericMouseDownListener(node, wrapHandler, useCapture);
};
export const addStandardDisposableGenericMouseUpListener = function addStandardDisposableListener(node: HTMLElement, handler: (event: any) => void, useCapture?: boolean): IDisposable {
const wrapHandler = _wrapAsStandardMouseEvent(handler);
const wrapHandler = _wrapAsStandardMouseEvent(getWindow(node), handler);
return addDisposableGenericMouseUpListener(node, wrapHandler, useCapture);
};

View file

@ -195,7 +195,7 @@ export function renderMarkdown(markdown: IMarkdownString, options: MarkdownRende
const onClick = options.actionHandler.disposables.add(new DomEmitter(element, 'click'));
const onAuxClick = options.actionHandler.disposables.add(new DomEmitter(element, 'auxclick'));
options.actionHandler.disposables.add(Event.any(onClick.event, onAuxClick.event)(e => {
const mouseEvent = new StandardMouseEvent(e);
const mouseEvent = new StandardMouseEvent(DOM.getWindow(element), e);
if (!mouseEvent.leftButton && !mouseEvent.middleButton) {
return;
}

View file

@ -5,7 +5,6 @@
import * as browser from 'vs/base/browser/browser';
import { IframeUtils } from 'vs/base/browser/iframe';
import { $window } from 'vs/base/browser/window';
import * as platform from 'vs/base/common/platform';
export interface IMouseEvent {
@ -46,7 +45,7 @@ export class StandardMouseEvent implements IMouseEvent {
public readonly metaKey: boolean;
public readonly timestamp: number;
constructor(e: MouseEvent) {
constructor(targetWindow: Window, e: MouseEvent) {
this.timestamp = Date.now();
this.browserEvent = e;
this.leftButton = e.button === 0;
@ -75,7 +74,7 @@ export class StandardMouseEvent implements IMouseEvent {
}
// Find the position of the iframe this code is executing in relative to the iframe where the event was captured.
const iframeOffsets = IframeUtils.getPositionOfChildWindowRelativeToAncestorWindow($window, e.view);
const iframeOffsets = IframeUtils.getPositionOfChildWindowRelativeToAncestorWindow(targetWindow, e.view);
this.posx -= iframeOffsets.left;
this.posy -= iframeOffsets.top;
}
@ -93,8 +92,8 @@ export class DragMouseEvent extends StandardMouseEvent {
public readonly dataTransfer: DataTransfer;
constructor(e: MouseEvent) {
super(e);
constructor(targetWindow: Window, e: MouseEvent) {
super(targetWindow, e);
this.dataTransfer = (<any>e).dataTransfer;
}
}

View file

@ -1390,7 +1390,7 @@ export class List<T> implements ISpliceable<T>, IDisposable {
const fromMouse = Event.chain(this.view.onContextMenu, $ =>
$.filter(_ => !didJustPressContextMenuKey)
.map(({ element, index, browserEvent }) => ({ element, index, anchor: new StandardMouseEvent(browserEvent), browserEvent }))
.map(({ element, index, browserEvent }) => ({ element, index, anchor: new StandardMouseEvent(getWindow(this.view.domNode), browserEvent), browserEvent }))
);
return Event.any<IListContextMenuEvent<T>>(fromKeyDown, fromKeyUp, fromMouse);

View file

@ -478,7 +478,7 @@ class BaseMenuActionViewItem extends BaseActionViewItem {
// => to get the Copy and Paste context menu actions working on Firefox,
// there should be no timeout here
if (isFirefox) {
const mouseEvent = new StandardMouseEvent(e);
const mouseEvent = new StandardMouseEvent(getWindow(this.element), e);
// Allowing right click to trigger the event causes the issue described below,
// but since the solution below does not work in FF, we must disable right click

View file

@ -258,7 +258,7 @@ export class MenuBar extends Disposable {
this._register(DOM.addDisposableListener(buttonElement, DOM.EventType.MOUSE_DOWN, (e: MouseEvent) => {
// Ignore non-left-click
const mouseEvent = new StandardMouseEvent(e);
const mouseEvent = new StandardMouseEvent(DOM.getWindow(buttonElement), e);
if (!mouseEvent.leftButton) {
e.preventDefault();
return;
@ -367,7 +367,7 @@ export class MenuBar extends Disposable {
this._register(DOM.addDisposableListener(buttonElement, DOM.EventType.MOUSE_DOWN, (e) => {
// Ignore non-left-click
const mouseEvent = new StandardMouseEvent(e);
const mouseEvent = new StandardMouseEvent(DOM.getWindow(buttonElement), e);
if (!mouseEvent.leftButton) {
e.preventDefault();
return;

View file

@ -12,19 +12,19 @@ import { Disposable, IDisposable } from 'vs/base/common/lifecycle';
export abstract class Widget extends Disposable {
protected onclick(domNode: HTMLElement, listener: (e: IMouseEvent) => void): void {
this._register(dom.addDisposableListener(domNode, dom.EventType.CLICK, (e: MouseEvent) => listener(new StandardMouseEvent(e))));
this._register(dom.addDisposableListener(domNode, dom.EventType.CLICK, (e: MouseEvent) => listener(new StandardMouseEvent(dom.getWindow(domNode), e))));
}
protected onmousedown(domNode: HTMLElement, listener: (e: IMouseEvent) => void): void {
this._register(dom.addDisposableListener(domNode, dom.EventType.MOUSE_DOWN, (e: MouseEvent) => listener(new StandardMouseEvent(e))));
this._register(dom.addDisposableListener(domNode, dom.EventType.MOUSE_DOWN, (e: MouseEvent) => listener(new StandardMouseEvent(dom.getWindow(domNode), e))));
}
protected onmouseover(domNode: HTMLElement, listener: (e: IMouseEvent) => void): void {
this._register(dom.addDisposableListener(domNode, dom.EventType.MOUSE_OVER, (e: MouseEvent) => listener(new StandardMouseEvent(e))));
this._register(dom.addDisposableListener(domNode, dom.EventType.MOUSE_OVER, (e: MouseEvent) => listener(new StandardMouseEvent(dom.getWindow(domNode), e))));
}
protected onmouseleave(domNode: HTMLElement, listener: (e: IMouseEvent) => void): void {
this._register(dom.addDisposableListener(domNode, dom.EventType.MOUSE_LEAVE, (e: MouseEvent) => listener(new StandardMouseEvent(e))));
this._register(dom.addDisposableListener(domNode, dom.EventType.MOUSE_LEAVE, (e: MouseEvent) => listener(new StandardMouseEvent(dom.getWindow(domNode), e))));
}
protected onkeydown(domNode: HTMLElement, listener: (e: IKeyboardEvent) => void): void {

View file

@ -781,6 +781,10 @@ export class DisposableMap<K, V extends IDisposable = IDisposable> implements ID
return this._store.keys();
}
values(): IterableIterator<V> {
return this._store.values();
}
[Symbol.iterator](): IterableIterator<[K, V]> {
return this._store[Symbol.iterator]();
}

View file

@ -128,7 +128,7 @@ export class EditorMouseEvent extends StandardMouseEvent {
public readonly relativePos: CoordinatesRelativeToEditor;
constructor(e: MouseEvent, isFromPointerCapture: boolean, editorViewDomNode: HTMLElement) {
super(e);
super(dom.getWindow(editorViewDomNode), e);
this.isFromPointerCapture = isFromPointerCapture;
this.pos = new PageCoordinates(this.posx, this.posy);
this.editorPos = createEditorPagePosition(editorViewDomNode);

View file

@ -98,7 +98,7 @@ export class StickyScrollController extends Disposable implements IEditorContrib
}
}));
this._register(dom.addDisposableListener(stickyScrollDomNode, dom.EventType.CONTEXT_MENU, async (event: MouseEvent) => {
this._onContextMenu(event);
this._onContextMenu(dom.getWindow(stickyScrollDomNode), event);
}));
this._stickyScrollFocusedContextKey = EditorContextKeys.stickyScrollFocused.bindTo(this._contextKeyService);
this._stickyScrollVisibleContextKey = EditorContextKeys.stickyScrollVisible.bindTo(this._contextKeyService);
@ -378,8 +378,8 @@ export class StickyScrollController extends Disposable implements IEditorContrib
}));
}
private _onContextMenu(e: MouseEvent) {
const event = new StandardMouseEvent(e);
private _onContextMenu(targetWindow: Window, e: MouseEvent) {
const event = new StandardMouseEvent(targetWindow, e);
this._contextMenuService.showContextMenu({
menuId: MenuId.StickyScrollContext,

View file

@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { addDisposableListener } from 'vs/base/browser/dom';
import { addDisposableListener, getWindow } from 'vs/base/browser/dom';
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
import { IToolBarOptions, ToolBar } from 'vs/base/browser/ui/toolbar/toolbar';
import { IAction, Separator, SubmenuAction, toAction, WorkbenchActionExecutedClassification, WorkbenchActionExecutedEvent } from 'vs/base/common/actions';
@ -184,7 +184,7 @@ export class WorkbenchToolBar extends ToolBar {
// add context menu for toggle actions
if (toggleActions.length > 0) {
this._sessionDisposables.add(addDisposableListener(this.getElement(), 'contextmenu', e => {
const event = new StandardMouseEvent(e);
const event = new StandardMouseEvent(getWindow(this.getElement()), e);
const action = this.getItemAction(event.target);
if (!(action)) {

View file

@ -95,14 +95,14 @@ export class ContextMenuHandler {
menu.onDidCancel(() => this.contextViewService.hideContextView(true), null, menuDisposables);
menu.onDidBlur(() => this.contextViewService.hideContextView(true), null, menuDisposables);
const window = getWindow(container);
menuDisposables.add(addDisposableListener(window, EventType.BLUR, () => this.contextViewService.hideContextView(true)));
menuDisposables.add(addDisposableListener(window, EventType.MOUSE_DOWN, (e: MouseEvent) => {
const targetWindow = getWindow(container);
menuDisposables.add(addDisposableListener(targetWindow, EventType.BLUR, () => this.contextViewService.hideContextView(true)));
menuDisposables.add(addDisposableListener(targetWindow, EventType.MOUSE_DOWN, (e: MouseEvent) => {
if (e.defaultPrevented) {
return;
}
const event = new StandardMouseEvent(e);
const event = new StandardMouseEvent(targetWindow, e);
let element: HTMLElement | null = event.target;
// Don't do anything as we are likely creating a context menu

View file

@ -36,15 +36,11 @@ export class QuickInputBox extends Disposable {
}
onKeyDown = (handler: (event: StandardKeyboardEvent) => void): IDisposable => {
return dom.addDisposableListener(this.findInput.inputBox.inputElement, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => {
handler(new StandardKeyboardEvent(e));
});
return dom.addStandardDisposableListener(this.findInput.inputBox.inputElement, dom.EventType.KEY_DOWN, handler);
};
onMouseDown = (handler: (event: StandardMouseEvent) => void): IDisposable => {
return dom.addDisposableListener(this.findInput.inputBox.inputElement, dom.EventType.MOUSE_DOWN, (e: MouseEvent) => {
handler(new StandardMouseEvent(e));
});
return dom.addStandardDisposableListener(this.findInput.inputBox.inputElement, dom.EventType.MOUSE_DOWN, handler);
};
onDidChange = (handler: (event: string) => void): IDisposable => {

View file

@ -20,7 +20,6 @@ import { StatusBarItemsExtensionPoint } from 'vs/workbench/api/browser/statusBar
import './mainThreadLocalization';
import './mainThreadBulkEdits';
import './mainThreadChatProvider';
import './mainThreadChatAgents';
import './mainThreadChatAgents2';
import './mainThreadChatVariables';
import './mainThreadCodeInsets';

View file

@ -3,31 +3,23 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { DeferredPromise } from 'vs/base/common/async';
import { Emitter } from 'vs/base/common/event';
import { IMarkdownString } from 'vs/base/common/htmlContent';
import { Disposable, DisposableMap } from 'vs/base/common/lifecycle';
import { revive } from 'vs/base/common/marshalling';
import { URI, UriComponents } from 'vs/base/common/uri';
import { ExtHostChatShape, ExtHostContext, IChatRequestDto, IChatResponseProgressDto, ILocationDto, MainContext, MainThreadChatShape } from 'vs/workbench/api/common/extHost.protocol';
import { ExtHostChatShape, ExtHostContext, MainContext, MainThreadChatShape } from 'vs/workbench/api/common/extHost.protocol';
import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat';
import { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService';
import { isCompleteInteractiveProgressTreeData } from 'vs/workbench/contrib/chat/common/chatModel';
import { IChat, IChatDynamicRequest, IChatProgress, IChatResponse, IChatResponseProgressFileTreeData, IChatService } from 'vs/workbench/contrib/chat/common/chatService';
import { IChatDynamicRequest, IChatService } from 'vs/workbench/contrib/chat/common/chatService';
import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers';
@extHostNamedCustomer(MainContext.MainThreadChat)
export class MainThreadChat extends Disposable implements MainThreadChatShape {
private readonly _providerRegistrations = this._register(new DisposableMap<number>());
private readonly _activeRequestProgressCallbacks = new Map<string, (progress: IChatProgress) => (DeferredPromise<string | IMarkdownString> | void)>();
private readonly _stateEmitters = new Map<number, Emitter<any>>();
private readonly _proxy: ExtHostChatShape;
private _responsePartHandlePool = 0;
private readonly _activeResponsePartPromises = new Map<string, DeferredPromise<string | IMarkdownString | { treeData: IChatResponseProgressFileTreeData }>>();
constructor(
extHostContext: IExtHostContext,
@IChatService private readonly _chatService: IChatService,
@ -64,8 +56,8 @@ export class MainThreadChat extends Disposable implements MainThreadChatShape {
const unreg = this._chatService.registerProvider({
id,
displayName: registration.label,
prepareSession: async (initialState, token) => {
const session = await this._proxy.$prepareChat(handle, initialState, token);
prepareSession: async (token) => {
const session = await this._proxy.$prepareChat(handle, token);
if (!session) {
return undefined;
}
@ -75,14 +67,13 @@ export class MainThreadChat extends Disposable implements MainThreadChatShape {
const emitter = new Emitter<any>();
this._stateEmitters.set(session.id, emitter);
return <IChat>{
return {
id: session.id,
requesterUsername: session.requesterUsername,
requesterAvatarIconUri: URI.revive(session.requesterAvatarIconUri),
responderUsername: session.responderUsername,
responderAvatarIconUri,
inputPlaceholder: session.inputPlaceholder,
onDidChangeState: emitter.event,
dispose: () => {
emitter.dispose();
this._stateEmitters.delete(session.id);
@ -90,87 +81,17 @@ export class MainThreadChat extends Disposable implements MainThreadChatShape {
}
};
},
provideReply: async (request, progress, token) => {
const id = `${handle}_${request.session.id}`;
this._activeRequestProgressCallbacks.set(id, progress);
try {
const requestDto: IChatRequestDto = {
message: request.message,
variables: request.variables
};
const dto = await this._proxy.$provideReply(handle, request.session.id, requestDto, token);
return <IChatResponse>{
session: request.session,
...dto
};
} finally {
this._activeRequestProgressCallbacks.delete(id);
}
},
provideWelcomeMessage: (token) => {
return this._proxy.$provideWelcomeMessage(handle, token);
},
provideSampleQuestions: (token) => {
return this._proxy.$provideSampleQuestions(handle, token);
},
provideSlashCommands: (session, token) => {
return this._proxy.$provideSlashCommands(handle, session.id, token);
},
provideFollowups: (session, token) => {
return this._proxy.$provideFollowups(handle, session.id, token);
},
removeRequest: (session, requestId) => {
return this._proxy.$removeRequest(handle, session.id, requestId);
}
});
this._providerRegistrations.set(handle, unreg);
}
async $acceptResponseProgress(handle: number, sessionId: number, progress: IChatResponseProgressDto, responsePartHandle?: number): Promise<number | void> {
const id = `${handle}_${sessionId}`;
if ('placeholder' in progress) {
const responsePartId = `${id}_${++this._responsePartHandlePool}`;
const deferredContentPromise = new DeferredPromise<string | IMarkdownString | { treeData: IChatResponseProgressFileTreeData }>();
this._activeResponsePartPromises.set(responsePartId, deferredContentPromise);
this._activeRequestProgressCallbacks.get(id)?.({ ...progress, resolvedContent: deferredContentPromise.p });
return this._responsePartHandlePool;
} else if (responsePartHandle) {
// Complete an existing deferred promise with resolved content
const responsePartId = `${id}_${responsePartHandle}`;
const deferredContentPromise = this._activeResponsePartPromises.get(responsePartId);
if (deferredContentPromise && isCompleteInteractiveProgressTreeData(progress)) {
const withRevivedUris = revive<{ treeData: IChatResponseProgressFileTreeData }>(progress);
deferredContentPromise.complete(withRevivedUris);
this._activeResponsePartPromises.delete(responsePartId);
} else if (deferredContentPromise && 'content' in progress) {
deferredContentPromise.complete(progress.content);
this._activeResponsePartPromises.delete(responsePartId);
}
return;
}
// No need to support standalone tree data that's not attached to a placeholder in API
if (isCompleteInteractiveProgressTreeData(progress)) {
return;
}
// TS won't let us change the type of `progress`
let revivedProgress: IChatProgress;
if ('documents' in progress) {
revivedProgress = { documents: revive(progress.documents) };
} else if ('reference' in progress) {
revivedProgress = revive<{ reference: UriComponents | ILocationDto }>(progress);
} else if ('inlineReference' in progress) {
revivedProgress = revive<{ inlineReference: UriComponents | ILocationDto; name?: string }>(progress);
} else {
revivedProgress = progress;
}
this._activeRequestProgressCallbacks.get(id)?.(revivedProgress);
}
async $acceptChatState(sessionId: number, state: any): Promise<void> {
this._stateEmitters.get(sessionId)?.fire(state);
}

View file

@ -1,75 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { DisposableMap } from 'vs/base/common/lifecycle';
import { revive } from 'vs/base/common/marshalling';
import { IProgress } from 'vs/platform/progress/common/progress';
import { ExtHostChatAgentsShape, ExtHostContext, MainContext, MainThreadChatAgentsShape } from 'vs/workbench/api/common/extHost.protocol';
import { IChatAgentCommand, IChatAgentMetadata, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
import { IChatProgress } from 'vs/workbench/contrib/chat/common/chatService';
import { IChatSlashFragment } from 'vs/workbench/contrib/chat/common/chatSlashCommands';
import { IExtHostContext, extHostNamedCustomer } from 'vs/workbench/services/extensions/common/extHostCustomers';
@extHostNamedCustomer(MainContext.MainThreadChatAgents)
export class MainThreadChatAgents implements MainThreadChatAgentsShape {
private readonly _agents = new DisposableMap<number>;
private readonly _pendingProgress = new Map<number, IProgress<IChatProgress>>();
private readonly _proxy: ExtHostChatAgentsShape;
constructor(
extHostContext: IExtHostContext,
@IChatAgentService private readonly _chatAgentService: IChatAgentService
) {
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostChatAgents);
}
$unregisterAgent(handle: number): void {
this._agents.deleteAndDispose(handle);
}
dispose(): void {
this._agents.clearAndDisposeAll();
}
$registerAgent(handle: number, name: string, metadata: IChatAgentMetadata & { subCommands: IChatAgentCommand[] }): void {
const d = this._chatAgentService.registerAgent({
id: name,
metadata: revive(metadata),
invoke: async (request, progress, history, token) => {
const requestId = Math.random();
this._pendingProgress.set(requestId, { report: progress });
try {
const message = request.command ? `/${request.command} ${request.message}` : request.message;
const result = await this._proxy.$invokeAgent(handle, requestId, message, { history }, token);
return {
followUp: result?.followUp ?? [],
};
} finally {
this._pendingProgress.delete(requestId);
}
},
async provideSlashCommands() {
return metadata.subCommands;
},
});
this._agents.set(handle, d);
}
async $handleProgressChunk(requestId: number, chunk: IChatSlashFragment): Promise<void> {
// An extra step because TS really struggles with type inference in the Revived generic parameter?
const revived = revive<IChatSlashFragment>(chunk);
if (typeof revived.content === 'string') {
this._pendingProgress.get(requestId)?.report({ content: revived.content });
} else {
this._pendingProgress.get(requestId)?.report(revived.content);
}
}
$unregisterCommand(handle: number): void {
this._agents.deleteAndDispose(handle);
}
}

View file

@ -75,7 +75,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA
}
},
provideFollowups: async (sessionId, token): Promise<IChatFollowup[]> => {
if (!this._agents.get(handle)?.hasSlashCommands) {
if (!this._agents.get(handle)?.hasFollowups) {
return [];
}

View file

@ -6,110 +6,109 @@
import { CancellationTokenSource } from 'vs/base/common/cancellation';
import * as errors from 'vs/base/common/errors';
import { Emitter, Event } from 'vs/base/common/event';
import { combinedDisposable } from 'vs/base/common/lifecycle';
import { Schemas } from 'vs/base/common/network';
import Severity from 'vs/base/common/severity';
import { URI } from 'vs/base/common/uri';
import { TextEditorCursorStyle } from 'vs/editor/common/config/editorOptions';
import { OverviewRulerLane } from 'vs/editor/common/model';
import * as languageConfiguration from 'vs/editor/common/languages/languageConfiguration';
import { score } from 'vs/editor/common/languageSelector';
import * as languageConfiguration from 'vs/editor/common/languages/languageConfiguration';
import { OverviewRulerLane } from 'vs/editor/common/model';
import { ExtensionIdentifierSet, IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import * as files from 'vs/platform/files/common/files';
import { ExtHostContext, MainContext, CandidatePortSource, ExtHostLogLevelServiceShape } from 'vs/workbench/api/common/extHost.protocol';
import { UIKind } from 'vs/workbench/services/extensions/common/extensionHostProtocol';
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { ILogService, ILoggerService, LogLevel } from 'vs/platform/log/common/log';
import { matchesScheme } from 'vs/platform/opener/common/opener';
import { getRemoteName } from 'vs/platform/remote/common/remoteHosts';
import { TelemetryTrustedValue } from 'vs/platform/telemetry/common/telemetryUtils';
import { EditSessionIdentityMatch } from 'vs/platform/workspace/common/editSessions';
import { CandidatePortSource, ExtHostContext, ExtHostLogLevelServiceShape, MainContext } from 'vs/workbench/api/common/extHost.protocol';
import { ExtHostRelatedInformation } from 'vs/workbench/api/common/extHostAiRelatedInformation';
import { ExtHostApiCommands } from 'vs/workbench/api/common/extHostApiCommands';
import { IExtHostApiDeprecationService } from 'vs/workbench/api/common/extHostApiDeprecationService';
import { ExtHostAuthentication } from 'vs/workbench/api/common/extHostAuthentication';
import { ExtHostBulkEdits } from 'vs/workbench/api/common/extHostBulkEdits';
import { ExtHostChat } from 'vs/workbench/api/common/extHostChat';
import { ExtHostChatAgents2 } from 'vs/workbench/api/common/extHostChatAgents2';
import { ExtHostChatProvider } from 'vs/workbench/api/common/extHostChatProvider';
import { ExtHostChatVariables } from 'vs/workbench/api/common/extHostChatVariables';
import { ExtHostClipboard } from 'vs/workbench/api/common/extHostClipboard';
import { ExtHostEditorInsets } from 'vs/workbench/api/common/extHostCodeInsets';
import { IExtHostCommands } from 'vs/workbench/api/common/extHostCommands';
import { createExtHostComments } from 'vs/workbench/api/common/extHostComments';
import { ExtHostConfigProvider, IExtHostConfiguration } from 'vs/workbench/api/common/extHostConfiguration';
import { ExtHostCustomEditors } from 'vs/workbench/api/common/extHostCustomEditors';
import { IExtHostDebugService } from 'vs/workbench/api/common/extHostDebugService';
import { IExtHostDecorations } from 'vs/workbench/api/common/extHostDecorations';
import { ExtHostDiagnostics } from 'vs/workbench/api/common/extHostDiagnostics';
import { ExtHostDialogs } from 'vs/workbench/api/common/extHostDialogs';
import { ExtHostDocumentContentProvider } from 'vs/workbench/api/common/extHostDocumentContentProviders';
import { ExtHostDocumentSaveParticipant } from 'vs/workbench/api/common/extHostDocumentSaveParticipant';
import { ExtHostDocuments } from 'vs/workbench/api/common/extHostDocuments';
import { IExtHostDocumentsAndEditors } from 'vs/workbench/api/common/extHostDocumentsAndEditors';
import { IExtHostEditorTabs } from 'vs/workbench/api/common/extHostEditorTabs';
import { ExtHostAiEmbeddingVector } from 'vs/workbench/api/common/extHostEmbeddingVector';
import { Extension, IExtHostExtensionService } from 'vs/workbench/api/common/extHostExtensionService';
import { ExtHostFileSystem } from 'vs/workbench/api/common/extHostFileSystem';
import { IExtHostConsumerFileSystem } from 'vs/workbench/api/common/extHostFileSystemConsumer';
import { ExtHostFileSystemEventService, FileSystemWatcherCreateOptions } from 'vs/workbench/api/common/extHostFileSystemEventService';
import { IExtHostFileSystemInfo } from 'vs/workbench/api/common/extHostFileSystemInfo';
import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService';
import { ExtHostInteractiveEditor } from 'vs/workbench/api/common/extHostInlineChat';
import { ExtHostInteractive } from 'vs/workbench/api/common/extHostInteractive';
import { ExtHostIssueReporter } from 'vs/workbench/api/common/extHostIssueReporter';
import { ExtHostLabelService } from 'vs/workbench/api/common/extHostLabelService';
import { ExtHostLanguageFeatures } from 'vs/workbench/api/common/extHostLanguageFeatures';
import { ExtHostLanguages } from 'vs/workbench/api/common/extHostLanguages';
import { IExtHostLocalizationService } from 'vs/workbench/api/common/extHostLocalizationService';
import { IExtHostManagedSockets } from 'vs/workbench/api/common/extHostManagedSockets';
import { ExtHostMessageService } from 'vs/workbench/api/common/extHostMessageService';
import { ExtHostNotebookController } from 'vs/workbench/api/common/extHostNotebook';
import { ExtHostNotebookDocumentSaveParticipant } from 'vs/workbench/api/common/extHostNotebookDocumentSaveParticipant';
import { ExtHostNotebookDocuments } from 'vs/workbench/api/common/extHostNotebookDocuments';
import { ExtHostNotebookEditors } from 'vs/workbench/api/common/extHostNotebookEditors';
import { ExtHostNotebookKernels } from 'vs/workbench/api/common/extHostNotebookKernels';
import { ExtHostNotebookRenderers } from 'vs/workbench/api/common/extHostNotebookRenderers';
import { IExtHostOutputService } from 'vs/workbench/api/common/extHostOutput';
import { ExtHostProfileContentHandlers } from 'vs/workbench/api/common/extHostProfileContentHandler';
import { ExtHostProgress } from 'vs/workbench/api/common/extHostProgress';
import { ExtHostQuickDiff } from 'vs/workbench/api/common/extHostQuickDiff';
import { createExtHostQuickOpen } from 'vs/workbench/api/common/extHostQuickOpen';
import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService';
import { ExtHostSCM } from 'vs/workbench/api/common/extHostSCM';
import { IExtHostSearch } from 'vs/workbench/api/common/extHostSearch';
import { IExtHostSecretState } from 'vs/workbench/api/common/extHostSecretState';
import { ExtHostShare } from 'vs/workbench/api/common/extHostShare';
import { ExtHostSpeech } from 'vs/workbench/api/common/extHostSpeech';
import { ExtHostStatusBar } from 'vs/workbench/api/common/extHostStatusBar';
import { IExtHostStorage } from 'vs/workbench/api/common/extHostStorage';
import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths';
import { IExtHostTask } from 'vs/workbench/api/common/extHostTask';
import { ExtHostTelemetryLogger, IExtHostTelemetry, isNewAppInstall } from 'vs/workbench/api/common/extHostTelemetry';
import { IExtHostTerminalService } from 'vs/workbench/api/common/extHostTerminalService';
import { ExtHostTesting } from 'vs/workbench/api/common/extHostTesting';
import { ExtHostEditors } from 'vs/workbench/api/common/extHostTextEditors';
import { ExtHostTheming } from 'vs/workbench/api/common/extHostTheming';
import { ExtHostTimeline } from 'vs/workbench/api/common/extHostTimeline';
import { ExtHostTreeViews } from 'vs/workbench/api/common/extHostTreeViews';
import { IExtHostTunnelService } from 'vs/workbench/api/common/extHostTunnelService';
import * as typeConverters from 'vs/workbench/api/common/extHostTypeConverters';
import * as extHostTypes from 'vs/workbench/api/common/extHostTypes';
import { TelemetryTrustedValue } from 'vs/platform/telemetry/common/telemetryUtils';
import { ExtHostUriOpeners } from 'vs/workbench/api/common/extHostUriOpener';
import { IURITransformerService } from 'vs/workbench/api/common/extHostUriTransformerService';
import { ExtHostUrls } from 'vs/workbench/api/common/extHostUrls';
import { ExtHostWebviews } from 'vs/workbench/api/common/extHostWebview';
import { ExtHostWebviewPanels } from 'vs/workbench/api/common/extHostWebviewPanels';
import { ExtHostWebviewViews } from 'vs/workbench/api/common/extHostWebviewView';
import { IExtHostWindow } from 'vs/workbench/api/common/extHostWindow';
import { IExtHostWorkspace } from 'vs/workbench/api/common/extHostWorkspace';
import { ProxyIdentifier } from 'vs/workbench/services/extensions/common/proxyIdentifier';
import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry';
import type * as vscode from 'vscode';
import { ExtensionIdentifierSet, IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { ExtHostEditorInsets } from 'vs/workbench/api/common/extHostCodeInsets';
import { ExtHostLabelService } from 'vs/workbench/api/common/extHostLabelService';
import { getRemoteName } from 'vs/platform/remote/common/remoteHosts';
import { ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { IExtHostDecorations } from 'vs/workbench/api/common/extHostDecorations';
import { IExtHostTask } from 'vs/workbench/api/common/extHostTask';
import { IExtHostDebugService } from 'vs/workbench/api/common/extHostDebugService';
import { IExtHostSearch } from 'vs/workbench/api/common/extHostSearch';
import { ILoggerService, ILogService, LogLevel } from 'vs/platform/log/common/log';
import { IURITransformerService } from 'vs/workbench/api/common/extHostUriTransformerService';
import { IExtHostRpcService } from 'vs/workbench/api/common/extHostRpcService';
import { IExtHostInitDataService } from 'vs/workbench/api/common/extHostInitDataService';
import { ExtHostNotebookController } from 'vs/workbench/api/common/extHostNotebook';
import { ExtHostTheming } from 'vs/workbench/api/common/extHostTheming';
import { IExtHostTunnelService } from 'vs/workbench/api/common/extHostTunnelService';
import { IExtHostApiDeprecationService } from 'vs/workbench/api/common/extHostApiDeprecationService';
import { ExtHostAuthentication } from 'vs/workbench/api/common/extHostAuthentication';
import { ExtHostTimeline } from 'vs/workbench/api/common/extHostTimeline';
import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths';
import { IExtHostConsumerFileSystem } from 'vs/workbench/api/common/extHostFileSystemConsumer';
import { ExtHostWebviewViews } from 'vs/workbench/api/common/extHostWebviewView';
import { ExtHostCustomEditors } from 'vs/workbench/api/common/extHostCustomEditors';
import { ExtHostWebviewPanels } from 'vs/workbench/api/common/extHostWebviewPanels';
import { ExtHostBulkEdits } from 'vs/workbench/api/common/extHostBulkEdits';
import { IExtHostFileSystemInfo } from 'vs/workbench/api/common/extHostFileSystemInfo';
import { ExtHostTesting } from 'vs/workbench/api/common/extHostTesting';
import { ExtHostUriOpeners } from 'vs/workbench/api/common/extHostUriOpener';
import { IExtHostSecretState } from 'vs/workbench/api/common/extHostSecretState';
import { IExtHostEditorTabs } from 'vs/workbench/api/common/extHostEditorTabs';
import { ExtHostTelemetryLogger, IExtHostTelemetry, isNewAppInstall } from 'vs/workbench/api/common/extHostTelemetry';
import { ExtHostNotebookKernels } from 'vs/workbench/api/common/extHostNotebookKernels';
import { TextSearchCompleteMessageType } from 'vs/workbench/services/search/common/searchExtTypes';
import { ExtHostNotebookRenderers } from 'vs/workbench/api/common/extHostNotebookRenderers';
import { Schemas } from 'vs/base/common/network';
import { matchesScheme } from 'vs/platform/opener/common/opener';
import { ExtHostNotebookEditors } from 'vs/workbench/api/common/extHostNotebookEditors';
import { ExtHostNotebookDocuments } from 'vs/workbench/api/common/extHostNotebookDocuments';
import { ExtHostInteractive } from 'vs/workbench/api/common/extHostInteractive';
import { combinedDisposable } from 'vs/base/common/lifecycle';
import { checkProposedApiEnabled, isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions';
import { DebugConfigurationProviderTriggerKind } from 'vs/workbench/contrib/debug/common/debug';
import { IExtHostLocalizationService } from 'vs/workbench/api/common/extHostLocalizationService';
import { EditSessionIdentityMatch } from 'vs/platform/workspace/common/editSessions';
import { ExtHostProfileContentHandlers } from 'vs/workbench/api/common/extHostProfileContentHandler';
import { ExtHostQuickDiff } from 'vs/workbench/api/common/extHostQuickDiff';
import { ExtHostChat } from 'vs/workbench/api/common/extHostChat';
import { ExtHostInteractiveEditor } from 'vs/workbench/api/common/extHostInlineChat';
import { ExtHostNotebookDocumentSaveParticipant } from 'vs/workbench/api/common/extHostNotebookDocumentSaveParticipant';
import { ExtHostIssueReporter } from 'vs/workbench/api/common/extHostIssueReporter';
import { IExtHostManagedSockets } from 'vs/workbench/api/common/extHostManagedSockets';
import { ExtHostShare } from 'vs/workbench/api/common/extHostShare';
import { ExtHostChatProvider } from 'vs/workbench/api/common/extHostChatProvider';
import { ExtHostSpeech } from 'vs/workbench/api/common/extHostSpeech';
import { ExtHostChatVariables } from 'vs/workbench/api/common/extHostChatVariables';
import { ExtHostRelatedInformation } from 'vs/workbench/api/common/extHostAiRelatedInformation';
import { ExtHostAiEmbeddingVector } from 'vs/workbench/api/common/extHostEmbeddingVector';
import { ExtHostChatAgents } from 'vs/workbench/api/common/extHostChatAgents';
import { ExtHostChatAgents2 } from 'vs/workbench/api/common/extHostChatAgents2';
import { ExtensionDescriptionRegistry } from 'vs/workbench/services/extensions/common/extensionDescriptionRegistry';
import { UIKind } from 'vs/workbench/services/extensions/common/extensionHostProtocol';
import { checkProposedApiEnabled, isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions';
import { ProxyIdentifier } from 'vs/workbench/services/extensions/common/proxyIdentifier';
import { TextSearchCompleteMessageType } from 'vs/workbench/services/search/common/searchExtTypes';
import type * as vscode from 'vscode';
export interface IExtensionRegistries {
mine: ExtensionDescriptionRegistry;
@ -210,10 +209,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
rpcProtocol.set(ExtHostContext.ExtHostInteractive, new ExtHostInteractive(rpcProtocol, extHostNotebook, extHostDocumentsAndEditors, extHostCommands, extHostLogService));
const extHostInteractiveEditor = rpcProtocol.set(ExtHostContext.ExtHostInlineChat, new ExtHostInteractiveEditor(rpcProtocol, extHostCommands, extHostDocuments, extHostLogService));
const extHostChatProvider = rpcProtocol.set(ExtHostContext.ExtHostChatProvider, new ExtHostChatProvider(rpcProtocol, extHostLogService));
const extHostChatAgents = rpcProtocol.set(ExtHostContext.ExtHostChatAgents, new ExtHostChatAgents(rpcProtocol, extHostChatProvider, extHostLogService));
const extHostChatAgents2 = rpcProtocol.set(ExtHostContext.ExtHostChatAgents2, new ExtHostChatAgents2(rpcProtocol, extHostChatProvider, extHostLogService));
const extHostChatVariables = rpcProtocol.set(ExtHostContext.ExtHostChatVariables, new ExtHostChatVariables(rpcProtocol));
const extHostChat = rpcProtocol.set(ExtHostContext.ExtHostChat, new ExtHostChat(rpcProtocol, extHostLogService));
const extHostChat = rpcProtocol.set(ExtHostContext.ExtHostChat, new ExtHostChat(rpcProtocol));
const extHostAiRelatedInformation = rpcProtocol.set(ExtHostContext.ExtHostAiRelatedInformation, new ExtHostRelatedInformation(rpcProtocol));
const extHostAiEmbeddingVector = rpcProtocol.set(ExtHostContext.ExtHostAiEmbeddingVector, new ExtHostAiEmbeddingVector(rpcProtocol));
const extHostIssueReporter = rpcProtocol.set(ExtHostContext.ExtHostIssueReporter, new ExtHostIssueReporter(rpcProtocol));
@ -1382,10 +1380,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
checkProposedApiEnabled(extension, 'chatAgents2');
return extHostChatAgents2.createChatAgent(extension, name, handler);
},
registerAgent(name: string, agent: vscode.ChatAgent, metadata: vscode.ChatAgentMetadata) {
checkProposedApiEnabled(extension, 'chatAgents');
return extHostChatAgents.registerAgent(extension.identifier, name, agent, metadata);
}
};
// namespace: speech

View file

@ -52,8 +52,7 @@ import { IRevealOptions, ITreeItem, IViewBadge } from 'vs/workbench/common/views
import { CallHierarchyItem } from 'vs/workbench/contrib/callHierarchy/common/callHierarchy';
import { IChatAgentCommand, IChatAgentMetadata, IChatAgentRequest, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents';
import { IChatMessage, IChatResponseFragment, IChatResponseProviderMetadata } from 'vs/workbench/contrib/chat/common/chatProvider';
import { IChatAgentDetection, IChatDynamicRequest, IChatFollowup, IChatReplyFollowup, IChatResponseErrorDetails, IChatUserActionEvent, ISlashCommand, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService';
import { IChatSlashFragment } from 'vs/workbench/contrib/chat/common/chatSlashCommands';
import { IChatAgentDetection, IChatDynamicRequest, IChatFollowup, IChatReplyFollowup, IChatResponseErrorDetails, IChatUserActionEvent, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService';
import { IChatRequestVariableValue, IChatVariableData } from 'vs/workbench/contrib/chat/common/chatVariables';
import { DebugConfigurationProviderTriggerKind, IAdapterDescriptor, IConfig, IDebugSessionReplMode } from 'vs/workbench/contrib/debug/common/debug';
import { IInlineChatBulkEditResponse, IInlineChatEditResponse, IInlineChatMessageResponse, IInlineChatProgressItem, IInlineChatRequest, IInlineChatSession, InlineChatResponseFeedbackKind } from 'vs/workbench/contrib/inlineChat/common/inlineChat';
@ -63,6 +62,7 @@ import { ICellExecutionComplete, ICellExecutionStateUpdate } from 'vs/workbench/
import { ICellRange } from 'vs/workbench/contrib/notebook/common/notebookRange';
import { InputValidationType } from 'vs/workbench/contrib/scm/common/scm';
import { IWorkspaceSymbol, NotebookPriorityInfo } from 'vs/workbench/contrib/search/common/search';
import { IRawClosedNotebookFileMatch } from 'vs/workbench/contrib/search/common/searchNotebookHelpers';
import { ISpeechProviderMetadata, ISpeechToTextEvent } from 'vs/workbench/contrib/speech/common/speechService';
import { CoverageDetails, ExtensionRunTestsRequest, ICallProfileRunHandler, IFileCoverage, ISerializedTestResults, IStartControllerTests, ITestItem, ITestMessage, ITestRunProfile, ITestRunTask, ResolvedTestRunRequest, TestResultState, TestsDiffOp } from 'vs/workbench/contrib/testing/common/testTypes';
import { Timeline, TimelineChangeEvent, TimelineOptions, TimelineProviderDescriptor } from 'vs/workbench/contrib/timeline/common/timeline';
@ -80,7 +80,6 @@ import { CandidatePort } from 'vs/workbench/services/remote/common/tunnelModel';
import { ITextQueryBuilderOptions } from 'vs/workbench/services/search/common/queryBuilder';
import * as search from 'vs/workbench/services/search/common/search';
import { ISaveProfileResult } from 'vs/workbench/services/userDataProfile/common/userDataProfile';
import { IRawClosedNotebookFileMatch } from 'vs/workbench/contrib/search/common/searchNotebookHelpers';
export interface IWorkspaceData extends IStaticWorkspaceData {
folders: { uri: UriComponents; name: string; index: number }[];
@ -1166,12 +1165,6 @@ export interface ExtHostChatProviderShape {
$handleResponseFragment(requestId: number, chunk: IChatResponseFragment): Promise<void>;
}
export interface MainThreadChatAgentsShape extends IDisposable {
$registerAgent(handle: number, name: string, metadata: IChatAgentMetadata & { subCommands: IChatAgentCommand[] }): void;
$unregisterAgent(handle: number): void;
$handleProgressChunk(requestId: number, chunk: IChatSlashFragment): Promise<void>;
}
export interface IExtensionChatAgentMetadata extends Dto<IChatAgentMetadata> {
hasSlashCommands?: boolean;
hasFollowup?: boolean;
@ -1184,10 +1177,6 @@ export interface MainThreadChatAgentsShape2 extends IDisposable {
$handleProgressChunk(requestId: string, chunk: IChatResponseProgressDto, responsePartHandle?: number): Promise<number | void>;
}
export interface ExtHostChatAgentsShape {
$invokeAgent(handle: number, requestId: number, prompt: string, context: { history: IChatMessage[] }, token: CancellationToken): Promise<any>;
}
export interface ExtHostChatAgentsShape2 {
$invokeAgent(handle: number, sessionId: string, requestId: string, request: IChatAgentRequest, context: { history: IChatMessage[] }, token: CancellationToken): Promise<IChatAgentResult | undefined>;
$provideSlashCommands(handle: number, token: CancellationToken): Promise<IChatAgentCommand[]>;
@ -1263,7 +1252,6 @@ export type IDocumentContextDto = {
export type IChatResponseProgressDto =
| { content: string | IMarkdownString }
| { requestId: string }
| { placeholder: string }
| { treeData: IChatResponseProgressFileTreeData }
| { documents: IDocumentContextDto[] }
@ -1276,18 +1264,13 @@ export interface MainThreadChatShape extends IDisposable {
$acceptChatState(sessionId: number, state: any): Promise<void>;
$sendRequestToProvider(providerId: string, message: IChatDynamicRequest): void;
$unregisterChatProvider(handle: number): Promise<void>;
$acceptResponseProgress(handle: number, sessionId: number, progress: IChatResponseProgressDto, responsePartHandle?: number): Promise<number | void>;
$transferChatSession(sessionId: number, toWorkspace: UriComponents): void;
}
export interface ExtHostChatShape {
$prepareChat(handle: number, initialState: any, token: CancellationToken): Promise<IChatDto | undefined>;
$prepareChat(handle: number, token: CancellationToken): Promise<IChatDto | undefined>;
$provideWelcomeMessage(handle: number, token: CancellationToken): Promise<(string | IMarkdownString | IChatReplyFollowup[])[] | undefined>;
$provideSampleQuestions(handle: number, token: CancellationToken): Promise<IChatReplyFollowup[] | undefined>;
$provideFollowups(handle: number, sessionId: number, token: CancellationToken): Promise<IChatFollowup[] | undefined>;
$provideReply(handle: number, sessionId: number, request: IChatRequestDto, token: CancellationToken): Promise<IChatResponseDto | undefined>;
$removeRequest(handle: number, sessionId: number, requestId: string): void;
$provideSlashCommands(handle: number, sessionId: number, token: CancellationToken): Promise<ISlashCommand[] | undefined>;
$releaseSession(sessionId: number): void;
$onDidPerformUserAction(event: IChatUserActionEvent): Promise<void>;
}
@ -2721,7 +2704,6 @@ export const MainContext = {
MainThreadAuthentication: createProxyIdentifier<MainThreadAuthenticationShape>('MainThreadAuthentication'),
MainThreadBulkEdits: createProxyIdentifier<MainThreadBulkEditsShape>('MainThreadBulkEdits'),
MainThreadChatProvider: createProxyIdentifier<MainThreadChatProviderShape>('MainThreadChatProvider'),
MainThreadChatAgents: createProxyIdentifier<MainThreadChatAgentsShape>('MainThreadChatAgents'),
MainThreadChatAgents2: createProxyIdentifier<MainThreadChatAgentsShape2>('MainThreadChatAgents2'),
MainThreadChatVariables: createProxyIdentifier<MainThreadChatVariablesShape>('MainThreadChatVariables'),
MainThreadClipboard: createProxyIdentifier<MainThreadClipboardShape>('MainThreadClipboard'),
@ -2844,7 +2826,6 @@ export const ExtHostContext = {
ExtHostInteractive: createProxyIdentifier<ExtHostInteractiveShape>('ExtHostInteractive'),
ExtHostInlineChat: createProxyIdentifier<ExtHostInlineChatShape>('ExtHostInlineChatShape'),
ExtHostChat: createProxyIdentifier<ExtHostChatShape>('ExtHostChat'),
ExtHostChatAgents: createProxyIdentifier<ExtHostChatAgentsShape>('ExtHostChatAgents'),
ExtHostChatAgents2: createProxyIdentifier<ExtHostChatAgentsShape2>('ExtHostChatAgents'),
ExtHostChatVariables: createProxyIdentifier<ExtHostChatVariablesShape>('ExtHostChatVariables'),
ExtHostChatProvider: createProxyIdentifier<ExtHostChatProviderShape>('ExtHostChatProvider'),

View file

@ -3,19 +3,15 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { raceCancellation } from 'vs/base/common/async';
import { CancellationToken } from 'vs/base/common/cancellation';
import { Emitter } from 'vs/base/common/event';
import { IMarkdownString } from 'vs/base/common/htmlContent';
import { Iterable } from 'vs/base/common/iterator';
import { toDisposable } from 'vs/base/common/lifecycle';
import { StopWatch } from 'vs/base/common/stopwatch';
import { localize } from 'vs/nls';
import { IRelaxedExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { ILogService } from 'vs/platform/log/common/log';
import { ExtHostChatShape, IChatRequestDto, IChatResponseDto, IChatDto, IMainContext, MainContext, MainThreadChatShape } from 'vs/workbench/api/common/extHost.protocol';
import { ExtHostChatShape, IChatDto, IMainContext, MainContext, MainThreadChatShape } from 'vs/workbench/api/common/extHost.protocol';
import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters';
import { IChatFollowup, IChatReplyFollowup, IChatUserActionEvent, ISlashCommand } from 'vs/workbench/contrib/chat/common/chatService';
import { IChatReplyFollowup, IChatUserActionEvent } from 'vs/workbench/contrib/chat/common/chatService';
import type * as vscode from 'vscode';
class ChatProviderWrapper<T> {
@ -36,7 +32,6 @@ export class ExtHostChat implements ExtHostChatShape {
private readonly _chatProvider = new Map<number, ChatProviderWrapper<vscode.InteractiveSessionProvider>>();
private readonly _chatSessions = new Map<number, vscode.InteractiveSession>();
// private readonly _providerResponsesByRequestId = new Map<number, { response: vscode.ProviderResult<vscode.InteractiveResponse | vscode.InteractiveResponseForProgress>; sessionId: number }>();
private readonly _onDidPerformUserAction = new Emitter<vscode.InteractiveSessionUserActionEvent>();
public readonly onDidPerformUserAction = this._onDidPerformUserAction.event;
@ -45,7 +40,6 @@ export class ExtHostChat implements ExtHostChatShape {
constructor(
mainContext: IMainContext,
private readonly logService: ILogService
) {
this._proxy = mainContext.getProxy(MainContext.MainThreadChat);
}
@ -75,13 +69,13 @@ export class ExtHostChat implements ExtHostChatShape {
this._proxy.$sendRequestToProvider(providerId, message);
}
async $prepareChat(handle: number, initialState: any, token: CancellationToken): Promise<IChatDto | undefined> {
async $prepareChat(handle: number, token: CancellationToken): Promise<IChatDto | undefined> {
const entry = this._chatProvider.get(handle);
if (!entry) {
return undefined;
}
const session = await entry.provider.prepareSession(initialState, token);
const session = await entry.provider.prepareSession(token);
if (!session) {
return undefined;
}
@ -124,25 +118,6 @@ export class ExtHostChat implements ExtHostChatShape {
});
}
async $provideFollowups(handle: number, sessionId: number, token: CancellationToken): Promise<IChatFollowup[] | undefined> {
const entry = this._chatProvider.get(handle);
if (!entry) {
return undefined;
}
const realSession = this._chatSessions.get(sessionId);
if (!realSession) {
return;
}
if (!entry.provider.provideFollowups) {
return undefined;
}
const rawFollowups = await entry.provider.provideFollowups(realSession, token);
return rawFollowups?.map(f => typeConvert.ChatFollowup.from(f));
}
async $provideSampleQuestions(handle: number, token: CancellationToken): Promise<IChatReplyFollowup[] | undefined> {
const entry = this._chatProvider.get(handle);
if (!entry) {
@ -161,121 +136,6 @@ export class ExtHostChat implements ExtHostChatShape {
return rawFollowups?.map(f => typeConvert.ChatReplyFollowup.from(f));
}
$removeRequest(handle: number, sessionId: number, requestId: string): void {
const entry = this._chatProvider.get(handle);
if (!entry) {
return;
}
const realSession = this._chatSessions.get(sessionId);
if (!realSession) {
return;
}
if (!entry.provider.removeRequest) {
return;
}
entry.provider.removeRequest(realSession, requestId);
}
async $provideReply(handle: number, sessionId: number, request: IChatRequestDto, token: CancellationToken): Promise<IChatResponseDto | undefined> {
const entry = this._chatProvider.get(handle);
if (!entry) {
return undefined;
}
const realSession = this._chatSessions.get(sessionId);
if (!realSession) {
return;
}
const requestObj: vscode.InteractiveRequest = {
session: realSession,
message: request.message,
variables: {}
};
if (request.variables) {
for (const key of Object.keys(request.variables)) {
requestObj.variables[key] = request.variables[key].map(typeConvert.ChatVariable.to);
}
}
const stopWatch = StopWatch.create(false);
let firstProgress: number | undefined;
const progressObj: vscode.Progress<vscode.InteractiveProgress> = {
report: (progress: vscode.InteractiveProgress) => {
if (token.isCancellationRequested) {
return;
}
if (typeof firstProgress === 'undefined') {
firstProgress = stopWatch.elapsed();
}
const convertedProgress = typeConvert.ChatResponseProgress.from(entry.extension, progress);
if ('placeholder' in progress && 'resolvedContent' in progress) {
const resolvedContent = Promise.all([this._proxy.$acceptResponseProgress(handle, sessionId, convertedProgress), progress.resolvedContent]);
raceCancellation(resolvedContent, token).then((res) => {
if (!res) {
return; /* Cancelled */
}
const [progressHandle, progressContent] = res;
this._proxy.$acceptResponseProgress(handle, sessionId, progressContent, progressHandle ?? undefined);
});
} else {
this._proxy.$acceptResponseProgress(handle, sessionId, convertedProgress);
}
}
};
let result: vscode.InteractiveResponseForProgress | undefined | null;
try {
result = await entry.provider.provideResponseWithProgress(requestObj, progressObj, token);
if (!result) {
result = { errorDetails: { message: localize('emptyResponse', "Provider returned null response") } };
}
} catch (err) {
result = { errorDetails: { message: localize('errorResponse', "Error from provider: {0}", err.message), responseIsIncomplete: true } };
this.logService.error(err);
}
try {
// Check that the session has not been released since the request started
if (realSession.saveState && this._chatSessions.has(sessionId)) {
const newState = realSession.saveState();
this._proxy.$acceptChatState(sessionId, newState);
}
} catch (err) {
this.logService.warn(err);
}
const timings = { firstProgress: firstProgress ?? 0, totalElapsed: stopWatch.elapsed() };
return { errorDetails: result.errorDetails, timings };
}
async $provideSlashCommands(handle: number, sessionId: number, token: CancellationToken): Promise<ISlashCommand[] | undefined> {
const entry = this._chatProvider.get(handle);
if (!entry) {
return undefined;
}
const realSession = this._chatSessions.get(sessionId);
if (!realSession) {
return undefined;
}
if (!entry.provider.provideSlashCommands) {
return undefined;
}
const slashCommands = await entry.provider.provideSlashCommands(realSession, token);
return slashCommands?.map(c => (<ISlashCommand>{
...c,
kind: typeConvert.CompletionItemKind.from(c.kind)
}));
}
$releaseSession(sessionId: number) {
this._chatSessions.delete(sessionId);
}

View file

@ -1,91 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { DeferredPromise, raceCancellation } from 'vs/base/common/async';
import { CancellationToken } from 'vs/base/common/cancellation';
import { IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
import { ILogService } from 'vs/platform/log/common/log';
import { Progress } from 'vs/platform/progress/common/progress';
import { ExtHostChatAgentsShape, IMainContext, MainContext, MainThreadChatAgentsShape } from 'vs/workbench/api/common/extHost.protocol';
import { ExtHostChatProvider } from 'vs/workbench/api/common/extHostChatProvider';
import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters';
import { ChatMessageRole } from 'vs/workbench/api/common/extHostTypes';
import { IChatMessage } from 'vs/workbench/contrib/chat/common/chatProvider';
import type * as vscode from 'vscode';
export class ExtHostChatAgents implements ExtHostChatAgentsShape {
private static _idPool = 0;
private readonly _agents = new Map<number, { extension: ExtensionIdentifier; agent: vscode.ChatAgent }>();
private readonly _proxy: MainThreadChatAgentsShape;
constructor(
mainContext: IMainContext,
private readonly _extHostChatProvider: ExtHostChatProvider,
private readonly _logService: ILogService,
) {
this._proxy = mainContext.getProxy(MainContext.MainThreadChatAgents);
}
registerAgent(extension: ExtensionIdentifier, name: string, agent: vscode.ChatAgent, metadata: vscode.ChatAgentMetadata): IDisposable {
const handle = ExtHostChatAgents._idPool++;
this._agents.set(handle, { extension, agent });
this._proxy.$registerAgent(handle, name, metadata);
return toDisposable(() => {
this._proxy.$unregisterAgent(handle);
this._agents.delete(handle);
});
}
async $invokeAgent(handle: number, requestId: number, prompt: string, context: { history: IChatMessage[] }, token: CancellationToken): Promise<any> {
const data = this._agents.get(handle);
if (!data) {
this._logService.warn(`[CHAT](${handle}) CANNOT invoke agent because the agent is not registered`);
return;
}
let done = false;
function throwIfDone() {
if (done) {
throw new Error('Only valid while executing the command');
}
}
const commandExecution = new DeferredPromise<void>();
token.onCancellationRequested(() => commandExecution.complete());
setTimeout(() => commandExecution.complete(), 10 * 1000);
this._extHostChatProvider.allowListExtensionWhile(data.extension, commandExecution.p);
const task = data.agent(
{ role: ChatMessageRole.User, content: prompt },
{ history: context.history.map(typeConvert.ChatMessage.to) },
new Progress<vscode.ChatAgentResponse>(p => {
throwIfDone();
this._proxy.$handleProgressChunk(requestId, { content: isInteractiveProgressFileTree(p.message) ? p.message : p.message.value });
}),
token
);
try {
return await raceCancellation(Promise.resolve(task).then((v) => {
if (v && 'followUp' in v) {
const convertedFollowup = v?.followUp?.map(f => typeConvert.ChatFollowup.from(f));
return { followUp: convertedFollowup };
}
return undefined;
}), token);
} finally {
done = true;
commandExecution.complete();
}
}
}
function isInteractiveProgressFileTree(thing: unknown): thing is vscode.InteractiveProgressFileTree {
return !!thing && typeof thing === 'object' && 'treeData' in thing;
}

View file

@ -217,6 +217,9 @@ class ExtHostChatAgent {
private _fullName: string | undefined;
private _iconPath: vscode.Uri | { light: vscode.Uri; dark: vscode.Uri } | vscode.ThemeIcon | undefined;
private _isDefault: boolean | undefined;
private _helpTextPrefix: string | vscode.MarkdownString | undefined;
private _helpTextPostfix: string | vscode.MarkdownString | undefined;
private _sampleRequest?: string;
private _isSecondary: boolean | undefined;
private _onDidReceiveFeedback = new Emitter<vscode.ChatAgentResult2Feedback>();
private _onDidPerformAction = new Emitter<vscode.ChatAgentUserActionEvent>();
@ -259,7 +262,14 @@ class ExtHostChatAgent {
return [];
}
this._lastSlashCommands = result;
return result.map(c => ({ name: c.name, description: c.description, followupPlaceholder: c.followupPlaceholder, shouldRepopulate: c.shouldRepopulate }));
return result
.map(c => ({
name: c.name,
description: c.description,
followupPlaceholder: c.followupPlaceholder,
shouldRepopulate: c.shouldRepopulate,
sampleRequest: c.sampleRequest
}));
}
async provideFollowups(result: vscode.ChatAgentResult2, token: CancellationToken): Promise<IChatFollowup[]> {
@ -300,6 +310,9 @@ class ExtHostChatAgent {
hasFollowup: this._followupProvider !== undefined,
isDefault: this._isDefault,
isSecondary: this._isSecondary,
helpTextPrefix: (!this._helpTextPrefix || typeof this._helpTextPrefix === 'string') ? this._helpTextPrefix : typeConvert.MarkdownString.from(this._helpTextPrefix),
helpTextPostfix: (!this._helpTextPostfix || typeof this._helpTextPostfix === 'string') ? this._helpTextPostfix : typeConvert.MarkdownString.from(this._helpTextPostfix),
sampleRequest: this._sampleRequest,
});
updateScheduled = false;
});
@ -354,6 +367,32 @@ class ExtHostChatAgent {
that._isDefault = v;
updateMetadataSoon();
},
get helpTextPrefix() {
checkProposedApiEnabled(that.extension, 'defaultChatAgent');
return that._helpTextPrefix;
},
set helpTextPrefix(v) {
checkProposedApiEnabled(that.extension, 'defaultChatAgent');
if (!that._isDefault) {
throw new Error('helpTextPrefix is only available on the default chat agent');
}
that._helpTextPrefix = v;
updateMetadataSoon();
},
get helpTextPostfix() {
checkProposedApiEnabled(that.extension, 'defaultChatAgent');
return that._helpTextPostfix;
},
set helpTextPostfix(v) {
checkProposedApiEnabled(that.extension, 'defaultChatAgent');
if (!that._isDefault) {
throw new Error('helpTextPostfix is only available on the default chat agent');
}
that._helpTextPostfix = v;
updateMetadataSoon();
},
get isSecondary() {
checkProposedApiEnabled(that.extension, 'defaultChatAgent');
return that._isSecondary;
@ -363,6 +402,13 @@ class ExtHostChatAgent {
that._isSecondary = v;
updateMetadataSoon();
},
get sampleRequest() {
return that._sampleRequest;
},
set sampleRequest(v) {
that._sampleRequest = v;
updateMetadataSoon();
},
get onDidReceiveFeedback() {
return that._onDidReceiveFeedback.event;
},

View file

@ -2157,20 +2157,10 @@ export namespace DataTransfer {
}
export namespace ChatReplyFollowup {
export function to(followup: IChatReplyFollowup): vscode.InteractiveSessionReplyFollowup {
return {
message: followup.message,
metadata: followup.metadata,
title: followup.title,
tooltip: followup.tooltip,
};
}
export function from(followup: vscode.InteractiveSessionReplyFollowup): IChatReplyFollowup {
return {
kind: 'reply',
message: followup.message,
metadata: followup.metadata,
title: followup.title,
tooltip: followup.tooltip,
};
@ -2178,7 +2168,7 @@ export namespace ChatReplyFollowup {
}
export namespace ChatFollowup {
export function from(followup: string | vscode.InteractiveSessionFollowup): IChatFollowup {
export function from(followup: string | vscode.ChatAgentFollowup): IChatFollowup {
if (typeof followup === 'string') {
return <IChatReplyFollowup>{ title: followup, message: followup, kind: 'reply' };
} else if ('commandId' in followup) {
@ -2303,11 +2293,9 @@ export namespace InteractiveEditorResponseFeedbackKind {
}
export namespace ChatResponseProgress {
export function from(extension: IExtensionDescription, progress: vscode.InteractiveProgress | vscode.ChatAgentExtendedProgress): extHostProtocol.IChatResponseProgressDto {
export function from(extension: IExtensionDescription, progress: vscode.ChatAgentExtendedProgress): extHostProtocol.IChatResponseProgressDto {
if ('placeholder' in progress && 'resolvedContent' in progress) {
return { placeholder: progress.placeholder };
} else if ('responseId' in progress) {
return { requestId: progress.responseId };
} else if ('markdownContent' in progress) {
checkProposedApiEnabled(extension, 'chatAgents2Additions');
return { content: MarkdownString.from(progress.markdownContent) };

View file

@ -8,7 +8,7 @@ import { localize } from 'vs/nls';
import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { Disposable } from 'vs/base/common/lifecycle';
import { EventHelper, addDisposableListener, getActiveDocument } from 'vs/base/browser/dom';
import { EventHelper, addDisposableListener, getActiveDocument, getWindow } from 'vs/base/browser/dom';
import { IWorkbenchContribution, IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from 'vs/workbench/common/contributions';
import { Registry } from 'vs/platform/registry/common/platform';
import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';
@ -78,12 +78,12 @@ export class TextInputActionsProvider extends Disposable implements IWorkbenchCo
// Context menu support in input/textarea
this._register(Event.runAndSubscribe(this.layoutService.onDidAddContainer, container => {
const listener = addDisposableListener(container, 'contextmenu', e => this.onContextMenu(e));
const listener = addDisposableListener(container, 'contextmenu', e => this.onContextMenu(getWindow(container), e));
this._register(Event.filter(this.layoutService.onDidRemoveContainer, removed => removed === container, this._store)(() => listener.dispose()));
}, this.layoutService.container));
}
private onContextMenu(e: MouseEvent): void {
private onContextMenu(targetWindow: Window, e: MouseEvent): void {
if (e.defaultPrevented) {
return; // make sure to not show these actions by accident if component indicated to prevent
}
@ -95,7 +95,7 @@ export class TextInputActionsProvider extends Disposable implements IWorkbenchCo
EventHelper.stop(e, true);
const event = new StandardMouseEvent(e);
const event = new StandardMouseEvent(targetWindow, e);
this.contextMenuService.showContextMenu({
getAnchor: () => event,

View file

@ -9,7 +9,7 @@ import { IActivity } from 'vs/workbench/services/activity/common/activity';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { ActionBar, ActionsOrientation } from 'vs/base/browser/ui/actionbar/actionbar';
import { CompositeActionViewItem, CompositeOverflowActivityAction, CompositeOverflowActivityActionViewItem, CompositeBarAction, ICompositeBar, ICompositeBarColors, IActivityHoverOptions } from 'vs/workbench/browser/parts/compositeBarActions';
import { Dimension, $, addDisposableListener, EventType, EventHelper, isAncestor } from 'vs/base/browser/dom';
import { Dimension, $, addDisposableListener, EventType, EventHelper, isAncestor, getWindow } from 'vs/base/browser/dom';
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
import { Widget } from 'vs/base/browser/ui/widget';
@ -227,9 +227,9 @@ export class CompositeBar extends Widget implements ICompositeBar {
}));
// Contextmenu for composites
this._register(addDisposableListener(parent, EventType.CONTEXT_MENU, e => this.showContextMenu(e)));
this._register(addDisposableListener(parent, EventType.CONTEXT_MENU, e => this.showContextMenu(getWindow(parent), e)));
this._register(Gesture.addTarget(parent));
this._register(addDisposableListener(parent, TouchEventType.Contextmenu, e => this.showContextMenu(e)));
this._register(addDisposableListener(parent, TouchEventType.Contextmenu, e => this.showContextMenu(getWindow(parent), e)));
// Register a drop target on the whole bar to prevent forbidden feedback
let insertDropBefore: Before2D | undefined = undefined;
@ -620,10 +620,10 @@ export class CompositeBar extends Widget implements ICompositeBar {
return this.model.visibleItems.filter(c => overflowingIds.includes(c.id)).map(item => { return { id: item.id, name: this.getAction(item.id)?.label || item.name }; });
}
private showContextMenu(e: MouseEvent | GestureEvent): void {
private showContextMenu(targetWindow: Window, e: MouseEvent | GestureEvent): void {
EventHelper.stop(e, true);
const event = new StandardMouseEvent(e);
const event = new StandardMouseEvent(targetWindow, e);
this.contextMenuService.showContextMenu({
getAnchor: () => event,
getActions: () => this.getContextMenuActions(e)

View file

@ -11,7 +11,7 @@ import { EditorInput } from 'vs/workbench/common/editor/editorInput';
import { SideBySideEditorInput } from 'vs/workbench/common/editor/sideBySideEditorInput';
import { Emitter, Relay } from 'vs/base/common/event';
import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation';
import { Dimension, trackFocus, addDisposableListener, EventType, EventHelper, findParentWithClass, isAncestor, IDomNodePagePosition, isMouseEvent, isActiveElement, focusWindow } from 'vs/base/browser/dom';
import { Dimension, trackFocus, addDisposableListener, EventType, EventHelper, findParentWithClass, isAncestor, IDomNodePagePosition, isMouseEvent, isActiveElement, focusWindow, getWindow } from 'vs/base/browser/dom';
import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection';
import { IContextKeyService } from 'vs/platform/contextkey/common/contextkey';
import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar';
@ -405,7 +405,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView {
// Find target anchor
let anchor: HTMLElement | StandardMouseEvent = this.element;
if (e) {
anchor = new StandardMouseEvent(e);
anchor = new StandardMouseEvent(getWindow(this.element), e);
}
// Show it

View file

@ -6,7 +6,7 @@
import 'vs/css!./media/editortabscontrol';
import { localize } from 'vs/nls';
import { applyDragImage, DataTransfers } from 'vs/base/browser/dnd';
import { Dimension, isMouseEvent } from 'vs/base/browser/dom';
import { Dimension, getWindow, isMouseEvent } from 'vs/base/browser/dom';
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
import { ActionsOrientation, IActionViewItem, prepareActions } from 'vs/base/browser/ui/actionbar/actionbar';
import { IAction, ActionRunner } from 'vs/base/common/actions';
@ -381,7 +381,7 @@ export abstract class EditorTabsControl extends Themable implements IEditorTabsC
// Find target anchor
let anchor: HTMLElement | StandardMouseEvent = node;
if (isMouseEvent(e)) {
anchor = new StandardMouseEvent(e);
anchor = new StandardMouseEvent(getWindow(node), e);
}
// Show it

View file

@ -457,7 +457,7 @@ export class MultiEditorTabsControl extends EditorTabsControl {
// Find target anchor
let anchor: HTMLElement | StandardMouseEvent = tabsContainer;
if (isMouseEvent(e)) {
anchor = new StandardMouseEvent(e);
anchor = new StandardMouseEvent(getWindow(tabsContainer), e);
}
// Show it

View file

@ -227,7 +227,7 @@ abstract class AbstractGlobalActivityActionViewItem extends CompoisteBarActionVi
const disposables = new DisposableStore();
const actions = await this.resolveContextMenuActions(disposables);
const event = new StandardMouseEvent(e);
const event = new StandardMouseEvent(getWindow(this.container), e);
this.contextMenuService.showContextMenu({
getAnchor: () => event,

View file

@ -15,7 +15,7 @@ import { IView } from 'vs/base/browser/ui/grid/grid';
import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService';
import { CompositePart, ICompositeTitleLabel } from 'vs/workbench/browser/parts/compositePart';
import { IPaneCompositeBarOptions, PaneCompositeBar } from 'vs/workbench/browser/parts/paneCompositeBar';
import { Dimension, EventHelper, trackFocus, $, addDisposableListener, EventType, prepend } from 'vs/base/browser/dom';
import { Dimension, EventHelper, trackFocus, $, addDisposableListener, EventType, prepend, getWindow } from 'vs/base/browser/dom';
import { Registry } from 'vs/platform/registry/common/platform';
import { INotificationService } from 'vs/platform/notification/common/notification';
import { IStorageService } from 'vs/platform/storage/common/storage';
@ -291,11 +291,11 @@ export abstract class AbstractPaneCompositePart extends CompositePart<PaneCompos
const titleArea = super.createTitleArea(parent);
this._register(addDisposableListener(titleArea, EventType.CONTEXT_MENU, e => {
this.onTitleAreaContextMenu(new StandardMouseEvent(e));
this.onTitleAreaContextMenu(new StandardMouseEvent(getWindow(titleArea), e));
}));
this._register(Gesture.addTarget(titleArea));
this._register(addDisposableListener(titleArea, GestureEventType.Contextmenu, e => {
this.onTitleAreaContextMenu(new StandardMouseEvent(e));
this.onTitleAreaContextMenu(new StandardMouseEvent(getWindow(titleArea), e));
}));
const globalTitleActionsContainer = titleArea.appendChild($('.global-actions'));

View file

@ -16,7 +16,7 @@ import { IThemeService } from 'vs/platform/theme/common/themeService';
import { STATUS_BAR_BACKGROUND, STATUS_BAR_FOREGROUND, STATUS_BAR_NO_FOLDER_BACKGROUND, STATUS_BAR_ITEM_HOVER_BACKGROUND, STATUS_BAR_BORDER, STATUS_BAR_NO_FOLDER_FOREGROUND, STATUS_BAR_NO_FOLDER_BORDER, STATUS_BAR_ITEM_COMPACT_HOVER_BACKGROUND, STATUS_BAR_ITEM_FOCUS_BORDER, STATUS_BAR_FOCUS_BORDER } from 'vs/workbench/common/theme';
import { IWorkspaceContextService, WorkbenchState } from 'vs/platform/workspace/common/workspace';
import { contrastBorder, activeContrastBorder } from 'vs/platform/theme/common/colorRegistry';
import { EventHelper, createStyleSheet, addDisposableListener, EventType, clearNode } from 'vs/base/browser/dom';
import { EventHelper, createStyleSheet, addDisposableListener, EventType, clearNode, getWindow } from 'vs/base/browser/dom';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { Parts, IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions';
@ -451,7 +451,7 @@ export class StatusbarPart extends Part implements IStatusbarService {
private showContextMenu(e: MouseEvent | GestureEvent): void {
EventHelper.stop(e, true);
const event = new StandardMouseEvent(e);
const event = new StandardMouseEvent(getWindow(this.element), e);
let actions: IAction[] | undefined = undefined;
this.contextMenuService.showContextMenu({

View file

@ -19,7 +19,7 @@ import { ThemeIcon } from 'vs/base/common/themables';
import { TITLE_BAR_ACTIVE_BACKGROUND, TITLE_BAR_ACTIVE_FOREGROUND, TITLE_BAR_INACTIVE_FOREGROUND, TITLE_BAR_INACTIVE_BACKGROUND, TITLE_BAR_BORDER, WORKBENCH_BACKGROUND } from 'vs/workbench/common/theme';
import { isMacintosh, isWindows, isLinux, isWeb, isNative, platformLocale } from 'vs/base/common/platform';
import { Color } from 'vs/base/common/color';
import { EventType, EventHelper, Dimension, append, $, addDisposableListener, prepend, reset } from 'vs/base/browser/dom';
import { EventType, EventHelper, Dimension, append, $, addDisposableListener, prepend, reset, getWindow } from 'vs/base/browser/dom';
import { CustomMenubarControl } from 'vs/workbench/browser/parts/titlebar/menubarControl';
import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiation/common/instantiation';
import { Emitter, Event } from 'vs/base/common/event';
@ -533,7 +533,7 @@ export class TitlebarPart extends Part implements ITitleService {
protected onContextMenu(e: MouseEvent, menuId: MenuId): void {
// Find target anchor
const event = new StandardMouseEvent(e);
const event = new StandardMouseEvent(getWindow(this.rootContainer), e);
// Show it
this.contextMenuService.showContextMenu({

View file

@ -3,7 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { addDisposableListener, Dimension, DragAndDropObserver, EventType, focusWindow, isAncestor } from 'vs/base/browser/dom';
import { addDisposableListener, Dimension, DragAndDropObserver, EventType, focusWindow, getWindow, isAncestor } from 'vs/base/browser/dom';
import { StandardMouseEvent } from 'vs/base/browser/mouseEvent';
import { EventType as TouchEventType, Gesture } from 'vs/base/browser/touch';
import { IActionViewItem } from 'vs/base/browser/ui/actionbar/actionbar';
@ -409,9 +409,9 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer {
this._register(this.paneview.onDidDrop(({ from, to }) => this.movePane(from as ViewPane, to as ViewPane)));
this._register(this.paneview.onDidScroll(_ => this.onDidScrollPane()));
this._register(this.paneview.onDidSashReset((index) => this.onDidSashReset(index)));
this._register(addDisposableListener(parent, EventType.CONTEXT_MENU, (e: MouseEvent) => this.showContextMenu(new StandardMouseEvent(e))));
this._register(addDisposableListener(parent, EventType.CONTEXT_MENU, (e: MouseEvent) => this.showContextMenu(new StandardMouseEvent(getWindow(parent), e))));
this._register(Gesture.addTarget(parent));
this._register(addDisposableListener(parent, TouchEventType.Contextmenu, (e: MouseEvent) => this.showContextMenu(new StandardMouseEvent(e))));
this._register(addDisposableListener(parent, TouchEventType.Contextmenu, (e: MouseEvent) => this.showContextMenu(new StandardMouseEvent(getWindow(parent), e))));
this._menuActions = this._register(this.instantiationService.createInstance(ViewContainerMenuActions, this.paneview.element, this.viewContainer));
this._register(this._menuActions.onDidChange(() => this.updateTitleArea()));
@ -788,7 +788,7 @@ export class ViewPaneContainer extends Component implements IViewPaneContainer {
const contextMenuDisposable = addDisposableListener(pane.draggableElement, 'contextmenu', e => {
e.stopPropagation();
e.preventDefault();
this.onContextMenu(new StandardMouseEvent(e), pane);
this.onContextMenu(new StandardMouseEvent(getWindow(pane.draggableElement), e), pane);
});
const collapseDisposable = Event.latch(Event.map(pane.onDidChange, () => !pane.isExpanded()))(collapsed => {

View file

@ -28,7 +28,7 @@ import { CHAT_CATEGORY } from 'vs/workbench/contrib/chat/browser/actions/chatAct
import { IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat';
import { ICodeBlockActionContext } from 'vs/workbench/contrib/chat/browser/codeBlockPart';
import { CONTEXT_IN_CHAT_SESSION, CONTEXT_PROVIDER_EXISTS } from 'vs/workbench/contrib/chat/common/chatContextKeys';
import { IChatCopyAction, IChatService, IDocumentContext, InteractiveSessionCopyKind } from 'vs/workbench/contrib/chat/common/chatService';
import { IChatService, IDocumentContext, InteractiveSessionCopyKind } from 'vs/workbench/contrib/chat/common/chatService';
import { IChatResponseViewModel, isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel';
import { CTX_INLINE_CHAT_VISIBLE } from 'vs/workbench/contrib/inlineChat/common/inlineChat';
import { insertCell } from 'vs/workbench/contrib/notebook/browser/controller/cellOperations';
@ -111,9 +111,8 @@ export function registerChatCodeBlockActions() {
agentId: context.element.agent?.id,
sessionId: context.element.sessionId,
requestId: context.element.requestId,
action: <IChatCopyAction>{
action: {
kind: 'copy',
responseId: context.element.providerResponseId,
codeBlockIndex: context.codeBlockIndex,
copyType: InteractiveSessionCopyKind.Toolbar,
copiedCharacters: context.code.length,
@ -149,24 +148,21 @@ export function registerChatCodeBlockActions() {
const totalCharacters = editorModel.getValueLength();
// Report copy to extensions
if (context.element.providerResponseId) {
const chatService = accessor.get(IChatService);
chatService.notifyUserAction({
providerId: context.element.providerId,
agentId: context.element.agent?.id,
sessionId: context.element.sessionId,
requestId: context.element.requestId,
action: {
kind: 'copy',
codeBlockIndex: context.codeBlockIndex,
responseId: context.element.providerResponseId,
copyType: InteractiveSessionCopyKind.Action,
copiedText,
copiedCharacters: copiedText.length,
totalCharacters,
}
});
}
const chatService = accessor.get(IChatService);
chatService.notifyUserAction({
providerId: context.element.providerId,
agentId: context.element.agent?.id,
sessionId: context.element.sessionId,
requestId: context.element.requestId,
action: {
kind: 'copy',
codeBlockIndex: context.codeBlockIndex,
copyType: InteractiveSessionCopyKind.Action,
copiedText,
copiedCharacters: copiedText.length,
totalCharacters,
}
});
// Copy full cell if no selection, otherwise fall back on normal editor implementation
if (noSelection) {
@ -337,7 +333,6 @@ export function registerChatCodeBlockActions() {
requestId: context.element.requestId,
action: {
kind: 'insert',
responseId: context.element.providerResponseId!,
codeBlockIndex: context.codeBlockIndex,
totalCharacters: context.code.length,
}
@ -386,7 +381,6 @@ export function registerChatCodeBlockActions() {
requestId: context.element.requestId,
action: {
kind: 'insert',
responseId: context.element.providerResponseId!,
codeBlockIndex: context.codeBlockIndex,
totalCharacters: context.code.length,
newFile: true
@ -482,7 +476,6 @@ export function registerChatCodeBlockActions() {
requestId: context.element.requestId,
action: {
kind: 'runInTerminal',
responseId: context.element.providerResponseId!,
codeBlockIndex: context.codeBlockIndex,
languageId: context.languageId,
}

View file

@ -8,19 +8,15 @@ import { ServicesAccessor } from 'vs/editor/browser/editorExtensions';
import { localize } from 'vs/nls';
import { Action2, MenuId, registerAction2 } from 'vs/platform/actions/common/actions';
import { CHAT_CATEGORY } from 'vs/workbench/contrib/chat/browser/actions/chatActions';
import { IChatWidget } from 'vs/workbench/contrib/chat/browser/chat';
import { IChatWidget, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat';
import { CONTEXT_CHAT_INPUT_HAS_TEXT, CONTEXT_CHAT_REQUEST_IN_PROGRESS } from 'vs/workbench/contrib/chat/common/chatContextKeys';
import { IChatService } from 'vs/workbench/contrib/chat/common/chatService';
export interface IChatExecuteActionContext {
widget: IChatWidget;
widget?: IChatWidget;
inputValue?: string;
}
export function isExecuteActionContext(thing: unknown): thing is IChatExecuteActionContext {
return typeof thing === 'object' && thing !== null && 'widget' in thing;
}
export class SubmitAction extends Action2 {
static readonly ID = 'workbench.action.chat.submit';
@ -44,12 +40,11 @@ export class SubmitAction extends Action2 {
}
run(accessor: ServicesAccessor, ...args: any[]) {
const context = args[0];
if (!isExecuteActionContext(context)) {
return;
}
const context: IChatExecuteActionContext = args[0];
context.widget.acceptInput(context.inputValue);
const widgetService = accessor.get(IChatWidgetService);
const widget = context.widget ?? widgetService.lastFocusedWidget;
widget?.acceptInput(context.inputValue);
}
}
@ -76,8 +71,8 @@ export function registerChatExecuteActions() {
}
run(accessor: ServicesAccessor, ...args: any[]) {
const context = args[0];
if (!isExecuteActionContext(context)) {
const context: IChatExecuteActionContext = args[0];
if (!context.widget) {
return;
}

View file

@ -60,7 +60,6 @@ export function registerChatTitleActions() {
action: {
kind: 'vote',
direction: InteractiveSessionVoteDirection.Up,
responseId: item.providerResponseId!,
}
});
item.setVote(InteractiveSessionVoteDirection.Up);
@ -103,7 +102,6 @@ export function registerChatTitleActions() {
action: {
kind: 'vote',
direction: InteractiveSessionVoteDirection.Down,
responseId: item.providerResponseId!,
}
});
item.setVote(InteractiveSessionVoteDirection.Down);
@ -222,12 +220,12 @@ export function registerChatTitleActions() {
item = widget?.getFocus();
}
const providerRequestId = isRequestVM(item) ? item.providerRequestId :
isResponseVM(item) ? item.providerResponseId : undefined;
const requestId = isRequestVM(item) ? item.id :
isResponseVM(item) ? item.requestId : undefined;
if (providerRequestId) {
if (requestId) {
const chatService = accessor.get(IChatService);
chatService.removeRequest(item.sessionId, providerRequestId);
chatService.removeRequest(item.sessionId, requestId);
}
}
});

View file

@ -18,7 +18,7 @@ import { EditorExtensions, IEditorFactoryRegistry } from 'vs/workbench/common/ed
import { registerChatActions } from 'vs/workbench/contrib/chat/browser/actions/chatActions';
import { registerChatCodeBlockActions } from 'vs/workbench/contrib/chat/browser/actions/chatCodeblockActions';
import { registerChatCopyActions } from 'vs/workbench/contrib/chat/browser/actions/chatCopyActions';
import { registerChatExecuteActions } from 'vs/workbench/contrib/chat/browser/actions/chatExecuteActions';
import { IChatExecuteActionContext, SubmitAction, registerChatExecuteActions } from 'vs/workbench/contrib/chat/browser/actions/chatExecuteActions';
import { registerQuickChatActions } from 'vs/workbench/contrib/chat/browser/actions/chatQuickInputActions';
import { registerChatTitleActions } from 'vs/workbench/contrib/chat/browser/actions/chatTitleActions';
import { registerChatExportActions } from 'vs/workbench/contrib/chat/browser/actions/chatImportExport';
@ -45,7 +45,7 @@ import { ChatAccessibilityService } from 'vs/workbench/contrib/chat/browser/chat
import { ICodeEditorService } from 'vs/editor/browser/services/codeEditorService';
import { AccessibilityVerbositySettingId, AccessibleViewProviderId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration';
import { ChatWelcomeMessageModel } from 'vs/workbench/contrib/chat/common/chatModel';
import { IMarkdownString } from 'vs/base/common/htmlContent';
import { IMarkdownString, MarkdownString } from 'vs/base/common/htmlContent';
import { ChatProviderService, IChatProviderService } from 'vs/workbench/contrib/chat/common/chatProvider';
import { ChatSlashCommandService, IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands';
import { alertFocusChange } from 'vs/workbench/contrib/accessibility/browser/accessibilityContributions';
@ -56,6 +56,8 @@ import { registerChatFileTreeActions } from 'vs/workbench/contrib/chat/browser/a
import { QuickChatService } from 'vs/workbench/contrib/chat/browser/chatQuick';
import { ChatAgentService, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
import { ChatVariablesService } from 'vs/workbench/contrib/chat/browser/chatVariables';
import { chatAgentLeader, chatSubcommandLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes';
import { CancellationToken } from 'vs/base/common/cancellation';
// Register configuration
const configurationRegistry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Configuration);
@ -221,16 +223,52 @@ class ChatSlashStaticSlashCommandsContribution extends Disposable {
constructor(
@IChatSlashCommandService slashCommandService: IChatSlashCommandService,
@ICommandService commandService: ICommandService,
@IChatAgentService chatAgentService: IChatAgentService,
) {
super();
this._store.add(slashCommandService.registerSlashCommand({
command: 'clear',
detail: nls.localize('clear', "Clear the session"),
sortText: 'z_clear',
sortText: 'z2_clear',
executeImmediately: true
}, async () => {
commandService.executeCommand(ACTION_ID_CLEAR_CHAT);
}));
this._store.add(slashCommandService.registerSlashCommand({
command: 'help',
detail: '',
sortText: 'z1_help',
executeImmediately: true
}, async (prompt, progress) => {
const defaultAgent = chatAgentService.getDefaultAgent();
const agents = chatAgentService.getAgents();
if (defaultAgent?.metadata.helpTextPrefix) {
progress.report({ content: defaultAgent.metadata.helpTextPrefix });
progress.report({ content: '\n\n' });
}
const agentText = (await Promise.all(agents
.filter(a => a.id !== defaultAgent?.id)
.map(async a => {
const agentWithLeader = `${chatAgentLeader}${a.id}`;
const actionArg: IChatExecuteActionContext = { inputValue: `${agentWithLeader} ${a.metadata.sampleRequest}` };
const urlSafeArg = encodeURIComponent(JSON.stringify(actionArg));
const agentLine = `* [\`${agentWithLeader}\`](command:${SubmitAction.ID}?${urlSafeArg}) - ${a.metadata.description}`;
const commands = await a.provideSlashCommands(CancellationToken.None);
const commandText = commands.map(c => {
const actionArg: IChatExecuteActionContext = { inputValue: `${agentWithLeader} ${chatSubcommandLeader}${c.name} ${c.sampleRequest ?? ''}` };
const urlSafeArg = encodeURIComponent(JSON.stringify(actionArg));
return `\t* [\`${chatSubcommandLeader}${c.name}\`](command:${SubmitAction.ID}?${urlSafeArg}) - ${c.description}`;
}).join('\n');
return agentLine + '\n' + commandText;
}))).join('\n');
progress.report({ content: new MarkdownString(agentText, { isTrusted: { enabledCommands: [SubmitAction.ID] } }) });
if (defaultAgent?.metadata.helpTextPostfix) {
progress.report({ content: '\n\n' });
progress.report({ content: defaultAgent.metadata.helpTextPostfix });
}
}));
}
}

View file

@ -3,14 +3,13 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { ISlashCommand } from 'vs/workbench/contrib/chat/common/chatService';
import { IChatRequestViewModel, IChatResponseViewModel, IChatViewModel, IChatWelcomeMessageViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel';
import { Event } from 'vs/base/common/event';
import { URI } from 'vs/base/common/uri';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { Selection } from 'vs/editor/common/core/selection';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IChatWidgetContrib } from 'vs/workbench/contrib/chat/browser/chatWidget';
import { Selection } from 'vs/editor/common/core/selection';
import { IChatRequestViewModel, IChatResponseViewModel, IChatViewModel, IChatWelcomeMessageViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel';
export const IChatWidgetService = createDecorator<IChatWidgetService>('chatWidgetService');
export const IQuickChatService = createDecorator<IQuickChatService>('quickChatService');
@ -121,7 +120,6 @@ export interface IChatWidget {
focusLastMessage(): void;
focusInput(): void;
hasInputFocus(): boolean;
getSlashCommands(): Promise<ISlashCommand[] | undefined>;
getCodeBlockInfoForEditor(uri: URI): IChatCodeBlockInfo | undefined;
getCodeBlockInfosForResponse(response: IChatResponseViewModel): IChatCodeBlockInfo[];
getFileTreeInfosForResponse(response: IChatResponseViewModel): IChatFileTreeInfo[];

View file

@ -54,10 +54,10 @@ import { convertParsedRequestToMarkdown, reduceInlineContentReferences, walkTree
import { ChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatOptions';
import { CodeBlockPart, ICodeBlockData, ICodeBlockPart } from 'vs/workbench/contrib/chat/browser/codeBlockPart';
import { IChatAgentMetadata } from 'vs/workbench/contrib/chat/common/chatAgents';
import { CONTEXT_REQUEST, CONTEXT_RESPONSE, CONTEXT_RESPONSE_FILTERED, CONTEXT_RESPONSE_HAS_PROVIDER_ID, CONTEXT_RESPONSE_VOTE } from 'vs/workbench/contrib/chat/common/chatContextKeys';
import { CONTEXT_REQUEST, CONTEXT_RESPONSE, CONTEXT_RESPONSE_FILTERED, CONTEXT_RESPONSE_VOTE } from 'vs/workbench/contrib/chat/common/chatContextKeys';
import { IPlaceholderMarkdownString } from 'vs/workbench/contrib/chat/common/chatModel';
import { chatAgentLeader, chatSubcommandLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes';
import { IChatContentReference, IChatReplyFollowup, IChatResponseProgressFileTreeData, IChatService, ISlashCommand, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService';
import { IChatContentReference, IChatReplyFollowup, IChatResponseProgressFileTreeData, IChatService, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService';
import { IChatResponseMarkdownRenderData, IChatResponseRenderData, IChatResponseViewModel, IChatWelcomeMessageViewModel, isRequestVM, isResponseVM, isWelcomeVM } from 'vs/workbench/contrib/chat/common/chatViewModel';
import { IWordCountResult, getNWords } from 'vs/workbench/contrib/chat/common/chatWordCounter';
import { createFileIconThemableTreeContainerScope } from 'vs/workbench/contrib/files/browser/views/explorerView';
@ -89,7 +89,6 @@ const forceVerboseLayoutTracing = false;
export interface IChatRendererDelegate {
getListLength(): number;
getSlashCommands(): ISlashCommand[];
}
export interface IChatListItemRendererOptions {
@ -264,7 +263,6 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
CONTEXT_RESPONSE.bindTo(templateData.contextKeyService).set(isResponseVM(element));
CONTEXT_REQUEST.bindTo(templateData.contextKeyService).set(isRequestVM(element));
CONTEXT_RESPONSE_HAS_PROVIDER_ID.bindTo(templateData.contextKeyService).set(isResponseVM(element) && !!element.providerResponseId);
if (isResponseVM(element)) {
CONTEXT_RESPONSE_VOTE.bindTo(templateData.contextKeyService).set(element.vote === InteractiveSessionVoteDirection.Up ? 'up' : element.vote === InteractiveSessionVoteDirection.Down ? 'down' : '');
} else {
@ -465,7 +463,6 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
private renderWelcomeMessage(element: IChatWelcomeMessageViewModel, templateData: IChatListItemTemplate, height?: number) {
dom.clearNode(templateData.value);
dom.clearNode(templateData.referencesListContainer);
const slashCommands = this.delegate.getSlashCommands();
for (const item of element.content) {
if (Array.isArray(item)) {
@ -477,11 +474,6 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
templateData.contextKeyService));
} else {
const result = this.renderMarkdown(item as IMarkdownString, element, templateData);
for (const codeElement of result.element.querySelectorAll('code')) {
if (codeElement.textContent && slashCommands.find(command => codeElement.textContent === `/${command.command}`)) {
codeElement.classList.add('interactive-slash-command');
}
}
templateData.value.appendChild(result.element);
templateData.elementDisposables.add(result);
}
@ -779,11 +771,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
const disposables = new DisposableStore();
let codeBlockIndex = 0;
// TODO if the slash commands stay completely dynamic, this isn't quite right
const slashCommands = this.delegate.getSlashCommands();
const usedSlashCommand = slashCommands.find(s => markdown.value.startsWith(`/${s.command} `));
const toRender = usedSlashCommand ? markdown.value.slice(usedSlashCommand.command.length + 2) : markdown.value;
markdown = new MarkdownString(toRender, {
markdown = new MarkdownString(markdown.value, {
isTrusted: {
// Disable all other config options except isTrusted
enabledCommands: typeof markdown.isTrusted === 'object' ? markdown.isTrusted?.enabledCommands : [] ?? []
@ -831,15 +819,6 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
walkTreeAndAnnotateReferenceLinks(result.element);
if (usedSlashCommand) {
const slashCommandElement = $('span.interactive-slash-command', { title: usedSlashCommand.detail }, `/${usedSlashCommand.command} `);
if (result.element.firstChild?.nodeName.toLowerCase() === 'p') {
result.element.firstChild.insertBefore(slashCommandElement, result.element.firstChild.firstChild);
} else {
result.element.insertBefore($('p', undefined, slashCommandElement), result.element.firstChild);
}
}
orderedDisposablesList.reverse().forEach(d => disposables.add(d));
return {
element: result.element,

View file

@ -6,7 +6,6 @@
import * as dom from 'vs/base/browser/dom';
import { ITreeContextMenuEvent, ITreeElement } from 'vs/base/browser/ui/tree/tree';
import { disposableTimeout } from 'vs/base/common/async';
import { CancellationToken } from 'vs/base/common/cancellation';
import { toErrorMessage } from 'vs/base/common/errorMessage';
import { Emitter } from 'vs/base/common/event';
import { Disposable, DisposableStore, IDisposable, MutableDisposable, combinedDisposable, toDisposable } from 'vs/base/common/lifecycle';
@ -23,7 +22,7 @@ import { ServiceCollection } from 'vs/platform/instantiation/common/serviceColle
import { WorkbenchObjectTree } from 'vs/platform/list/browser/listService';
import { ILogService } from 'vs/platform/log/common/log';
import { IViewsService } from 'vs/workbench/common/views';
import { ChatTreeItem, IChatWidgetViewOptions, IChatAccessibilityService, IChatCodeBlockInfo, IChatFileTreeInfo, IChatWidget, IChatWidgetService, IChatWidgetViewContext } from 'vs/workbench/contrib/chat/browser/chat';
import { ChatTreeItem, IChatAccessibilityService, IChatCodeBlockInfo, IChatFileTreeInfo, IChatWidget, IChatWidgetService, IChatWidgetViewContext, IChatWidgetViewOptions } from 'vs/workbench/contrib/chat/browser/chat';
import { ChatInputPart } from 'vs/workbench/contrib/chat/browser/chatInputPart';
import { ChatAccessibilityProvider, ChatListDelegate, ChatListItemRenderer, IChatListItemRendererOptions, IChatRendererDelegate } from 'vs/workbench/contrib/chat/browser/chatListRenderer';
import { ChatEditorOptions } from 'vs/workbench/contrib/chat/browser/chatOptions';
@ -31,7 +30,7 @@ import { ChatViewPane } from 'vs/workbench/contrib/chat/browser/chatViewPane';
import { CONTEXT_CHAT_REQUEST_IN_PROGRESS, CONTEXT_IN_CHAT_SESSION } from 'vs/workbench/contrib/chat/common/chatContextKeys';
import { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService';
import { ChatModelInitState, IChatModel } from 'vs/workbench/contrib/chat/common/chatModel';
import { IChatReplyFollowup, IChatService, ISlashCommand } from 'vs/workbench/contrib/chat/common/chatService';
import { IChatReplyFollowup, IChatService } from 'vs/workbench/contrib/chat/common/chatService';
import { ChatViewModel, IChatResponseViewModel, isRequestVM, isResponseVM, isWelcomeVM } from 'vs/workbench/contrib/chat/common/chatViewModel';
const $ = dom.$;
@ -109,15 +108,6 @@ export class ChatWidget extends Disposable implements IChatWidget {
this.viewModelDisposables.add(viewModel);
}
this.slashCommandsPromise = undefined;
this.lastSlashCommands = undefined;
this.getSlashCommands().then(() => {
if (!this._isDisposed) {
this.onDidChangeItems();
}
});
this._onDidChangeViewModel.fire();
}
@ -125,9 +115,6 @@ export class ChatWidget extends Disposable implements IChatWidget {
return this._viewModel;
}
private lastSlashCommands: ISlashCommand[] | undefined;
private slashCommandsPromise: Promise<ISlashCommand[] | undefined> | undefined;
constructor(
readonly viewContext: IChatWidgetViewContext,
private readonly viewOptions: IChatWidgetViewOptions,
@ -164,12 +151,6 @@ export class ChatWidget extends Disposable implements IChatWidget {
return this.inputPart.inputUri;
}
private _isDisposed: boolean = false;
public override dispose(): void {
this._isDisposed = true;
super.dispose();
}
render(parent: HTMLElement): void {
const viewId = 'viewId' in this.viewContext ? this.viewContext.viewId : undefined;
this.editorOptions = this._register(this.instantiationService.createInstance(ChatEditorOptions, viewId, this.styles.listForeground, this.styles.inputEditorBackground, this.styles.resultEditorBackground));
@ -260,7 +241,7 @@ export class ChatWidget extends Disposable implements IChatWidget {
// TODO? We can give the welcome message a proper VM or get rid of the rest of the VMs
((isWelcomeVM(element) && this.viewModel) ? `_${ChatModelInitState[this.viewModel.initState]}` : '') +
// Ensure re-rendering an element once slash commands are loaded, so the colorization can be applied.
`${(isRequestVM(element) || isWelcomeVM(element)) && !!this.lastSlashCommands ? '_scLoaded' : ''}` +
`${(isRequestVM(element) || isWelcomeVM(element)) /* && !!this.lastSlashCommands ? '_scLoaded' : '' */}` +
// If a response is in the process of progressive rendering, we need to ensure that it will
// be re-rendered so progressive rendering is restarted, even if the model wasn't updated.
`${isResponseVM(element) && element.renderData ? `_${this.visibleChangeCount}` : ''}` +
@ -309,27 +290,11 @@ export class ChatWidget extends Disposable implements IChatWidget {
}
}
async getSlashCommands(): Promise<ISlashCommand[] | undefined> {
if (!this.viewModel) {
return;
}
if (!this.slashCommandsPromise) {
this.slashCommandsPromise = this.chatService.getSlashCommands(this.viewModel.sessionId, CancellationToken.None).then(commands => {
this.lastSlashCommands = commands ?? [];
return this.lastSlashCommands;
});
}
return this.slashCommandsPromise;
}
private createList(listContainer: HTMLElement, options: IChatListItemRendererOptions): void {
const scopedInstantiationService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.contextKeyService]));
const delegate = scopedInstantiationService.createInstance(ChatListDelegate);
const rendererDelegate: IChatRendererDelegate = {
getListLength: () => this.tree.getNode(null).visibleChildrenCount,
getSlashCommands: () => this.lastSlashCommands ?? [],
};
this.renderer = this._register(scopedInstantiationService.createInstance(
ChatListItemRenderer,
@ -462,7 +427,6 @@ export class ChatWidget extends Disposable implements IChatWidget {
this.container.setAttribute('data-session-id', model.sessionId);
this.viewModel = this.instantiationService.createInstance(ChatViewModel, model);
this.viewModelDisposables.add(this.viewModel.onDidChange(e => {
this.slashCommandsPromise = undefined;
this.requestInProgress.set(this.viewModel!.requestInProgress);
this.onDidChangeItems();
if (e?.kind === 'addRequest') {
@ -536,8 +500,7 @@ export class ChatWidget extends Disposable implements IChatWidget {
'query' in opts ? opts.query :
`${opts.prefix} ${editorValue}`;
const isUserQuery = !opts || 'query' in opts;
const usedSlashCommand = this.lookupSlashCommand(input);
const result = await this.chatService.sendRequest(this.viewModel.sessionId, input, usedSlashCommand);
const result = await this.chatService.sendRequest(this.viewModel.sessionId, input);
if (result) {
this.inputPart.acceptInput(isUserQuery ? input : undefined);
@ -552,10 +515,6 @@ export class ChatWidget extends Disposable implements IChatWidget {
}
}
private lookupSlashCommand(input: string): ISlashCommand | undefined {
return this.lastSlashCommands?.find(sc => input.startsWith(`/${sc.command}`));
}
getCodeBlockInfosForResponse(response: IChatResponseViewModel): IChatCodeBlockInfo[] {
return this.renderer.getCodeBlockInfosForResponse(response);
}

View file

@ -32,6 +32,7 @@ import { chatSlashCommandBackground, chatSlashCommandForeground } from 'vs/workb
import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, ChatRequestTextPart, ChatRequestVariablePart, chatAgentLeader, chatSubcommandLeader, chatVariableLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes';
import { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestParser';
import { IChatService, ISlashCommand } from 'vs/workbench/contrib/chat/common/chatService';
import { IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands';
import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables';
import { isResponseVM } from 'vs/workbench/contrib/chat/common/chatViewModel';
import { LifecyclePhase } from 'vs/workbench/services/lifecycle/common/lifecycle';
@ -49,7 +50,7 @@ class InputEditorDecorations extends Disposable {
public readonly id = 'inputEditorDecorations';
private readonly previouslyUsedSlashCommands = new Set<string>();
private readonly previouslyUsedAgents = new Set<string>();
private readonly viewModelDisposables = this._register(new MutableDisposable());
@ -71,16 +72,12 @@ class InputEditorDecorations extends Disposable {
this._register(this.widget.inputEditor.onDidChangeModelContent(() => this.updateInputEditorDecorations()));
this._register(this.widget.onDidChangeViewModel(() => {
this.registerViewModelListeners();
this.previouslyUsedSlashCommands.clear();
this.previouslyUsedAgents.clear();
this.updateInputEditorDecorations();
}));
this._register(this.chatService.onDidSubmitSlashCommand((e) => {
this._register(this.chatService.onDidSubmitAgent((e) => {
if (e.sessionId === this.widget.viewModel?.sessionId) {
if ('agent' in e) {
this.previouslyUsedSlashCommands.add(agentAndCommandToKey(e.agent.id, e.slashCommand.name));
} else {
this.previouslyUsedSlashCommands.add(e.slashCommand);
}
this.previouslyUsedAgents.add(agentAndCommandToKey(e.agent.id, e.slashCommand.name));
}
}));
@ -187,7 +184,7 @@ class InputEditorDecorations extends Disposable {
const onlyAgentCommandAndWhitespace = agentPart && agentSubcommandPart && parsedRequest.every(p => p instanceof ChatRequestTextPart && !p.text.trim().length || p instanceof ChatRequestAgentPart || p instanceof ChatRequestAgentSubcommandPart);
if (onlyAgentCommandAndWhitespace) {
// Agent reference and subcommand with no other text - show the placeholder
const isFollowupSlashCommand = this.previouslyUsedSlashCommands.has(agentAndCommandToKey(agentPart.agent.id, agentSubcommandPart.command.name));
const isFollowupSlashCommand = this.previouslyUsedAgents.has(agentAndCommandToKey(agentPart.agent.id, agentSubcommandPart.command.name));
const shouldRenderFollowupPlaceholder = isFollowupSlashCommand && agentSubcommandPart.command.followupPlaceholder;
if (agentSubcommandPart?.command.description) {
placeholderDecoration = [{
@ -210,7 +207,7 @@ class InputEditorDecorations extends Disposable {
const onlySlashCommandAndWhitespace = slashCommandPart && parsedRequest.every(p => p instanceof ChatRequestTextPart && !p.text.trim().length || p instanceof ChatRequestSlashCommandPart);
if (onlySlashCommandAndWhitespace) {
// Command reference with no other text - show the placeholder
const isFollowupSlashCommand = this.previouslyUsedSlashCommands.has(slashCommandPart.slashCommand.command);
const isFollowupSlashCommand = this.previouslyUsedAgents.has(slashCommandPart.slashCommand.command);
const shouldRenderFollowupPlaceholder = isFollowupSlashCommand && slashCommandPart.slashCommand.followupPlaceholder;
if (shouldRenderFollowupPlaceholder || slashCommandPart.slashCommand.detail) {
placeholderDecoration = [{
@ -264,15 +261,12 @@ class InputEditorSlashCommandMode extends Disposable {
@IChatService private readonly chatService: IChatService
) {
super();
this._register(this.chatService.onDidSubmitSlashCommand(e => {
this._register(this.chatService.onDidSubmitAgent(e => {
if (this.widget.viewModel?.sessionId !== e.sessionId) {
return;
}
if ('agent' in e) {
this.repopulateAgentCommand(e.agent, e.slashCommand);
} else {
this.repopulateSlashCommand(e.slashCommand);
}
this.repopulateAgentCommand(e.agent, e.slashCommand);
}));
}
@ -283,20 +277,6 @@ class InputEditorSlashCommandMode extends Disposable {
this.widget.inputEditor.setPosition({ lineNumber: 1, column: value.length + 1 });
}
}
private async repopulateSlashCommand(slashCommand: string) {
const slashCommands = await this.widget.getSlashCommands();
if (this.widget.inputEditor.getValue().trim().length !== 0) {
return;
}
if (slashCommands?.find(c => c.command === slashCommand)?.shouldRepopulate) {
const value = `/${slashCommand} `;
this.widget.inputEditor.setValue(value);
this.widget.inputEditor.setPosition({ lineNumber: 1, column: value.length + 1 });
}
}
}
ChatWidget.CONTRIBS.push(InputEditorDecorations, InputEditorSlashCommandMode);
@ -306,11 +286,12 @@ class SlashCommandCompletions extends Disposable {
@ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService,
@IChatWidgetService private readonly chatWidgetService: IChatWidgetService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IChatSlashCommandService private readonly chatSlashCommandService: IChatSlashCommandService
) {
super();
this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, {
_debugDisplayName: 'chatSlashCommand',
_debugDisplayName: 'globalSlashCommands',
triggerCharacters: ['/'],
provideCompletionItems: async (model: ITextModel, _position: Position, _context: CompletionContext, _token: CancellationToken) => {
const widget = this.chatWidgetService.getWidgetByInputUri(model.uri);
@ -329,7 +310,7 @@ class SlashCommandCompletions extends Disposable {
return;
}
const slashCommands = await widget.getSlashCommands();
const slashCommands = this.chatSlashCommandService.getCommands();
if (!slashCommands) {
return null;
}

View file

@ -390,10 +390,6 @@
color: var(--vscode-notificationsInfoIcon-foreground) !important; /* Have to override default styles which apply to all lists */
}
.interactive-item-container .value .interactive-slash-command {
color: var(--vscode-textLink-foreground);
}
.interactive-session .interactive-input-part {
padding: 12px 0px;
display: flex;

View file

@ -5,13 +5,14 @@
import { CancellationToken } from 'vs/base/common/cancellation';
import { Emitter, Event } from 'vs/base/common/event';
import { IMarkdownString } from 'vs/base/common/htmlContent';
import { Iterable } from 'vs/base/common/iterator';
import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { ThemeIcon } from 'vs/base/common/themables';
import { URI } from 'vs/base/common/uri';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IChatMessage } from 'vs/workbench/contrib/chat/common/chatProvider';
import { IChatFollowup, IChatProgress, IChatResponseErrorDetails, IChatResponseProgressFileTreeData } from 'vs/workbench/contrib/chat/common/chatService';
import { IChatFollowup, IChatProgress, IChatResponseErrorDetails } from 'vs/workbench/contrib/chat/common/chatService';
import { IChatRequestVariableValue } from 'vs/workbench/contrib/chat/common/chatVariables';
//#region agent service, commands etc
@ -27,40 +28,44 @@ export interface IChatAgent extends IChatAgentData {
provideSlashCommands(token: CancellationToken): Promise<IChatAgentCommand[]>;
}
export interface IChatAgentFragment {
content: string | { treeData: IChatResponseProgressFileTreeData };
}
export interface IChatAgentCommand {
name: string;
description: string;
/**
* Whether the command should execute as soon
* as it is entered. Defaults to `false`.
*/
executeImmediately?: boolean;
/**
* Whether executing the command puts the
* chat into a persistent mode, where the
* slash command is prepended to the chat input.
*/
shouldRepopulate?: boolean;
/**
* Placeholder text to render in the chat input
* when the slash command has been repopulated.
* Has no effect if `shouldRepopulate` is `false`.
*/
followupPlaceholder?: string;
sampleRequest?: string;
}
export interface IChatAgentMetadata {
description?: string;
isDefault?: boolean; // The agent invoked when no agent is specified
helpTextPrefix?: string | IMarkdownString;
helpTextPostfix?: string | IMarkdownString;
isSecondary?: boolean; // Invoked by ctrl/cmd+enter
fullName?: string;
icon?: URI;
iconDark?: URI;
themeIcon?: ThemeIcon;
sampleRequest?: string;
}
export interface IChatAgentRequest {

View file

@ -6,7 +6,6 @@
import { localize } from 'vs/nls';
import { RawContextKey } from 'vs/platform/contextkey/common/contextkey';
export const CONTEXT_RESPONSE_HAS_PROVIDER_ID = new RawContextKey<boolean>('chatSessionResponseHasProviderId', false, { type: 'boolean', description: localize('interactiveSessionResponseHasProviderId', "True when the provider has assigned an id to this response.") });
export const CONTEXT_RESPONSE_VOTE = new RawContextKey<string>('chatSessionResponseVote', '', { type: 'string', description: localize('interactiveSessionResponseVote', "When the response has been voted up, is set to 'up'. When voted down, is set to 'down'. Otherwise an empty string.") });
export const CONTEXT_RESPONSE_FILTERED = new RawContextKey<boolean>('chatSessionResponseFiltered', false, { type: 'boolean', description: localize('chatResponseFiltered', "True when the chat response was filtered out by the server.") });
export const CONTEXT_CHAT_REQUEST_IN_PROGRESS = new RawContextKey<boolean>('chatSessionRequestInProgress', false, { type: 'boolean', description: localize('interactiveSessionRequestInProgress', "True when the current request is still in progress.") });

View file

@ -20,7 +20,6 @@ import { IChat, IChatContentInlineReference, IChatContentReference, IChatFollowu
export interface IChatRequestModel {
readonly id: string;
readonly providerRequestId: string | undefined;
readonly username: string;
readonly avatarIconUri?: URI;
readonly session: IChatModel;
@ -53,7 +52,6 @@ export interface IChatResponseModel {
readonly onDidChange: Event<void>;
readonly id: string;
readonly providerId: string;
readonly providerResponseId: string | undefined;
readonly requestId: string;
readonly username: string;
readonly avatarIconUri?: URI;
@ -79,10 +77,6 @@ export class ChatRequestModel implements IChatRequestModel {
return this._id;
}
public get providerRequestId(): string | undefined {
return this._providerRequestId;
}
public get username(): string {
return this.session.requesterUsername;
}
@ -93,14 +87,9 @@ export class ChatRequestModel implements IChatRequestModel {
constructor(
public readonly session: ChatModel,
public readonly message: IParsedChatRequest,
private _providerRequestId?: string) {
public readonly message: IParsedChatRequest) {
this._id = 'request_' + ChatRequestModel.nextId++;
}
setProviderRequestId(providerRequestId: string) {
this._providerRequestId = providerRequestId;
}
}
export interface IPlaceholderMarkdownString extends IMarkdownString {
@ -169,7 +158,11 @@ export class Response implements IResponse {
} else if (lastResponsePart) {
// Combine this part with the last, non-resolving string part
if (isMarkdownString(responsePart)) {
this._responseParts[responsePartLength] = { string: new MarkdownString(lastResponsePart.string.value + responsePart.value, responsePart) };
// Merge all enabled commands
const lastPartEnabledCommands = typeof lastResponsePart.string.isTrusted === 'object' ? lastResponsePart.string.isTrusted.enabledCommands : [];
const thisPartEnabledCommands = typeof responsePart.isTrusted === 'object' ? responsePart.isTrusted.enabledCommands : [];
const enabledCommands = [...lastPartEnabledCommands, ...thisPartEnabledCommands];
this._responseParts[responsePartLength] = { string: new MarkdownString(lastResponsePart.string.value + responsePart.value, { isTrusted: { enabledCommands } }) };
} else {
this._responseParts[responsePartLength] = { string: new MarkdownString(lastResponsePart.string.value + responsePart, lastResponsePart.string) };
}
@ -252,10 +245,6 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel
return this._id;
}
public get providerResponseId(): string | undefined {
return this._providerResponseId;
}
public get isComplete(): boolean {
return this._isComplete;
}
@ -313,7 +302,6 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel
private _isComplete: boolean = false,
private _isCanceled = false,
private _vote?: InteractiveSessionVoteDirection,
private _providerResponseId?: string,
private _errorDetails?: IChatResponseErrorDetails,
followups?: ReadonlyArray<IChatFollowup>
) {
@ -335,10 +323,6 @@ export class ChatResponseModel extends Disposable implements IChatResponseModel
this._onDidChange.fire();
}
setProviderResponseId(providerResponseId: string) {
this._providerResponseId = providerResponseId;
}
setErrorDetails(errorDetails?: IChatResponseErrorDetails): void {
this._errorDetails = errorDetails;
this._onDidChange.fire();
@ -388,7 +372,6 @@ export interface ISerializableChatsData {
export type ISerializableChatAgentData = UriDto<IChatAgentData>;
export interface ISerializableChatRequestData {
providerRequestId: string | undefined;
message: string | IParsedChatRequest;
response: ReadonlyArray<IMarkdownString | IChatResponseProgressFileTreeData | IChatContentInlineReference> | undefined;
agent?: ISerializableChatAgentData;
@ -410,7 +393,6 @@ export interface IExportableChatData {
responderUsername: string;
requesterAvatarIconUri: UriComponents | undefined;
responderAvatarIconUri: UriComponents | undefined;
providerState: any;
}
export interface ISerializableChatData extends IExportableChatData {
@ -486,11 +468,6 @@ export class ChatModel extends Disposable implements IChatModel {
return this._welcomeMessage;
}
private _providerState: any;
get providerState(): any {
return this._providerState;
}
// TODO to be clear, this is not the same as the id from the session object, which belongs to the provider.
// It's easier to be able to identify this model before its async initialization is complete
private _sessionId: string;
@ -556,7 +533,6 @@ export class ChatModel extends Disposable implements IChatModel {
this._isImported = (!!initialData && !isSerializableSessionData(initialData)) || (initialData?.isImported ?? false);
this._sessionId = (isSerializableSessionData(initialData) && initialData.sessionId) || generateUuid();
this._requests = initialData ? this._deserialize(initialData) : [];
this._providerState = initialData ? initialData.providerState : undefined;
this._creationDate = (isSerializableSessionData(initialData) && initialData.creationDate) || Date.now();
this._initialRequesterAvatarIconUri = initialData?.requesterAvatarIconUri && URI.revive(initialData.requesterAvatarIconUri);
@ -581,11 +557,11 @@ export class ChatModel extends Disposable implements IChatModel {
typeof raw.message === 'string'
? this.getParsedRequestFromString(raw.message)
: reviveParsedChatRequest(raw.message);
const request = new ChatRequestModel(this, parsedRequest, raw.providerRequestId);
const request = new ChatRequestModel(this, parsedRequest);
if (raw.response || raw.responseErrorDetails) {
const agent = (raw.agent && 'metadata' in raw.agent) ? // Check for the new format, ignore entries in the old format
revive<ISerializableChatAgentData>(raw.agent) : undefined;
request.response = new ChatResponseModel(raw.response ?? [new MarkdownString(raw.response)], this, agent, request.id, true, raw.isCanceled, raw.vote, raw.providerRequestId, raw.responseErrorDetails, raw.followups);
request.response = new ChatResponseModel(raw.response ?? [new MarkdownString(raw.response)], this, agent, request.id, true, raw.isCanceled, raw.vote, raw.responseErrorDetails, raw.followups);
if (raw.usedContext) { // @ulugbekna: if this's a new vscode sessions, doc versions are incorrect anyway?
request.response.updateContent(raw.usedContext);
}
@ -638,13 +614,6 @@ export class ChatModel extends Disposable implements IChatModel {
}
this._isInitializedDeferred.complete();
if (session.onDidChangeState) {
this._register(session.onDidChangeState(state => {
this._providerState = state;
this.logService.trace('ChatModel#acceptNewSessionState');
}));
}
this._onDidChange.fire({ kind: 'initialize' });
}
@ -703,21 +672,15 @@ export class ChatModel extends Disposable implements IChatModel {
if (agent) {
request.response.setAgent(agent, progress.command);
}
} else {
request.setProviderRequestId(progress.requestId);
request.response.setProviderResponseId(progress.requestId);
}
}
removeRequest(requestId: string): void {
const index = this._requests.findIndex(request => request.providerRequestId === requestId);
removeRequest(id: string): void {
const index = this._requests.findIndex(request => request.id === id);
const request = this._requests[index];
if (!request.providerRequestId) {
return;
}
if (index !== -1) {
this._onDidChange.fire({ kind: 'removeRequest', requestId: request.providerRequestId, responseId: request.response?.providerResponseId });
this._onDidChange.fire({ kind: 'removeRequest', requestId: request.id, responseId: request.response?.id });
this._requests.splice(index, 1);
request.response?.dispose();
}
@ -778,21 +741,19 @@ export class ChatModel extends Disposable implements IChatModel {
}),
requests: this._requests.map((r): ISerializableChatRequestData => {
return {
providerRequestId: r.providerRequestId,
message: r.message,
response: r.response ? r.response.response.value : undefined,
responseErrorDetails: r.response?.errorDetails,
followups: r.response?.followups,
isCanceled: r.response?.isCanceled,
vote: r.response?.vote,
agent: r.response?.agent,
agent: r.response?.agent ? { id: r.response.agent.id, metadata: r.response.agent.metadata } : undefined, // May actually be the full IChatAgent instance, just take the data props
slashCommand: r.response?.slashCommand,
usedContext: r.response?.response.usedContext,
contentReferences: r.response?.response.contentReferences
};
}),
providerId: this.providerId,
providerState: this._providerState
};
}

View file

@ -9,7 +9,7 @@ import { IPosition, Position } from 'vs/editor/common/core/position';
import { Range } from 'vs/editor/common/core/range';
import { IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestDynamicReferencePart, ChatRequestSlashCommandPart, ChatRequestTextPart, ChatRequestVariablePart, IParsedChatRequest, IParsedChatRequestPart, chatAgentLeader, chatSubcommandLeader, chatVariableLeader } from 'vs/workbench/contrib/chat/common/chatParserTypes';
import { IChatService } from 'vs/workbench/contrib/chat/common/chatService';
import { IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands';
import { IChatVariablesService, IDynamicReference } from 'vs/workbench/contrib/chat/common/chatVariables';
const agentReg = /^@([\w_\-]+)(?=(\s|$|\b))/i; // An @-agent
@ -21,7 +21,7 @@ export class ChatRequestParser {
constructor(
@IChatAgentService private readonly agentService: IChatAgentService,
@IChatVariablesService private readonly variableService: IChatVariablesService,
@IChatService private readonly chatService: IChatService,
@IChatSlashCommandService private readonly slashCommandService: IChatSlashCommandService
) { }
async parseChatRequest(sessionId: string, message: string): Promise<IParsedChatRequest> {
@ -174,7 +174,7 @@ export class ChatRequestParser {
return new ChatRequestAgentSubcommandPart(slashRange, slashEditorRange, subCommand);
}
} else {
const slashCommands = await this.chatService.getSlashCommands(sessionId, CancellationToken.None);
const slashCommands = this.slashCommandService.getCommands();
const slashCommand = slashCommands.find(c => c.command === command);
if (slashCommand) {
// Valid standalone slash command

View file

@ -23,7 +23,6 @@ export interface IChat {
responderUsername: string;
responderAvatarIconUri?: URI;
inputPlaceholder?: string;
onDidChangeState?: Event<any>;
dispose?(): void;
}
@ -100,7 +99,6 @@ export interface IChatAgentDetection {
export type IChatProgress =
| { content: string | IMarkdownString }
| { requestId: string }
| { treeData: IChatResponseProgressFileTreeData }
| { placeholder: string; resolvedContent: Promise<string | IMarkdownString | { treeData: IChatResponseProgressFileTreeData }> }
| IUsedContext
@ -108,18 +106,13 @@ export type IChatProgress =
| IChatContentInlineReference
| IChatAgentDetection;
export interface IPersistedChatState { }
export interface IChatProvider {
readonly id: string;
readonly displayName: string;
readonly iconUrl?: string;
prepareSession(initialState: IPersistedChatState | undefined, token: CancellationToken): ProviderResult<IChat | undefined>;
prepareSession(token: CancellationToken): ProviderResult<IChat | undefined>;
provideWelcomeMessage?(token: CancellationToken): ProviderResult<(string | IMarkdownString | IChatReplyFollowup[])[] | undefined>;
provideSampleQuestions?(token: CancellationToken): ProviderResult<IChatReplyFollowup[] | undefined>;
provideFollowups?(session: IChat, token: CancellationToken): ProviderResult<IChatFollowup[] | undefined>;
provideReply(request: IChatRequest, progress: (progress: IChatProgress) => void, token: CancellationToken): ProviderResult<IChatResponse>;
provideSlashCommands?(session: IChat, token: CancellationToken): ProviderResult<ISlashCommand[]>;
removeRequest?(session: IChat, requestId: string): void;
}
export interface ISlashCommand {
@ -156,7 +149,6 @@ export interface IChatReplyFollowup {
message: string;
title?: string;
tooltip?: string;
metadata?: any;
}
export interface IChatResponseCommandFollowup {
@ -177,7 +169,6 @@ export enum InteractiveSessionVoteDirection {
export interface IChatVoteAction {
kind: 'vote';
responseId: string;
direction: InteractiveSessionVoteDirection;
}
@ -189,7 +180,6 @@ export enum InteractiveSessionCopyKind {
export interface IChatCopyAction {
kind: 'copy';
responseId: string;
codeBlockIndex: number;
copyType: InteractiveSessionCopyKind;
copiedCharacters: number;
@ -199,7 +189,6 @@ export interface IChatCopyAction {
export interface IChatInsertAction {
kind: 'insert';
responseId: string;
codeBlockIndex: number;
totalCharacters: number;
newFile?: boolean;
@ -207,7 +196,6 @@ export interface IChatInsertAction {
export interface IChatTerminalAction {
kind: 'runInTerminal';
responseId: string;
codeBlockIndex: number;
languageId?: string;
}
@ -271,7 +259,7 @@ export interface IChatService {
_serviceBrand: undefined;
transferredSessionData: IChatTransferredSessionData | undefined;
onDidSubmitSlashCommand: Event<{ slashCommand: string; sessionId: string } | { agent: IChatAgentData; slashCommand: IChatAgentCommand; sessionId: string }>;
onDidSubmitAgent: Event<{ agent: IChatAgentData; slashCommand: IChatAgentCommand; sessionId: string }>;
onDidRegisterProvider: Event<{ providerId: string }>;
registerProvider(provider: IChatProvider): IDisposable;
hasSessions(providerId: string): boolean;
@ -285,10 +273,9 @@ export interface IChatService {
/**
* Returns whether the request was accepted.
*/
sendRequest(sessionId: string, message: string, usedSlashCommand?: ISlashCommand): Promise<{ responseCompletePromise: Promise<void> } | undefined>;
sendRequest(sessionId: string, message: string): Promise<{ responseCompletePromise: Promise<void> } | undefined>;
removeRequest(sessionid: string, requestId: string): Promise<void>;
cancelCurrentRequestForSession(sessionId: string): void;
getSlashCommands(sessionId: string, token: CancellationToken): Promise<ISlashCommand[]>;
clearSession(sessionId: string): void;
addCompleteRequest(sessionId: string, message: IParsedChatRequest | string, response: IChatCompleteResponse): void;
sendRequestToProvider(sessionId: string, message: IChatDynamicRequest): void;

View file

@ -8,7 +8,7 @@ import { CancellationToken } from 'vs/base/common/cancellation';
import { Emitter, Event } from 'vs/base/common/event';
import { MarkdownString, isMarkdownString } from 'vs/base/common/htmlContent';
import { Iterable } from 'vs/base/common/iterator';
import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { Disposable, DisposableMap, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { revive } from 'vs/base/common/marshalling';
import { StopWatch } from 'vs/base/common/stopwatch';
import { URI, UriComponents } from 'vs/base/common/uri';
@ -27,8 +27,8 @@ import { ChatModel, ChatModelInitState, ChatRequestModel, ChatWelcomeMessageMode
import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart } from 'vs/workbench/contrib/chat/common/chatParserTypes';
import { ChatMessageRole, IChatMessage } from 'vs/workbench/contrib/chat/common/chatProvider';
import { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestParser';
import { IChat, IChatCompleteResponse, IChatDetail, IChatDynamicRequest, IChatFollowup, IChatProgress, IChatProvider, IChatProviderInfo, IChatRequest, IChatResponse, IChatService, IChatTransferredSessionData, IChatUserActionEvent, ISlashCommand, InteractiveSessionCopyKind, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService';
import { IChatSlashCommandService, IChatSlashFragment } from 'vs/workbench/contrib/chat/common/chatSlashCommands';
import { IChat, IChatCompleteResponse, IChatDetail, IChatDynamicRequest, IChatFollowup, IChatProgress, IChatProvider, IChatProviderInfo, IChatResponse, IChatService, IChatTransferredSessionData, IChatUserActionEvent, InteractiveSessionCopyKind, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService';
import { IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands';
import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
@ -134,7 +134,7 @@ export class ChatService extends Disposable implements IChatService {
private readonly _providers = new Map<string, IChatProvider>();
private readonly _sessionModels = new Map<string, ChatModel>();
private readonly _sessionModels = this._register(new DisposableMap<string, ChatModel>());
private readonly _pendingRequests = new Map<string, CancelablePromise<void>>();
private readonly _persistedSessions: ISerializableChatsData;
private readonly _hasProvider: IContextKey<boolean>;
@ -147,8 +147,8 @@ export class ChatService extends Disposable implements IChatService {
private readonly _onDidPerformUserAction = this._register(new Emitter<IChatUserActionEvent>());
public readonly onDidPerformUserAction: Event<IChatUserActionEvent> = this._onDidPerformUserAction.event;
private readonly _onDidSubmitSlashCommand = this._register(new Emitter<{ slashCommand: string; sessionId: string } | { agent: IChatAgentData; slashCommand: IChatAgentCommand; sessionId: string }>());
public readonly onDidSubmitSlashCommand = this._onDidSubmitSlashCommand.event;
private readonly _onDidSubmitAgent = this._register(new Emitter<{ agent: IChatAgentData; slashCommand: IChatAgentCommand; sessionId: string }>());
public readonly onDidSubmitAgent = this._onDidSubmitAgent.event;
private readonly _onDidDisposeSession = this._register(new Emitter<{ sessionId: string; providerId: string; reason: 'initializationFailed' | 'cleared' }>());
public readonly onDidDisposeSession = this._onDidDisposeSession.event;
@ -365,7 +365,7 @@ export class ChatService extends Disposable implements IChatService {
let session: IChat | undefined;
try {
session = await provider.prepareSession(model.providerState, token) ?? undefined;
session = await provider.prepareSession(token) ?? undefined;
} catch (err) {
this.trace('initializeSession', `Provider initializeSession threw: ${err}`);
}
@ -387,8 +387,7 @@ export class ChatService extends Disposable implements IChatService {
} catch (err) {
this.trace('startSession', `initializeSession failed: ${err}`);
model.setInitializationError(err);
model.dispose();
this._sessionModels.delete(model.sessionId);
this._sessionModels.deleteAndDispose(model.sessionId);
this._onDidDisposeSession.fire({ sessionId: model.sessionId, providerId: model.providerId, reason: 'initializationFailed' });
}
}
@ -424,7 +423,7 @@ export class ChatService extends Disposable implements IChatService {
return this._startSession(data.providerId, data, CancellationToken.None);
}
async sendRequest(sessionId: string, request: string, usedSlashCommand?: ISlashCommand): Promise<{ responseCompletePromise: Promise<void> } | undefined> {
async sendRequest(sessionId: string, request: string): Promise<{ responseCompletePromise: Promise<void> } | undefined> {
this.trace('sendRequest', `sessionId: ${sessionId}, message: ${request.substring(0, 20)}${request.length > 20 ? '[...]' : ''}}`);
if (!request.trim()) {
this.trace('sendRequest', 'Rejected empty message');
@ -448,10 +447,10 @@ export class ChatService extends Disposable implements IChatService {
}
// This method is only returning whether the request was accepted - don't block on the actual request
return { responseCompletePromise: this._sendRequestAsync(model, sessionId, provider, request, usedSlashCommand) };
return { responseCompletePromise: this._sendRequestAsync(model, sessionId, provider, request) };
}
private async _sendRequestAsync(model: ChatModel, sessionId: string, provider: IChatProvider, message: string, usedSlashCommand?: ISlashCommand): Promise<void> {
private async _sendRequestAsync(model: ChatModel, sessionId: string, provider: IChatProvider, message: string): Promise<void> {
const parsedRequest = await this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(sessionId, message);
let request: ChatRequestModel;
@ -486,7 +485,7 @@ export class ChatService extends Disposable implements IChatService {
} else if ('agentName' in progress) {
this.trace('sendRequest', `Provider returned an agent detection for session ${model.sessionId}:\n ${JSON.stringify(progress, null, '\t')}`);
} else {
this.trace('sendRequest', `Provider returned id for session ${model.sessionId}, ${progress.requestId}`);
this.trace('sendRequest', `Provider returned unknown progress for session ${model.sessionId}:\n ${JSON.stringify(progress, null, '\t')}`);
}
model.acceptResponseProgress(request, progress);
@ -503,7 +502,7 @@ export class ChatService extends Disposable implements IChatService {
result: 'cancelled',
requestType,
agent: agentPart?.agent.id ?? '',
slashCommand: agentSlashCommandPart ? agentSlashCommandPart.command.name : usedSlashCommand?.command,
slashCommand: agentSlashCommandPart ? agentSlashCommandPart.command.name : commandPart?.slashCommand.command,
chatSessionId: model.sessionId
});
@ -511,10 +510,8 @@ export class ChatService extends Disposable implements IChatService {
});
try {
if (usedSlashCommand?.command) {
this._onDidSubmitSlashCommand.fire({ slashCommand: usedSlashCommand.command, sessionId: model.sessionId });
} else if (agentPart && agentSlashCommandPart?.command) {
this._onDidSubmitSlashCommand.fire({ agent: agentPart.agent, slashCommand: agentSlashCommandPart.command, sessionId: model.sessionId });
if (agentPart && agentSlashCommandPart?.command) {
this._onDidSubmitAgent.fire({ agent: agentPart.agent, slashCommand: agentSlashCommandPart.command, sessionId: model.sessionId });
}
let rawResponse: IChatResponse | null | undefined;
@ -567,28 +564,14 @@ export class ChatService extends Disposable implements IChatService {
history.push({ role: ChatMessageRole.User, content: request.message.text });
history.push({ role: ChatMessageRole.Assistant, content: request.response.response.asString() });
}
const commandResult = await this.chatSlashCommandService.executeCommand(commandPart.slashCommand.command, message.substring(commandPart.slashCommand.command.length + 1).trimStart(), new Progress<IChatSlashFragment>(p => {
const { content } = p;
const data = isCompleteInteractiveProgressTreeData(content) ? content : { content };
progressCallback(data);
const commandResult = await this.chatSlashCommandService.executeCommand(commandPart.slashCommand.command, message.substring(commandPart.slashCommand.command.length + 1).trimStart(), new Progress<IChatProgress>(p => {
progressCallback(p);
}), history, token);
agentOrCommandFollowups = Promise.resolve(commandResult?.followUp);
rawResponse = { session: model.session! };
} else {
request = model.addRequest(parsedRequest);
const requestProps: IChatRequest = {
session: model.session!,
message,
variables: {}
};
if ('parts' in parsedRequest) {
const varResult = await this.chatVariablesService.resolveVariables(parsedRequest, model, token);
requestProps.variables = varResult.variables;
requestProps.message = varResult.prompt;
}
rawResponse = await provider.provideReply(requestProps, progressCallback, token);
throw new Error(`Can't handle request`);
}
if (token.isCancellationRequested) {
@ -610,7 +593,7 @@ export class ChatService extends Disposable implements IChatService {
result,
requestType,
agent: agentPart?.agent.id ?? '',
slashCommand: agentSlashCommandPart ? agentSlashCommandPart.command.name : usedSlashCommand?.command,
slashCommand: agentSlashCommandPart ? agentSlashCommandPart.command.name : commandPart?.slashCommand.command,
chatSessionId: model.sessionId
});
model.setResponse(request, rawResponse);
@ -622,11 +605,6 @@ export class ChatService extends Disposable implements IChatService {
model.setFollowups(request, followups);
model.completeResponse(request);
});
} else if (provider.provideFollowups) {
Promise.resolve(provider.provideFollowups(model.session!, CancellationToken.None)).then(providerFollowups => {
model.setFollowups(request, providerFollowups ?? undefined);
model.completeResponse(request);
});
} else {
model.completeResponse(request);
}
@ -655,43 +633,6 @@ export class ChatService extends Disposable implements IChatService {
}
model.removeRequest(requestId);
provider.removeRequest?.(model.session!, requestId);
}
async getSlashCommands(sessionId: string, token: CancellationToken): Promise<ISlashCommand[]> {
const model = this._sessionModels.get(sessionId);
if (!model) {
throw new Error(`Unknown session: ${sessionId}`);
}
await model.waitForInitialization();
const provider = this._providers.get(model.providerId);
if (!provider) {
throw new Error(`Unknown provider: ${model.providerId}`);
}
const serviceResults = this.chatSlashCommandService.getCommands().map(data => {
return <ISlashCommand>{
command: data.command,
detail: data.detail,
sortText: data.sortText,
executeImmediately: data.executeImmediately
};
});
const mainProviderRequest = provider.provideSlashCommands?.(model.session!, token);
try {
const providerResults = await mainProviderRequest;
if (providerResults) {
return providerResults.concat(serviceResults);
}
return serviceResults;
} catch (e) {
this.logService.error(e);
return serviceResults;
}
}
async sendRequestToProvider(sessionId: string, message: IChatDynamicRequest): Promise<{ responseCompletePromise: Promise<void> } | undefined> {
@ -748,8 +689,7 @@ export class ChatService extends Disposable implements IChatService {
this._persistedSessions[sessionId] = model.toJSON();
model.dispose();
this._sessionModels.delete(sessionId);
this._sessionModels.deleteAndDispose(sessionId);
this._pendingRequests.get(sessionId)?.cancel();
this._onDidDisposeSession.fire({ sessionId, providerId: model.providerId, reason: 'cleared' });
}

View file

@ -5,11 +5,11 @@
import { CancellationToken } from 'vs/base/common/cancellation';
import { Emitter, Event } from 'vs/base/common/event';
import { Disposable, IDisposable, combinedDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { Disposable, IDisposable, toDisposable } from 'vs/base/common/lifecycle';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { IProgress } from 'vs/platform/progress/common/progress';
import { IChatMessage } from 'vs/workbench/contrib/chat/common/chatProvider';
import { IChatFollowup, IChatResponseProgressFileTreeData } from 'vs/workbench/contrib/chat/common/chatService';
import { IChatFollowup, IChatProgress, IChatResponseProgressFileTreeData } from 'vs/workbench/contrib/chat/common/chatService';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
//#region slash service, commands etc
@ -29,21 +29,18 @@ export interface IChatSlashData {
export interface IChatSlashFragment {
content: string | { treeData: IChatResponseProgressFileTreeData };
}
export type IChatSlashCallback = { (prompt: string, progress: IProgress<IChatSlashFragment>, history: IChatMessage[], token: CancellationToken): Promise<{ followUp: IChatFollowup[] } | void> };
export type IChatSlashCallback = { (prompt: string, progress: IProgress<IChatProgress>, history: IChatMessage[], token: CancellationToken): Promise<{ followUp: IChatFollowup[] } | void> };
export const IChatSlashCommandService = createDecorator<IChatSlashCommandService>('chatSlashCommandService');
/**
* This currently only exists to drive /clear. Delete this when the agent service can handle that scenario
* This currently only exists to drive /clear and /help
*/
export interface IChatSlashCommandService {
_serviceBrand: undefined;
readonly onDidChangeCommands: Event<void>;
registerSlashData(data: IChatSlashData): IDisposable;
registerSlashCallback(id: string, command: IChatSlashCallback): IDisposable;
registerSlashCommand(data: IChatSlashData, command: IChatSlashCallback): IDisposable;
executeCommand(id: string, prompt: string, progress: IProgress<IChatSlashFragment>, history: IChatMessage[], token: CancellationToken): Promise<{ followUp: IChatFollowup[] } | void>;
executeCommand(id: string, prompt: string, progress: IProgress<IChatProgress>, history: IChatMessage[], token: CancellationToken): Promise<{ followUp: IChatFollowup[] } | void>;
getCommands(): Array<IChatSlashData>;
hasCommand(id: string): boolean;
}
@ -68,11 +65,12 @@ export class ChatSlashCommandService extends Disposable implements IChatSlashCom
this._commands.clear();
}
registerSlashData(data: IChatSlashData): IDisposable {
registerSlashCommand(data: IChatSlashData, command: IChatSlashCallback): IDisposable {
if (this._commands.has(data.command)) {
throw new Error(`Already registered a command with id ${data.command}}`);
}
this._commands.set(data.command, { data });
this._commands.set(data.command, { data, command });
this._onDidChangeCommands.fire();
return toDisposable(() => {
@ -82,22 +80,6 @@ export class ChatSlashCommandService extends Disposable implements IChatSlashCom
});
}
registerSlashCallback(id: string, command: IChatSlashCallback): IDisposable {
const data = this._commands.get(id);
if (!data) {
throw new Error(`No command with id ${id} registered`);
}
data.command = command;
return toDisposable(() => data.command = undefined);
}
registerSlashCommand(data: IChatSlashData, command: IChatSlashCallback): IDisposable {
return combinedDisposable(
this.registerSlashData(data),
this.registerSlashCallback(data.command, command)
);
}
getCommands(): Array<IChatSlashData> {
return Array.from(this._commands.values(), v => v.data);
}
@ -106,7 +88,7 @@ export class ChatSlashCommandService extends Disposable implements IChatSlashCom
return this._commands.has(id);
}
async executeCommand(id: string, prompt: string, progress: IProgress<IChatSlashFragment>, history: IChatMessage[], token: CancellationToken): Promise<{ followUp: IChatFollowup[] } | void> {
async executeCommand(id: string, prompt: string, progress: IProgress<IChatProgress>, history: IChatMessage[], token: CancellationToken): Promise<{ followUp: IChatFollowup[] } | void> {
const data = this._commands.get(id);
if (!data) {
throw new Error('No command with id ${id} NOT registered');

View file

@ -55,7 +55,6 @@ export interface IChatViewModel {
export interface IChatRequestViewModel {
readonly id: string;
readonly providerRequestId: string | undefined;
readonly sessionId: string;
/** This ID updates every time the underlying data changes */
readonly dataId: string;
@ -88,7 +87,6 @@ export interface IChatResponseViewModel {
/** This ID updates every time the underlying data changes */
readonly dataId: string;
readonly providerId: string;
readonly providerResponseId: string | undefined;
/** The ID of the associated IChatRequestViewModel */
readonly requestId: string;
readonly username: string;
@ -173,12 +171,12 @@ export class ChatViewModel extends Disposable implements IChatViewModel {
} else if (e.kind === 'addResponse') {
this.onAddResponse(e.response);
} else if (e.kind === 'removeRequest') {
const requestIdx = this._items.findIndex(item => isRequestVM(item) && item.providerRequestId === e.requestId);
const requestIdx = this._items.findIndex(item => isRequestVM(item) && item.id === e.requestId);
if (requestIdx >= 0) {
this._items.splice(requestIdx, 1);
}
const responseIdx = e.responseId && this._items.findIndex(item => isResponseVM(item) && item.providerResponseId === e.responseId);
const responseIdx = e.responseId && this._items.findIndex(item => isResponseVM(item) && item.id === e.responseId);
if (typeof responseIdx === 'number' && responseIdx >= 0) {
const items = this._items.splice(responseIdx, 1);
const item = items[0];
@ -218,10 +216,6 @@ export class ChatRequestViewModel implements IChatRequestViewModel {
return this._model.id;
}
get providerRequestId() {
return this._model.providerRequestId;
}
get dataId() {
return this.id + `_${ChatModelInitState[this._model.session.initState]}`;
}
@ -269,10 +263,6 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi
return this._model.providerId;
}
get providerResponseId() {
return this._model.providerResponseId;
}
get sessionId() {
return this._model.session.sessionId;
}

View file

@ -29,7 +29,6 @@ import { IViewsService } from 'vs/workbench/common/views';
import { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService';
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { KeyCode } from 'vs/base/common/keyCodes';
import { isExecuteActionContext } from 'vs/workbench/contrib/chat/browser/actions/chatExecuteActions';
import { IWorkbenchLayoutService, Parts } from 'vs/workbench/services/layout/browser/layoutService';
import { HasSpeechProvider, ISpeechService, SpeechToTextStatus } from 'vs/workbench/contrib/speech/common/speechService';
import { RunOnceScheduler } from 'vs/base/common/async';
@ -41,6 +40,7 @@ import { contrastBorder, focusBorder } from 'vs/platform/theme/common/colorRegis
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
import { isNumber } from 'vs/base/common/types';
import { AccessibilityVoiceSettingId, SpeechTimeoutDefault } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration';
import { IChatExecuteActionContext } from 'vs/workbench/contrib/chat/browser/actions/chatExecuteActions';
const CONTEXT_VOICE_CHAT_GETTING_READY = new RawContextKey<boolean>('voiceChatGettingReady', false, { type: 'boolean', description: localize('voiceChatGettingReady', "True when getting ready for receiving voice input from the microphone for voice chat.") });
const CONTEXT_VOICE_CHAT_IN_PROGRESS = new RawContextKey<boolean>('voiceChatInProgress', false, { type: 'boolean', description: localize('voiceChatInProgress', "True when voice recording from microphone is in progress for voice chat.") });
@ -486,7 +486,8 @@ export class StartVoiceChatAction extends Action2 {
const instantiationService = accessor.get(IInstantiationService);
const commandService = accessor.get(ICommandService);
if (isExecuteActionContext(context)) {
const widget = (context as IChatExecuteActionContext)?.widget;
if (widget) {
// if we already get a context when the action is executed
// from a toolbar within the chat widget, then make sure
// to move focus into the input field so that the controller
@ -494,7 +495,7 @@ export class StartVoiceChatAction extends Action2 {
// TODO@bpasero this will actually not work if the button
// is clicked from the inline editor while focus is in a
// chat input field in a view or picker
context.widget.focusInput();
widget.focusInput();
}
const controller = await VoiceChatSessionControllerFactory.create(accessor, 'focused');

View file

@ -6,32 +6,53 @@
welcomeMessage: undefined,
requests: [
{
providerRequestId: undefined,
message: {
text: "test request",
text: "@ChatProviderWithUsedContext test request",
parts: [
{
range: {
start: 0,
endExclusive: 12
endExclusive: 28
},
editorRange: {
startLineNumber: 1,
startColumn: 1,
endLineNumber: 1,
endColumn: 13
endColumn: 29
},
text: "test request",
agent: {
id: "ChatProviderWithUsedContext",
metadata: { },
provideSlashCommands: [Function provideSlashCommands],
invoke: [Function invoke]
},
kind: "agent"
},
{
range: {
start: 28,
endExclusive: 41
},
editorRange: {
startLineNumber: 1,
startColumn: 29,
endLineNumber: 1,
endColumn: 42
},
text: " test request",
kind: "text"
}
]
},
response: [ ],
responseErrorDetails: undefined,
followups: undefined,
followups: [ ],
isCanceled: false,
vote: undefined,
agent: undefined,
agent: {
id: "ChatProviderWithUsedContext",
metadata: { }
},
slashCommand: undefined,
usedContext: { documents: [
{
@ -58,6 +79,5 @@
contentReferences: [ ]
}
],
providerId: "ChatProviderWithUsedContext",
providerState: undefined
providerId: "testProvider"
}

View file

@ -5,6 +5,5 @@
responderAvatarIconUri: undefined,
welcomeMessage: undefined,
requests: [ ],
providerId: "ChatProviderWithUsedContext",
providerState: undefined
providerId: "testProvider"
}

View file

@ -6,32 +6,53 @@
welcomeMessage: undefined,
requests: [
{
providerRequestId: undefined,
message: {
parts: [
{
range: {
start: 0,
endExclusive: 12
endExclusive: 28
},
editorRange: {
startLineNumber: 1,
startColumn: 1,
endLineNumber: 1,
endColumn: 13
endColumn: 29
},
text: "test request",
agent: {
id: "ChatProviderWithUsedContext",
metadata: { },
provideSlashCommands: [Function provideSlashCommands],
invoke: [Function invoke]
},
kind: "agent"
},
{
range: {
start: 28,
endExclusive: 41
},
editorRange: {
startLineNumber: 1,
startColumn: 29,
endLineNumber: 1,
endColumn: 42
},
text: " test request",
kind: "text"
}
],
text: "test request"
text: "@ChatProviderWithUsedContext test request"
},
response: [ ],
responseErrorDetails: undefined,
followups: undefined,
followups: [ ],
isCanceled: false,
vote: undefined,
agent: undefined,
agent: {
id: "ChatProviderWithUsedContext",
metadata: { }
},
slashCommand: undefined,
usedContext: { documents: [
{
@ -58,6 +79,5 @@
contentReferences: [ ]
}
],
providerId: "ChatProviderWithUsedContext",
providerState: undefined
providerId: "testProvider"
}

View file

@ -6,11 +6,14 @@
import * as assert from 'assert';
import { timeout } from 'vs/base/common/async';
import { ensureNoDisposablesAreLeakedInTestSuite } from 'vs/base/test/common/utils';
import { Range } from 'vs/editor/common/core/range';
import { OffsetRange } from 'vs/editor/common/core/offsetRange';
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
import { ILogService, NullLogService } from 'vs/platform/log/common/log';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { ChatAgentService, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
import { ChatModel } from 'vs/workbench/contrib/chat/common/chatModel';
import { ChatRequestTextPart } from 'vs/workbench/contrib/chat/common/chatParserTypes';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { TestExtensionService, TestStorageService } from 'vs/workbench/test/common/workbenchTestServices';
@ -98,4 +101,18 @@ suite('ChatModel', () => {
assert.throws(() => model.initialize({} as any, undefined));
});
test('removeRequest', async () => {
const model = testDisposables.add(instantiationService.createInstance(ChatModel, 'provider', undefined));
model.startInitialize();
model.initialize({} as any, undefined);
const text = 'hello';
model.addRequest({ text, parts: [new ChatRequestTextPart(new OffsetRange(0, text.length), new Range(1, text.length, 1, text.length), text)] });
const requests = model.getRequests();
assert.strictEqual(requests.length, 1);
model.removeRequest(requests[0].id);
assert.strictEqual(model.getRequests().length, 0);
});
});

View file

@ -11,7 +11,7 @@ import { ILogService, NullLogService } from 'vs/platform/log/common/log';
import { IStorageService } from 'vs/platform/storage/common/storage';
import { ChatAgentService, IChatAgent, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
import { ChatRequestParser } from 'vs/workbench/contrib/chat/common/chatRequestParser';
import { IChatService } from 'vs/workbench/contrib/chat/common/chatService';
import { IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands';
import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { TestExtensionService, TestStorageService } from 'vs/workbench/test/common/workbenchTestServices';
@ -48,9 +48,9 @@ suite('ChatRequestParser', () => {
});
test('slash command', async () => {
const chatService = mockObject<IChatService>()({});
chatService.getSlashCommands.returns(Promise.resolve([{ command: 'fix' }]));
instantiationService.stub(IChatService, chatService as any);
const slashCommandService = mockObject<IChatSlashCommandService>()({});
slashCommandService.getCommands.returns([{ command: 'fix' }]);
instantiationService.stub(IChatSlashCommandService, slashCommandService as any);
parser = instantiationService.createInstance(ChatRequestParser);
const text = '/fix this';
@ -59,9 +59,9 @@ suite('ChatRequestParser', () => {
});
test('invalid slash command', async () => {
const chatService = mockObject<IChatService>()({});
chatService.getSlashCommands.returns(Promise.resolve([{ command: 'fix' }]));
instantiationService.stub(IChatService, chatService as any);
const slashCommandService = mockObject<IChatSlashCommandService>()({});
slashCommandService.getCommands.returns([{ command: 'fix' }]);
instantiationService.stub(IChatSlashCommandService, slashCommandService as any);
parser = instantiationService.createInstance(ChatRequestParser);
const text = '/explain this';
@ -70,9 +70,9 @@ suite('ChatRequestParser', () => {
});
test('multiple slash commands', async () => {
const chatService = mockObject<IChatService>()({});
chatService.getSlashCommands.returns(Promise.resolve([{ command: 'fix' }]));
instantiationService.stub(IChatService, chatService as any);
const slashCommandService = mockObject<IChatSlashCommandService>()({});
slashCommandService.getCommands.returns([{ command: 'fix' }]);
instantiationService.stub(IChatSlashCommandService, slashCommandService as any);
parser = instantiationService.createInstance(ChatRequestParser);
const text = '/fix /fix';

View file

@ -5,7 +5,6 @@
import * as assert from 'assert';
import { CancellationToken } from 'vs/base/common/cancellation';
import { Emitter } from 'vs/base/common/event';
import { Disposable } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { assertSnapshot } from 'vs/base/test/common/snapshot';
@ -22,10 +21,10 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry';
import { NullTelemetryService } from 'vs/platform/telemetry/common/telemetryUtils';
import { IWorkspaceContextService } from 'vs/platform/workspace/common/workspace';
import { IViewsService } from 'vs/workbench/common/views';
import { ChatAgentService, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
import { ChatAgentService, IChatAgent, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
import { IChatContributionService } from 'vs/workbench/contrib/chat/common/chatContributionService';
import { ISerializableChatData } from 'vs/workbench/contrib/chat/common/chatModel';
import { IChat, IChatProgress, IChatProvider, IChatRequest, IChatResponse, IPersistedChatState, ISlashCommand } from 'vs/workbench/contrib/chat/common/chatService';
import { IChat, IChatProgress, IChatProvider, IChatRequest } from 'vs/workbench/contrib/chat/common/chatService';
import { ChatService } from 'vs/workbench/contrib/chat/common/chatServiceImpl';
import { ChatSlashCommandService, IChatSlashCommandService } from 'vs/workbench/contrib/chat/common/chatSlashCommands';
import { IChatVariablesService } from 'vs/workbench/contrib/chat/common/chatVariables';
@ -36,29 +35,18 @@ import { TestContextService, TestExtensionService, TestStorageService } from 'vs
class SimpleTestProvider extends Disposable implements IChatProvider {
private static sessionId = 0;
lastInitialState = undefined;
readonly displayName = 'Test';
private _onDidChangeState = this._register(new Emitter());
constructor(readonly id: string) {
super();
}
prepareSession(initialState: any) {
this.lastInitialState = initialState;
return Promise.resolve(<IChat>{
async prepareSession(): Promise<IChat> {
return {
id: SimpleTestProvider.sessionId++,
username: 'test',
responderUsername: 'test',
requesterUsername: 'test',
onDidChangeState: this._onDidChangeState.event
});
}
changeState(state: any) {
this._onDidChangeState.fire(state);
};
}
async provideReply(request: IChatRequest, progress: (progress: IChatProgress) => void): Promise<{ session: IChat; followups: never[] }> {
@ -66,10 +54,14 @@ class SimpleTestProvider extends Disposable implements IChatProvider {
}
}
/** Chat provider for testing that returns used context */
class ChatProviderWithUsedContext extends SimpleTestProvider implements IChatProvider {
override provideReply(request: IChatRequest, progress: (progress: IChatProgress) => void): Promise<{ session: IChat; followups: never[] }> {
const chatAgentWithUsedContextId = 'ChatProviderWithUsedContext';
const chatAgentWithUsedContext: IChatAgent = {
id: chatAgentWithUsedContextId,
metadata: {},
async provideSlashCommands(token) {
return [];
},
async invoke(request, progress, history, token) {
progress({
documents: [
{
@ -82,9 +74,9 @@ class ChatProviderWithUsedContext extends SimpleTestProvider implements IChatPro
]
});
return super.provideReply(request, progress);
}
}
return {};
},
};
suite('Chat', () => {
const testDisposables = ensureNoDisposablesAreLeakedInTestSuite();
@ -92,6 +84,8 @@ suite('Chat', () => {
let storageService: IStorageService;
let instantiationService: TestInstantiationService;
let chatAgentService: IChatAgentService;
setup(async () => {
instantiationService = testDisposables.add(new TestInstantiationService(new ServiceCollection(
[IChatVariablesService, new MockChatVariablesService()],
@ -105,7 +99,18 @@ suite('Chat', () => {
instantiationService.stub(IChatContributionService, new TestExtensionService());
instantiationService.stub(IWorkspaceContextService, new TestContextService());
instantiationService.stub(IChatSlashCommandService, testDisposables.add(instantiationService.createInstance(ChatSlashCommandService)));
instantiationService.stub(IChatAgentService, testDisposables.add(instantiationService.createInstance(ChatAgentService)));
chatAgentService = testDisposables.add(instantiationService.createInstance(ChatAgentService));
instantiationService.stub(IChatAgentService, chatAgentService);
const agent = {
id: 'testAgent',
metadata: { isDefault: true },
async invoke(request, progress, history, token) {
return {};
},
} as IChatAgent;
testDisposables.add(chatAgentService.registerAgent(agent));
});
test('retrieveSession', async () => {
@ -123,12 +128,7 @@ suite('Chat', () => {
await session2.waitForInitialization();
session2!.addRequest({ parts: [], text: 'request 2' });
assert.strictEqual(provider1.lastInitialState, undefined);
assert.strictEqual(provider2.lastInitialState, undefined);
provider1.changeState({ state: 'provider1_state' });
provider2.changeState({ state: 'provider2_state' });
storageService.flush();
const testService2 = testDisposables.add(instantiationService.createInstance(ChatService));
testDisposables.add(testService2.registerProvider(provider1));
testDisposables.add(testService2.registerProvider(provider2));
@ -136,8 +136,8 @@ suite('Chat', () => {
await retrieved1!.waitForInitialization();
const retrieved2 = testDisposables.add(testService2.getOrRestoreSession(session2.sessionId)!);
await retrieved2!.waitForInitialization();
assert.deepStrictEqual(provider1.lastInitialState, { state: 'provider1_state' });
assert.deepStrictEqual(provider2.lastInitialState, { state: 'provider2_state' });
assert.deepStrictEqual(retrieved1.getRequests()[0]?.message.text, 'request 1');
assert.deepStrictEqual(retrieved2.getRequests()[0]?.message.text, 'request 2');
});
test('Handles failed session startup', async () => {
@ -172,10 +172,7 @@ suite('Chat', () => {
testDisposables.add(testService.registerProvider({
id,
displayName: 'Test',
prepareSession: function (initialState: IPersistedChatState | undefined, token: CancellationToken): ProviderResult<IChat | undefined> {
throw new Error('Function not implemented.');
},
provideReply: function (request: IChatRequest, progress: (progress: IChatProgress) => void, token: CancellationToken): ProviderResult<IChatResponse> {
prepareSession: function (token: CancellationToken): ProviderResult<IChat | undefined> {
throw new Error('Function not implemented.');
}
}));
@ -184,45 +181,13 @@ suite('Chat', () => {
testDisposables.add(testService.registerProvider({
id,
displayName: 'Test',
prepareSession: function (initialState: IPersistedChatState | undefined, token: CancellationToken): ProviderResult<IChat | undefined> {
throw new Error('Function not implemented.');
},
provideReply: function (request: IChatRequest, progress: (progress: IChatProgress) => void, token: CancellationToken): ProviderResult<IChatResponse> {
prepareSession: function (token: CancellationToken): ProviderResult<IChat | undefined> {
throw new Error('Function not implemented.');
}
}));
}, 'Expected to throw for dupe provider');
});
test('getSlashCommands', async () => {
const testService = testDisposables.add(instantiationService.createInstance(ChatService));
const provider = testDisposables.add(new class extends SimpleTestProvider {
constructor() {
super('testProvider');
}
provideSlashCommands(): ProviderResult<ISlashCommand[]> {
return [
{
command: 'command',
detail: 'detail',
sortText: 'sortText',
}
];
}
});
testDisposables.add(testService.registerProvider(provider));
const model = testDisposables.add(testService.startSession('testProvider', CancellationToken.None));
const commands = await testService.getSlashCommands(model.sessionId, CancellationToken.None);
assert.strictEqual(commands?.length, 1);
assert.strictEqual(commands?.[0].command, 'command');
assert.strictEqual(commands?.[0].detail, 'detail');
assert.strictEqual(commands?.[0].sortText, 'sortText');
});
test('sendRequestToProvider', async () => {
const testService = testDisposables.add(instantiationService.createInstance(ChatService));
testDisposables.add(testService.registerProvider(testDisposables.add(new SimpleTestProvider('testProvider'))));
@ -249,18 +214,16 @@ suite('Chat', () => {
});
test('can serialize', async () => {
testDisposables.add(chatAgentService.registerAgent(chatAgentWithUsedContext));
const testService = testDisposables.add(instantiationService.createInstance(ChatService));
const providerId = 'ChatProviderWithUsedContext';
testDisposables.add(testService.registerProvider(testDisposables.add(new SimpleTestProvider('testProvider'))));
testDisposables.add(testService.registerProvider(testDisposables.add(new ChatProviderWithUsedContext(providerId))));
const model = testDisposables.add(testService.startSession(providerId, CancellationToken.None));
const model = testDisposables.add(testService.startSession('testProvider', CancellationToken.None));
assert.strictEqual(model.getRequests().length, 0);
await assertSnapshot(model.toExport());
const response = await testService.sendRequest(model.sessionId, 'test request');
const response = await testService.sendRequest(model.sessionId, `@${chatAgentWithUsedContextId} test request`);
assert(response);
await response.responseCompletePromise;
@ -271,21 +234,18 @@ suite('Chat', () => {
});
test('can deserialize', async () => {
let serializedChatData: ISerializableChatData;
const providerId = 'ChatProviderWithUsedContext';
testDisposables.add(chatAgentService.registerAgent(chatAgentWithUsedContext));
// create the first service, send request, get response, and serialize the state
{ // serapate block to not leak variables in outer scope
const testService = testDisposables.add(instantiationService.createInstance(ChatService));
testDisposables.add(testService.registerProvider(testDisposables.add(new SimpleTestProvider('testProvider'))));
testDisposables.add(testService.registerProvider(testDisposables.add(new ChatProviderWithUsedContext(providerId))));
const chatModel1 = testDisposables.add(testService.startSession(providerId, CancellationToken.None));
const chatModel1 = testDisposables.add(testService.startSession('testProvider', CancellationToken.None));
assert.strictEqual(chatModel1.getRequests().length, 0);
const response = await testService.sendRequest(chatModel1.sessionId, 'test request');
const response = await testService.sendRequest(chatModel1.sessionId, `@${chatAgentWithUsedContextId} test request`);
assert(response);
await response.responseCompletePromise;
@ -296,17 +256,11 @@ suite('Chat', () => {
// try deserializing the state into a new service
const testService2 = testDisposables.add(instantiationService.createInstance(ChatService));
testDisposables.add(testService2.registerProvider(testDisposables.add(new ChatProviderWithUsedContext(providerId))));
testDisposables.add(testService2.registerProvider(testDisposables.add(new SimpleTestProvider('testProvider'))));
const chatModel2 = testService2.loadSessionFromContent(serializedChatData);
assert(chatModel2);
// should `loadSessionFromContent` return `ChatModel` that's disposable instead of `IChatModel`?
testDisposables.add({
dispose: () => testService2.clearSession(serializedChatData.sessionId)
});
await assertSnapshot(chatModel2.toExport());
});
});

View file

@ -729,7 +729,7 @@ export class CommentNode<T extends IRange | ICellRange> extends Disposable {
private onContextMenu(e: MouseEvent) {
const event = new StandardMouseEvent(e);
const event = new StandardMouseEvent(dom.getWindow(this._domNode), e);
this.contextMenuService.showContextMenu({
getAnchor: () => event,

View file

@ -117,7 +117,7 @@ export class CommentThreadHeader<T = IRange> extends Disposable {
if (!actions.length) {
return;
}
const event = new StandardMouseEvent(e);
const event = new StandardMouseEvent(dom.getWindow(this._headElement), e);
this._contextMenuService.showContextMenu({
getAnchor: () => event,
getActions: () => actions,

View file

@ -726,7 +726,7 @@ class InlineBreakpointWidget implements IContentWidget, IDisposable {
}
}));
this.toDispose.push(dom.addDisposableListener(this.domNode, dom.EventType.CONTEXT_MENU, e => {
const event = new StandardMouseEvent(e);
const event = new StandardMouseEvent(dom.getWindow(this.domNode), e);
const actions = this.getContextMenuActions();
this.contextMenuService.showContextMenu({
getAnchor: () => event,

View file

@ -151,7 +151,7 @@ export class DebugToolBar extends Themable implements IWorkbenchContribution {
this._register(dom.addDisposableListener(mainWindow, dom.EventType.RESIZE, () => this.setCoordinates()));
this._register(dom.addDisposableGenericMouseUpListener(this.dragArea, (event: MouseEvent) => {
const mouseClickEvent = new StandardMouseEvent(event);
const mouseClickEvent = new StandardMouseEvent(dom.getWindow(this.dragArea), event);
if (mouseClickEvent.detail === 2) {
// double click on debug bar centers it again #8250
const widgetWidth = this.$el.clientWidth;
@ -164,7 +164,7 @@ export class DebugToolBar extends Themable implements IWorkbenchContribution {
this.dragArea.classList.add('dragged');
const mouseMoveListener = dom.addDisposableGenericMouseMoveListener(mainWindow, (e: MouseEvent) => {
const mouseMoveEvent = new StandardMouseEvent(e);
const mouseMoveEvent = new StandardMouseEvent(mainWindow, e);
// Prevent default to stop editor selecting text #8524
mouseMoveEvent.preventDefault();
// Reduce x by width of drag handle to reduce jarring #16604

View file

@ -73,7 +73,7 @@ export class ExtensionsGridView extends Disposable {
e.preventDefault();
};
this.disposableStore.add(dom.addDisposableListener(template.name, dom.EventType.CLICK, (e: MouseEvent) => handleEvent(new StandardMouseEvent(e))));
this.disposableStore.add(dom.addDisposableListener(template.name, dom.EventType.CLICK, (e: MouseEvent) => handleEvent(new StandardMouseEvent(dom.getWindow(template.name), e))));
this.disposableStore.add(dom.addDisposableListener(template.name, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => handleEvent(new StandardKeyboardEvent(e))));
this.disposableStore.add(dom.addDisposableListener(extensionContainer, dom.EventType.KEY_DOWN, (e: KeyboardEvent) => handleEvent(new StandardKeyboardEvent(e))));

View file

@ -14,7 +14,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti
import { ZoneWidget } from 'vs/editor/contrib/zoneWidget/browser/zoneWidget';
import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_EMPTY, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_VISIBLE, MENU_INLINE_CHAT_WIDGET, MENU_INLINE_CHAT_WIDGET_STATUS, MENU_INLINE_CHAT_WIDGET_MARKDOWN_MESSAGE, CTX_INLINE_CHAT_MESSAGE_CROP_STATE, IInlineChatSlashCommand, MENU_INLINE_CHAT_WIDGET_FEEDBACK, ACTION_REGENERATE_RESPONSE, ACTION_VIEW_IN_CHAT, MENU_INLINE_CHAT_WIDGET_TOGGLE, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_INNER_CURSOR_START, CTX_INLINE_CHAT_INNER_CURSOR_END, CTX_INLINE_CHAT_RESPONSE_FOCUSED } from 'vs/workbench/contrib/inlineChat/common/inlineChat';
import { IModelDeltaDecoration, ITextModel } from 'vs/editor/common/model';
import { EventType, Dimension, addDisposableListener, getActiveElement, getTotalHeight, getTotalWidth, h, reset } from 'vs/base/browser/dom';
import { EventType, Dimension, addDisposableListener, getActiveElement, getTotalHeight, getTotalWidth, h, reset, getWindow } from 'vs/base/browser/dom';
import { Emitter, Event, MicrotaskEmitter } from 'vs/base/common/event';
import { IEditorConstructionOptions } from 'vs/editor/browser/config/editorConfiguration';
import { ICodeEditorWidgetOptions } from 'vs/editor/browser/widget/codeEditorWidget';
@ -403,7 +403,7 @@ export class InlineChatWidget {
}
private _onContextMenu(e: MouseEvent) {
const event = new StandardMouseEvent(e);
const event = new StandardMouseEvent(getWindow(this._elements.root), e);
this._contextMenuService.showContextMenu({
menuId: MENU_INLINE_CHAT_WIDGET_TOGGLE,
getAnchor: () => event,

View file

@ -135,7 +135,7 @@ export class NotebookStickyScroll extends Disposable {
}
private onContextMenu(e: MouseEvent) {
const event = new StandardMouseEvent(e);
const event = new StandardMouseEvent(DOM.getWindow(this.domNode), e);
this._contextMenuService.showContextMenu({
menuId: MenuId.NotebookStickyScrollContext,
getAnchor: () => event,

View file

@ -48,8 +48,8 @@ export class TerminalContextActionRunner extends ActionRunner {
}
}
export function openContextMenu(event: MouseEvent, contextInstances: SingleOrMany<ITerminalInstance> | undefined, menu: IMenu, contextMenuService: IContextMenuService, extraActions?: IAction[]): void {
const standardEvent = new StandardMouseEvent(event);
export function openContextMenu(targetWindow: Window, event: MouseEvent, contextInstances: SingleOrMany<ITerminalInstance> | undefined, menu: IMenu, contextMenuService: IContextMenuService, extraActions?: IAction[]): void {
const standardEvent = new StandardMouseEvent(targetWindow, event);
const actions: IAction[] = [];

View file

@ -142,7 +142,7 @@ export class TerminalEditor extends EditorPane {
// copyPaste: Shift+right click should open context menu
if (rightClickBehavior === 'copyPaste' && event.shiftKey) {
openContextMenu(event, this._editorInput?.terminalInstance, this._instanceMenu, this._contextMenuService);
openContextMenu(dom.getWindow(this._editorInstanceElement), event, this._editorInput?.terminalInstance, this._instanceMenu, this._contextMenuService);
return;
}
@ -180,7 +180,7 @@ export class TerminalEditor extends EditorPane {
else
if (!this._cancelContextMenu && rightClickBehavior !== 'copyPaste' && rightClickBehavior !== 'paste') {
if (!this._cancelContextMenu) {
openContextMenu(event, this._editorInput?.terminalInstance, this._instanceMenu, this._contextMenuService);
openContextMenu(dom.getWindow(this._editorInstanceElement), event, this._editorInput?.terminalInstance, this._instanceMenu, this._contextMenuService);
}
event.preventDefault();
event.stopImmediatePropagation();

View file

@ -353,7 +353,7 @@ export class TerminalTabbedView extends Disposable {
else if (rightClickBehavior === 'copyPaste' || rightClickBehavior === 'paste') {
// copyPaste: Shift+right click should open context menu
if (rightClickBehavior === 'copyPaste' && event.shiftKey) {
openContextMenu(event, terminal, this._instanceMenu, this._contextMenuService);
openContextMenu(dom.getWindow(terminalContainer), event, terminal, this._instanceMenu, this._contextMenuService);
return;
}
@ -387,7 +387,7 @@ export class TerminalTabbedView extends Disposable {
}
terminalContainer.focus();
if (!this._cancelContextMenu) {
openContextMenu(event, this._terminalGroupService.activeInstance!, this._instanceMenu, this._contextMenuService);
openContextMenu(dom.getWindow(terminalContainer), event, this._terminalGroupService.activeInstance!, this._instanceMenu, this._contextMenuService);
}
event.preventDefault();
event.stopImmediatePropagation();
@ -412,7 +412,7 @@ export class TerminalTabbedView extends Disposable {
selectedInstances.unshift(focusedInstance);
}
openContextMenu(event, selectedInstances, emptyList ? this._tabsListEmptyMenu : this._tabsListMenu, this._contextMenuService, emptyList ? this._getTabActions() : undefined);
openContextMenu(dom.getWindow(this._tabContainer), event, selectedInstances, emptyList ? this._tabsListEmptyMenu : this._tabsListMenu, this._contextMenuService, emptyList ? this._getTabActions() : undefined);
}
event.preventDefault();
event.stopImmediatePropagation();

View file

@ -66,8 +66,9 @@ export class TerminalFindWidget extends SimpleFindWidget {
event.stopPropagation();
}));
}
this._register(dom.addDisposableListener(this.getFindInputDomNode(), 'contextmenu', (event) => {
openContextMenu(event, _clipboardService, _contextMenuService);
const findInputDomNode = this.getFindInputDomNode();
this._register(dom.addDisposableListener(findInputDomNode, 'contextmenu', (event) => {
openContextMenu(dom.getWindow(findInputDomNode), event, _clipboardService, _contextMenuService);
event.stopPropagation();
}));
this._register(this._themeService.onDidColorThemeChange(() => {

View file

@ -11,8 +11,8 @@ import { localize } from 'vs/nls';
import { IClipboardService } from 'vs/platform/clipboard/common/clipboardService';
import { IContextMenuService } from 'vs/platform/contextview/browser/contextView';
export function openContextMenu(event: MouseEvent, clipboardService: IClipboardService, contextMenuService: IContextMenuService): void {
const standardEvent = new StandardMouseEvent(event);
export function openContextMenu(targetWindow: Window, event: MouseEvent, clipboardService: IClipboardService, contextMenuService: IContextMenuService): void {
const standardEvent = new StandardMouseEvent(targetWindow, event);
// Actions from workbench/browser/actions/textInputActions
const actions: IAction[] = [];

View file

@ -11,7 +11,6 @@ export const allApiProposals = Object.freeze({
authSession: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.authSession.d.ts',
canonicalUriProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.canonicalUriProvider.d.ts',
chat: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chat.d.ts',
chatAgents: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatAgents.d.ts',
chatAgents2: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatAgents2.d.ts',
chatAgents2Additions: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatAgents2Additions.d.ts',
chatProvider: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatProvider.d.ts',

View file

@ -1,39 +0,0 @@
/*---------------------------------------------------------------------------------------------
* 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 ChatAgentContext {
history: ChatMessage[];
}
export interface ChatAgentResponse {
message: MarkdownString | InteractiveProgressFileTree;
}
export interface ChatAgentResult {
followUp?: InteractiveSessionFollowup[];
}
export interface ChatAgentCommand {
name: string;
description: string;
}
export interface ChatAgentMetadata {
description: string;
fullName?: string;
icon?: Uri;
subCommands: ChatAgentCommand[];
}
export interface ChatAgent {
(prompt: ChatMessage, context: ChatAgentContext, progress: Progress<ChatAgentResponse>, token: CancellationToken): Thenable<ChatAgentResult | void>;
}
export namespace chat {
export function registerAgent(id: string, agent: ChatAgent, metadata: ChatAgentMetadata): Disposable;
}
}

View file

@ -91,6 +91,11 @@ declare module 'vscode' {
*/
readonly description: string;
/**
* When the user clicks this slash command in `/help`, this text will be submitted to this slash command
*/
readonly sampleRequest?: string;
/**
* Whether executing the command puts the
* chat into a persistent mode, where the
@ -204,6 +209,11 @@ declare module 'vscode' {
*/
followupProvider?: FollowupProvider;
/**
* When the user clicks this agent in `/help`, this text will be submitted to this slash command
*/
sampleRequest?: string;
/**
* An event that fires whenever feedback for a result is received, e.g. when a user up- or down-votes
* a result.

View file

@ -16,5 +16,15 @@ declare module 'vscode' {
* TODO@API name
*/
isSecondary?: boolean;
/**
* A string that will be added before the listing of chat agents in `/help`.
*/
helpTextPrefix?: string | MarkdownString;
/**
* A string that will be appended after the listing of chat agents in `/help`.
*/
helpTextPostfix?: string | MarkdownString;
}
}

View file

@ -85,9 +85,6 @@ declare module 'vscode' {
handleInteractiveEditorResponseFeedback?(session: S, response: R, kind: InteractiveEditorResponseFeedbackKind): void;
}
export interface InteractiveSessionState { }
export interface InteractiveSessionParticipantInformation {
name: string;
@ -101,81 +98,8 @@ declare module 'vscode' {
requester: InteractiveSessionParticipantInformation;
responder: InteractiveSessionParticipantInformation;
inputPlaceholder?: string;
saveState?(): InteractiveSessionState;
}
export interface InteractiveSessionRequestArgs {
command: string;
args: any;
}
export interface InteractiveRequest {
session: InteractiveSession;
message: string;
}
export interface InteractiveResponseErrorDetails {
message: string;
responseIsIncomplete?: boolean;
responseIsFiltered?: boolean;
}
export interface InteractiveResponseForProgress {
errorDetails?: InteractiveResponseErrorDetails;
}
export interface InteractiveContentReference {
reference: Uri | Location;
}
export interface InteractiveInlineContentReference {
inlineReference: Uri | Location;
title?: string; // eg symbol name
}
export interface InteractiveProgressContent {
content: string | MarkdownString;
}
export interface InteractiveProgressId {
responseId: string;
}
export interface InteractiveProgressTask {
placeholder: string;
resolvedContent: Thenable<InteractiveProgressContent | InteractiveProgressFileTree>;
}
export interface FileTreeData {
label: string;
uri: Uri;
children?: FileTreeData[];
}
export interface InteractiveProgressFileTree {
treeData: FileTreeData;
}
export interface DocumentContext {
uri: Uri;
version: number;
ranges: Range[];
}
export interface InteractiveProgressUsedContext {
documents: DocumentContext[];
}
export type InteractiveProgress =
| InteractiveProgressContent
| InteractiveProgressId
| InteractiveProgressTask
| InteractiveProgressFileTree
| InteractiveProgressUsedContext
| InteractiveContentReference
| InteractiveInlineContentReference;
export interface InteractiveResponseCommand {
commandId: string;
args?: any[];
@ -183,40 +107,18 @@ declare module 'vscode' {
when?: string;
}
export interface InteractiveSessionSlashCommand {
command: string;
kind: CompletionItemKind;
detail?: string;
shouldRepopulate?: boolean;
followupPlaceholder?: string;
executeImmediately?: boolean;
yieldTo?: ReadonlyArray<{ readonly command: string }>;
}
export interface InteractiveSessionReplyFollowup {
message: string;
tooltip?: string;
title?: string;
// Extensions can put any serializable data here, such as an ID/version
metadata?: any;
}
export type InteractiveSessionFollowup = InteractiveSessionReplyFollowup | InteractiveResponseCommand;
export type InteractiveWelcomeMessageContent = string | MarkdownString | InteractiveSessionReplyFollowup[];
export interface InteractiveSessionProvider<S extends InteractiveSession = InteractiveSession> {
provideWelcomeMessage?(token: CancellationToken): ProviderResult<InteractiveWelcomeMessageContent[]>;
provideSampleQuestions?(token: CancellationToken): ProviderResult<InteractiveSessionReplyFollowup[]>;
provideFollowups?(session: S, token: CancellationToken): ProviderResult<(string | InteractiveSessionFollowup)[]>;
provideSlashCommands?(session: S, token: CancellationToken): ProviderResult<InteractiveSessionSlashCommand[]>;
prepareSession(initialState: InteractiveSessionState | undefined, token: CancellationToken): ProviderResult<S>;
provideResponseWithProgress(request: InteractiveRequest, progress: Progress<InteractiveProgress>, token: CancellationToken): ProviderResult<InteractiveResponseForProgress>;
// eslint-disable-next-line local/vscode-dts-provider-naming
removeRequest(session: S, requestId: string): void;
prepareSession(token: CancellationToken): ProviderResult<S>;
}
export interface InteractiveSessionDynamicRequest {
@ -224,12 +126,6 @@ declare module 'vscode' {
* The message that will be displayed in the UI
*/
message: string;
/**
* Any extra metadata/context that will go to the provider.
* NOTE not actually used yet.
*/
metadata?: any;
}
export namespace interactive {

View file

@ -13,8 +13,6 @@ declare module 'vscode' {
export interface InteractiveSessionVoteAction {
// eslint-disable-next-line local/vscode-dts-string-type-literals
kind: 'vote';
// sessionId: string;
responseId: string;
direction: InteractiveSessionVoteDirection;
}
@ -27,8 +25,6 @@ declare module 'vscode' {
export interface InteractiveSessionCopyAction {
// eslint-disable-next-line local/vscode-dts-string-type-literals
kind: 'copy';
// sessionId: string;
responseId: string;
codeBlockIndex: number;
copyType: InteractiveSessionCopyKind;
copiedCharacters: number;
@ -39,8 +35,6 @@ declare module 'vscode' {
export interface InteractiveSessionInsertAction {
// eslint-disable-next-line local/vscode-dts-string-type-literals
kind: 'insert';
// sessionId: string;
responseId: string;
codeBlockIndex: number;
totalCharacters: number;
newFile?: boolean;
@ -49,8 +43,6 @@ declare module 'vscode' {
export interface InteractiveSessionTerminalAction {
// eslint-disable-next-line local/vscode-dts-string-type-literals
kind: 'runInTerminal';
// sessionId: string;
responseId: string;
codeBlockIndex: number;
languageId?: string;
}