Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chat: prototype of choices API for response stream #238011

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/vs/workbench/api/common/extHost.api.impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1774,6 +1774,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
ChatResponseWarningPart: extHostTypes.ChatResponseWarningPart,
ChatResponseTextEditPart: extHostTypes.ChatResponseTextEditPart,
ChatResponseMarkdownWithVulnerabilitiesPart: extHostTypes.ChatResponseMarkdownWithVulnerabilitiesPart,
ChatResponseChoicesPart: extHostTypes.ChatResponseChoicesPart,
ChatResponseCommandButtonPart: extHostTypes.ChatResponseCommandButtonPart,
ChatResponseDetectedParticipantPart: extHostTypes.ChatResponseDetectedParticipantPart,
ChatResponseConfirmationPart: extHostTypes.ChatResponseConfirmationPart,
Expand Down
14 changes: 13 additions & 1 deletion src/vs/workbench/api/common/extHostChatAgents2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,17 @@ class ChatAgentResponseStream {
_report(dto);
return this;
},
choices(titleOrOpts: string | vscode.ChatChoicesOptions, ...items: (string | vscode.ChatResponseChoice)[]) {
throwIfDone(this.choices);
checkProposedApiEnabled(that._extension, 'chatParticipantAdditions');

const part = typeof titleOrOpts === 'string'
? new extHostTypes.ChatResponseChoicesPart(titleOrOpts, items[0] as string, items.slice(1))
: new extHostTypes.ChatResponseChoicesPart(titleOrOpts.title, titleOrOpts.message, items, titleOrOpts.disableAfterUse);

const dto = typeConvert.ChatResponseChoicesPart.from(part);
_report(dto);
},
push(part) {
throwIfDone(this.push);

Expand All @@ -253,7 +264,8 @@ class ChatAgentResponseStream {
part instanceof extHostTypes.ChatResponseConfirmationPart ||
part instanceof extHostTypes.ChatResponseCodeCitationPart ||
part instanceof extHostTypes.ChatResponseMovePart ||
part instanceof extHostTypes.ChatResponseProgressPart2
part instanceof extHostTypes.ChatResponseProgressPart2 ||
part instanceof extHostTypes.ChatResponseChoicesPart
) {
checkProposedApiEnabled(that._extension, 'chatParticipantAdditions');
}
Expand Down
19 changes: 17 additions & 2 deletions src/vs/workbench/api/common/extHostTypeConverters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import { DEFAULT_EDITOR_ASSOCIATION, SaveReason } from '../../common/editor.js';
import { IViewBadge } from '../../common/views.js';
import { ChatAgentLocation, IChatAgentRequest, IChatAgentResult } from '../../contrib/chat/common/chatAgents.js';
import { IChatRequestVariableEntry } from '../../contrib/chat/common/chatModel.js';
import { IChatAgentDetection, IChatAgentMarkdownContentWithVulnerability, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatMoveMessage, IChatProgressMessage, IChatResponseCodeblockUriPart, IChatTaskDto, IChatTaskResult, IChatTextEdit, IChatTreeData, IChatUserActionEvent, IChatWarningMessage } from '../../contrib/chat/common/chatService.js';
import { IChatAgentDetection, IChatAgentMarkdownContentWithVulnerability, IChatChoices, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatFollowup, IChatMarkdownContent, IChatMoveMessage, IChatProgressMessage, IChatResponseCodeblockUriPart, IChatTaskDto, IChatTaskResult, IChatTextEdit, IChatTreeData, IChatUserActionEvent, IChatWarningMessage } from '../../contrib/chat/common/chatService.js';
import { IToolData, IToolResult } from '../../contrib/chat/common/languageModelToolsService.js';
import * as chatProvider from '../../contrib/chat/common/languageModels.js';
import { DebugTreeItemCollapsibleState, IDebugVisualizationTreeItem } from '../../contrib/debug/common/debug.js';
Expand Down Expand Up @@ -2519,6 +2519,18 @@ export namespace ChatResponseConfirmationPart {
}
}

export namespace ChatResponseChoicesPart {
export function from(part: vscode.ChatResponseChoicesPart): Dto<IChatChoices> {
return {
kind: 'choices',
title: part.title,
message: part.message,
disableAfterUse: part.disableAfterUse,
items: part.items,
};
}
}

export namespace ChatResponseFilesPart {
export function from(part: vscode.ChatResponseFileTreePart): IChatTreeData {
const { value, baseUri } = part;
Expand Down Expand Up @@ -2735,7 +2747,7 @@ export namespace ChatResponseCodeCitationPart {

export namespace ChatResponsePart {

export function from(part: vscode.ChatResponsePart | vscode.ChatResponseTextEditPart | vscode.ChatResponseMarkdownWithVulnerabilitiesPart | vscode.ChatResponseDetectedParticipantPart | vscode.ChatResponseWarningPart | vscode.ChatResponseConfirmationPart | vscode.ChatResponseReferencePart2 | vscode.ChatResponseMovePart, commandsConverter: CommandsConverter, commandDisposables: DisposableStore): extHostProtocol.IChatProgressDto {
export function from(part: vscode.ChatResponsePart | vscode.ChatResponseTextEditPart | vscode.ChatResponseMarkdownWithVulnerabilitiesPart | vscode.ChatResponseDetectedParticipantPart | vscode.ChatResponseWarningPart | vscode.ChatResponseConfirmationPart | vscode.ChatResponseReferencePart2 | vscode.ChatResponseMovePart | vscode.ChatResponseChoicesPart, commandsConverter: CommandsConverter, commandDisposables: DisposableStore): extHostProtocol.IChatProgressDto {
if (part instanceof types.ChatResponseMarkdownPart) {
return ChatResponseMarkdownPart.from(part);
} else if (part instanceof types.ChatResponseAnchorPart) {
Expand Down Expand Up @@ -2764,6 +2776,8 @@ export namespace ChatResponsePart {
return ChatResponseCodeCitationPart.from(part);
} else if (part instanceof types.ChatResponseMovePart) {
return ChatResponseMovePart.from(part);
} else if (part instanceof types.ChatResponseChoicesPart) {
return ChatResponseChoicesPart.from(part);
}

return {
Expand Down Expand Up @@ -2813,6 +2827,7 @@ export namespace ChatAgentRequest {
location: ChatLocation.to(request.location),
acceptedConfirmationData: request.acceptedConfirmationData,
rejectedConfirmationData: request.rejectedConfirmationData,
choiceData: request.choiceData,
location2,
toolInvocationToken: Object.freeze({ sessionId: request.sessionId }) as never,
model
Expand Down
9 changes: 9 additions & 0 deletions src/vs/workbench/api/common/extHostTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4508,6 +4508,15 @@ export class ChatResponseDetectedParticipantPart {
}
}

export class ChatResponseChoicesPart {
constructor(
public title: string,
public message: string,
public items: (string | vscode.ChatResponseChoice)[],
public disableAfterUse = false,
) { }
}

export class ChatResponseConfirmationPart {
title: string;
message: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -489,7 +489,7 @@ export function registerChatTitleActions() {
await chatService.sendRequest(editingSession.chatSessionId, '', {
agentId: editAgent.id,
acceptedConfirmationData: [{ _type: 'toEditTransfer', transferedTurnResults: sourceRequests.map(v => v.response?.result) }], // TODO@jrieken HACKY
confirmation: typeof this.desc.title === 'string' ? this.desc.title : this.desc.title.value
madeChoice: { title: typeof this.desc.title === 'string' ? this.desc.title : this.desc.title.value },
});
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { Emitter } from '../../../../../base/common/event.js';
import { Disposable, IDisposable } from '../../../../../base/common/lifecycle.js';
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
import { IChatProgressRenderableResponseContent } from '../../common/chatModel.js';
import { IChatChoices, IChatSendRequestOptions, IChatService } from '../../common/chatService.js';
import { isResponseVM } from '../../common/chatViewModel.js';
import { IChatWidgetService } from '../chat.js';
import { ChatChoicesWidget } from './chatChoicesWidget.js';
import { IChatContentPart, IChatContentPartRenderContext } from './chatContentParts.js';

export class ChatChoicesContentPart extends Disposable implements IChatContentPart {
public readonly domNode: HTMLElement;

private readonly _onDidChangeHeight = this._register(new Emitter<void>());
public readonly onDidChangeHeight = this._onDidChangeHeight.event;

private get showButtons() {
return this.choices.disableAfterUse ? !this.choices.isUsed : true;
}

constructor(
private readonly choices: IChatChoices,
context: IChatContentPartRenderContext,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IChatService private readonly chatService: IChatService,
@IChatWidgetService chatWidgetService: IChatWidgetService,
) {
super();

const element = context.element;
const buttonsWidget = this._register(this.instantiationService.createInstance(
ChatChoicesWidget<string | { title: string }>,
choices.title,
choices.message,
choices.items.map((choice, i) => ({
label: choiceLabel(choice),
data: choice,
isSecondary: i > 0,
}))
));
buttonsWidget.setShowButtons(this.showButtons);

this._register(buttonsWidget.onDidClick(async e => {
if (isResponseVM(element)) {
const prompt = `${e.label}: "${choices.title}"`;
const options: IChatSendRequestOptions = {
choiceData: [e.data],
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Having trouble to understand why this is an array and not just "the item", also how is the related to madeChoice. From the usage they seem equal'ish but from the API choiceData suggests to be all data and the latter seems to be the actual choice made?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was following the pattern of confirmations, but I'm good making it a single object if you think it makes more sense

agentId: element.agent?.id,
slashCommand: element.slashCommand?.name,
madeChoice: {
title: e.label,
responseId: context.element.id,
},
userSelectedModelId: chatWidgetService.getWidgetBySessionId(element.sessionId)?.input.currentLanguageModel,
};

const wasShowingButtons = this.showButtons;
if (await this.chatService.sendRequest(element.sessionId, prompt, options)) {
choices.isUsed = true;
if (this.showButtons !== wasShowingButtons) {
buttonsWidget.setShowButtons(this.showButtons);
this._onDidChangeHeight.fire();
}
}
}
}));

this.domNode = buttonsWidget.domNode;
}

hasSameContent(other: IChatProgressRenderableResponseContent): boolean {
// No other change allowed for this content type
return other.kind === 'choices';
}

addDisposable(disposable: IDisposable): void {
this._register(disposable);
}
}

const choiceLabel = (choice: string | { title: string }) => typeof choice === 'string' ? choice : choice.title;
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ import { MarkdownRenderer } from '../../../../../editor/browser/widget/markdownR
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
import { defaultButtonStyles } from '../../../../../platform/theme/browser/defaultStyles.js';

export interface IChatConfirmationButton {
export interface IChatConfirmationButton<T> {
label: string;
isSecondary?: boolean;
data: any;
data: T;
}

export class ChatConfirmationWidget extends Disposable {
private _onDidClick = this._register(new Emitter<IChatConfirmationButton>());
get onDidClick(): Event<IChatConfirmationButton> { return this._onDidClick.event; }
export class ChatChoicesWidget<T> extends Disposable {
private _onDidClick = this._register(new Emitter<IChatConfirmationButton<T>>());
get onDidClick(): Event<IChatConfirmationButton<T>> { return this._onDidClick.event; }

private _domNode: HTMLElement;
get domNode(): HTMLElement {
Expand All @@ -35,7 +35,7 @@ export class ChatConfirmationWidget extends Disposable {
constructor(
title: string,
message: string | IMarkdownString,
buttons: IChatConfirmationButton[],
buttons: IChatConfirmationButton<T>[],
@IInstantiationService private readonly instantiationService: IInstantiationService,
) {
super();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { IChatProgressRenderableResponseContent } from '../../common/chatModel.j
import { IChatConfirmation, IChatSendRequestOptions, IChatService } from '../../common/chatService.js';
import { isResponseVM } from '../../common/chatViewModel.js';
import { IChatWidgetService } from '../chat.js';
import { ChatConfirmationWidget } from './chatConfirmationWidget.js';
import { ChatChoicesWidget } from './chatChoicesWidget.js';
import { IChatContentPart, IChatContentPartRenderContext } from './chatContentParts.js';

export class ChatConfirmationContentPart extends Disposable implements IChatContentPart {
Expand Down Expand Up @@ -39,7 +39,7 @@ export class ChatConfirmationContentPart extends Disposable implements IChatCont
{ label: localize('accept', "Accept"), data: confirmation.data },
{ label: localize('dismiss', "Dismiss"), data: confirmation.data, isSecondary: true },
];
const confirmationWidget = this._register(this.instantiationService.createInstance(ChatConfirmationWidget, confirmation.title, confirmation.message, buttons));
const confirmationWidget = this._register(this.instantiationService.createInstance(ChatChoicesWidget, confirmation.title, confirmation.message, buttons));
confirmationWidget.setShowButtons(!confirmation.isUsed);

this._register(confirmationWidget.onDidClick(async e => {
Expand All @@ -50,7 +50,7 @@ export class ChatConfirmationContentPart extends Disposable implements IChatCont
{ acceptedConfirmationData: [e.data] };
options.agentId = element.agent?.id;
options.slashCommand = element.slashCommand?.name;
options.confirmation = e.label;
options.madeChoice = { title: e.label };
options.userSelectedModelId = chatWidgetService.getWidgetBySessionId(element.sessionId)?.input.currentLanguageModel;
if (await this.chatService.sendRequest(element.sessionId, prompt, options)) {
confirmation.isUsed = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { IInstantiationService } from '../../../../../platform/instantiation/com
import { IChatProgressMessage, IChatToolInvocation, IChatToolInvocationSerialized } from '../../common/chatService.js';
import { IChatRendererContent } from '../../common/chatViewModel.js';
import { ChatTreeItem } from '../chat.js';
import { ChatConfirmationWidget } from './chatConfirmationWidget.js';
import { ChatChoicesWidget } from './chatChoicesWidget.js';
import { IChatContentPart, IChatContentPartRenderContext } from './chatContentParts.js';
import { ChatProgressContentPart } from './chatProgressContentPart.js';

Expand Down Expand Up @@ -78,7 +78,7 @@ class ChatToolInvocationSubPart extends Disposable {
const title = toolInvocation.confirmationMessages.title;
const message = toolInvocation.confirmationMessages.message;
const confirmWidget = this._register(instantiationService.createInstance(
ChatConfirmationWidget,
ChatChoicesWidget<any>,
title,
message,
[{ label: localize('continue', "Continue"), data: true }, { label: localize('cancel', "Cancel"), data: false, isSecondary: true }]));
Expand Down
32 changes: 32 additions & 0 deletions src/vs/workbench/contrib/chat/browser/chatListFilter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { ITreeFilter, TreeFilterResult } from '../../../../base/browser/ui/tree/tree.js';
import { FuzzyScore } from '../../../../base/common/filters.js';
import { isRequestVM, isResponseVM } from '../common/chatViewModel.js';
import { ChatTreeItem } from './chat.js';

export interface IChatWidgetFilterDelegate {
getPrevElement(element: ChatTreeItem): ChatTreeItem | null;
}

export class ChatWidgetFilter implements ITreeFilter<ChatTreeItem, FuzzyScore> {
constructor(
private readonly delegate: IChatWidgetFilterDelegate,
private readonly inherited: undefined | ((item: ChatTreeItem) => boolean),
) { }

filter(element: ChatTreeItem): TreeFilterResult<FuzzyScore> {
if (isRequestVM(element)) {
const isChoiceFromResponseId = element.madeChoice?.responseId;
const previous = this.delegate.getPrevElement(element);
if (isResponseVM(previous) && previous.id === isChoiceFromResponseId) {
return false;
}
}

return this.inherited?.(element) ?? true;
}
}
Loading
Loading