diff --git a/src/vs/workbench/api/browser/mainThreadChat.ts b/src/vs/workbench/api/browser/mainThreadChat.ts index 630ea5116d2..a68c2d1c83d 100644 --- a/src/vs/workbench/api/browser/mainThreadChat.ts +++ b/src/vs/workbench/api/browser/mainThreadChat.ts @@ -38,7 +38,9 @@ export class MainThreadChat extends Disposable implements MainThreadChatShape { this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostChat); this._register(this._chatService.onDidPerformUserAction(e => { - this._proxy.$onDidPerformUserAction(e); + if (!e.agentId) { + this._proxy.$onDidPerformUserAction(e); + } })); } diff --git a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts index c6102cbeecc..438583607e9 100644 --- a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts +++ b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts @@ -36,6 +36,20 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA this._register(this._chatService.onDidDisposeSession(e => { this._proxy.$releaseSession(e.sessionId); })); + this._register(this._chatService.onDidPerformUserAction(e => { + if (e.agentId) { + for (const [handle, agent] of this._agents) { + if (agent.name === e.agentId) { + if (e.action.kind === 'vote') { + this._proxy.$acceptFeedback(handle, e.sessionId, e.action.direction); + } else { + this._proxy.$acceptAction(handle, e.sessionId, e); + } + break; + } + } + } + })); } $unregisterAgent(handle: number): void { diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 1fe5ef33fa6..cd983bdc650 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1372,7 +1372,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I }, createChatAgent(name: string, handler: vscode.ChatAgentHandler) { checkProposedApiEnabled(extension, 'chatAgents2'); - return extHostChatAgents2.createChatAgent(extension.identifier, name, handler); + return extHostChatAgents2.createChatAgent(extension, name, handler); }, registerAgent(name: string, agent: vscode.ChatAgent, metadata: vscode.ChatAgentMetadata) { checkProposedApiEnabled(extension, 'chatAgents'); @@ -1412,6 +1412,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I // types Breakpoint: extHostTypes.Breakpoint, TerminalOutputAnchor: extHostTypes.TerminalOutputAnchor, + ChatAgentResultFeedbackKind: extHostTypes.ChatAgentResultFeedbackKind, ChatMessage: extHostTypes.ChatMessage, ChatMessageRole: extHostTypes.ChatMessageRole, ChatVariableLevel: extHostTypes.ChatVariableLevel, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 86c7f4ade21..bf0b7392ca4 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -52,7 +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 { IChatDynamicRequest, IChatFollowup, IChatReplyFollowup, IChatResponseErrorDetails, IChatUserActionEvent, ISlashCommand } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatDynamicRequest, IChatFollowup, IChatReplyFollowup, IChatResponseErrorDetails, IChatUserActionEvent, ISlashCommand, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; import { IChatSlashFragment } from 'vs/workbench/contrib/chat/common/chatSlashCommands'; import { IChatRequestVariableValue, IChatVariableData } from 'vs/workbench/contrib/chat/common/chatVariables'; import { DebugConfigurationProviderTriggerKind, IAdapterDescriptor, IConfig, IDebugSessionReplMode } from 'vs/workbench/contrib/debug/common/debug'; @@ -1184,6 +1184,8 @@ export interface ExtHostChatAgentsShape2 { $invokeAgent(handle: number, sessionId: string, requestId: number, request: IChatAgentRequest, context: { history: IChatMessage[] }, token: CancellationToken): Promise; $provideSlashCommands(handle: number, token: CancellationToken): Promise; $provideFollowups(handle: number, sessionId: string, token: CancellationToken): Promise; + $acceptFeedback(handle: number, sessionId: string, vote: InteractiveSessionVoteDirection): void; + $acceptAction(handle: number, sessionId: string, action: IChatUserActionEvent): void; $releaseSession(sessionId: string): void; } diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index 3ec9a443b9c..bd48108cd4a 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -6,17 +6,20 @@ import { DeferredPromise, raceCancellation } 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 { assertType } from 'vs/base/common/types'; import { URI } from 'vs/base/common/uri'; -import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions'; +import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ILogService } from 'vs/platform/log/common/log'; import { Progress } from 'vs/platform/progress/common/progress'; import { ExtHostChatAgentsShape2, IMainContext, MainContext, MainThreadChatAgentsShape2 } 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 { ChatAgentResultFeedbackKind } from 'vs/workbench/api/common/extHostTypes'; import { IChatAgentCommand, IChatAgentRequest, IChatAgentResult } from 'vs/workbench/contrib/chat/common/chatAgents'; import { IChatMessage } from 'vs/workbench/contrib/chat/common/chatProvider'; -import { IChatFollowup } from 'vs/workbench/contrib/chat/common/chatService'; +import { IChatFollowup, IChatUserActionEvent, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; +import { isProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions'; import type * as vscode from 'vscode'; export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 { @@ -36,7 +39,7 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 { this._proxy = mainContext.getProxy(MainContext.MainThreadChatAgents2); } - createChatAgent(extension: ExtensionIdentifier, name: string, handler: vscode.ChatAgentHandler): vscode.ChatAgent2 { + createChatAgent(extension: IExtensionDescription, name: string, handler: vscode.ChatAgentHandler): vscode.ChatAgent2 { const handle = ExtHostChatAgents2._idPool++; const agent = new ExtHostChatAgent(extension, name, this._proxy, handle, handler); this._agents.set(handle, agent); @@ -61,7 +64,7 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 { const commandExecution = new DeferredPromise(); token.onCancellationRequested(() => commandExecution.complete()); setTimeout(() => commandExecution.complete(), 3 * 1000); - this._extHostChatProvider.allowListExtensionWhile(agent.extension, commandExecution.p); + this._extHostChatProvider.allowListExtensionWhile(agent.extension.identifier, commandExecution.p); const slashCommand = request.command ? await agent.validateSlashCommand(request.command) @@ -134,6 +137,44 @@ export class ExtHostChatAgents2 implements ExtHostChatAgentsShape2 { return agent.provideFollowups(result, token); } + + $acceptFeedback(handle: number, sessionId: string, vote: InteractiveSessionVoteDirection): void { + const agent = this._agents.get(handle); + if (!agent) { + return; + } + const result = this._previousResultMap.get(sessionId); + if (!result) { + return; + } + + let kind: ChatAgentResultFeedbackKind; + switch (vote) { + case InteractiveSessionVoteDirection.Down: + kind = ChatAgentResultFeedbackKind.Unhelpful; + break; + case InteractiveSessionVoteDirection.Up: + kind = ChatAgentResultFeedbackKind.Helpful; + break; + } + agent.acceptFeedback(Object.freeze({ result, kind })); + } + + $acceptAction(handle: number, sessionId: string, action: IChatUserActionEvent): void { + const agent = this._agents.get(handle); + if (!agent) { + return; + } + const result = this._previousResultMap.get(sessionId); + if (!result) { + return; + } + if (action.action.kind === 'vote') { + // handled by $acceptFeedback + return; + } + agent.acceptAction(Object.freeze({ action: action.action, result })); + } } class ExtHostChatAgent { @@ -144,15 +185,24 @@ class ExtHostChatAgent { private _description: string | undefined; private _fullName: string | undefined; private _iconPath: URI | undefined; + private _onDidReceiveFeedback = new Emitter(); + private _onDidPerformAction = new Emitter(); constructor( - public readonly extension: ExtensionIdentifier, + public readonly extension: IExtensionDescription, private readonly _id: string, private readonly _proxy: MainThreadChatAgentsShape2, private readonly _handle: number, private readonly _callback: vscode.ChatAgentHandler, ) { } + acceptFeedback(feedback: vscode.ChatAgentResult2Feedback) { + this._onDidReceiveFeedback.fire(feedback); + } + + acceptAction(event: vscode.ChatAgentUserActionEvent) { + this._onDidPerformAction.fire(event); + } async validateSlashCommand(command: string) { if (!this._lastSlashCommands) { @@ -191,9 +241,12 @@ class ExtHostChatAgent { } get apiAgent(): vscode.ChatAgent2 { - + let disposed = false; let updateScheduled = false; const updateMetadataSoon = () => { + if (disposed) { + return; + } if (updateScheduled) { return; } @@ -223,7 +276,7 @@ class ExtHostChatAgent { updateMetadataSoon(); }, get fullName() { - return that._fullName ?? that.extension.value; + return that._fullName ?? that.extension.displayName ?? that.extension.name; }, set fullName(v) { that._fullName = v; @@ -251,7 +304,18 @@ class ExtHostChatAgent { that._followupProvider = v; updateMetadataSoon(); }, + get onDidReceiveFeedback() { + return that._onDidReceiveFeedback.event; + }, + onDidPerformAction: !isProposedApiEnabled(this.extension, 'chatAgents2Additions') + ? undefined! + : this._onDidPerformAction.event + , dispose() { + disposed = true; + that._slashCommandProvider = undefined; + that._followupProvider = undefined; + that._onDidReceiveFeedback.dispose(); that._proxy.$unregisterAgent(that._handle); }, } satisfies vscode.ChatAgent2; diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index 55cde471a70..f92f0b4fc77 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -4128,6 +4128,11 @@ export class ChatMessage implements vscode.ChatMessage { } } +export enum ChatAgentResultFeedbackKind { + Unhelpful = 0, + Helpful = 1, +} + //#endregion //#region ai diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts index faa2d520396..e96cba918ef 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatCodeblockActions.ts @@ -105,6 +105,8 @@ export function registerChatCodeBlockActions() { const chatService = accessor.get(IChatService); chatService.notifyUserAction({ providerId: context.element.providerId, + agentId: context.element.agent?.id, + sessionId: context.element.sessionId, action: { kind: 'copy', responseId: context.element.providerResponseId, @@ -146,6 +148,8 @@ export function registerChatCodeBlockActions() { const chatService = accessor.get(IChatService); chatService.notifyUserAction({ providerId: context.element.providerId, + agentId: context.element.agent?.id, + sessionId: context.element.sessionId, action: { kind: 'copy', codeBlockIndex: context.codeBlockIndex, @@ -320,6 +324,8 @@ export function registerChatCodeBlockActions() { const chatService = accessor.get(IChatService); chatService.notifyUserAction({ providerId: context.element.providerId, + agentId: context.element.agent?.id, + sessionId: context.element.sessionId, action: { kind: 'insert', responseId: context.element.providerResponseId, @@ -363,6 +369,8 @@ export function registerChatCodeBlockActions() { chatService.notifyUserAction({ providerId: context.element.providerId, + agentId: context.element.agent?.id, + sessionId: context.element.sessionId, action: { kind: 'insert', responseId: context.element.providerResponseId, @@ -439,6 +447,8 @@ export function registerChatCodeBlockActions() { chatService.notifyUserAction({ providerId: context.element.providerId, + agentId: context.element.agent?.id, + sessionId: context.element.sessionId, action: { kind: 'runInTerminal', responseId: context.element.providerResponseId, diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts index 6d7a399b006..b2c5828ece2 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatTitleActions.ts @@ -54,10 +54,12 @@ export function registerChatTitleActions() { const chatService = accessor.get(IChatService); chatService.notifyUserAction({ providerId: item.providerId, + agentId: item.agent?.id, + sessionId: item.sessionId, action: { kind: 'vote', direction: InteractiveSessionVoteDirection.Up, - responseId: item.providerResponseId + responseId: item.providerResponseId, } }); item.setVote(InteractiveSessionVoteDirection.Up); @@ -94,10 +96,12 @@ export function registerChatTitleActions() { const chatService = accessor.get(IChatService); chatService.notifyUserAction({ providerId: item.providerId, + agentId: item.agent?.id, + sessionId: item.sessionId, action: { kind: 'vote', direction: InteractiveSessionVoteDirection.Down, - responseId: item.providerResponseId + responseId: item.providerResponseId, } }); item.setVote(InteractiveSessionVoteDirection.Down); diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index 84b9278cbc7..f4cd90ee8e4 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -365,9 +365,11 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer { this.chatService.notifyUserAction({ providerId: element.providerId, + agentId: element.agent?.id, + sessionId: element.sessionId, action: { kind: 'command', - command: followup + command: followup, } }); return this.commandService.executeCommand(followup.commandId, ...(followup.args ?? [])); diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index bdcb7101521..2d4dce54ed5 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -58,6 +58,7 @@ export interface IChatResponseModel { readonly username: string; readonly avatarIconUri?: URI; readonly session: IChatModel; + readonly agent?: IChatAgent; readonly response: IResponse; readonly isComplete: boolean; readonly isCanceled: boolean; diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index dacd94fd844..f71d8b7ae24 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -215,6 +215,8 @@ export type ChatUserAction = IChatVoteAction | IChatCopyAction | IChatInsertActi export interface IChatUserActionEvent { action: ChatUserAction; providerId: string; + agentId: string | undefined; + sessionId: string; } export interface IChatDynamicRequest { diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index af03c838a71..eb3b8680044 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -535,7 +535,11 @@ export class ChatService extends Disposable implements IChatService { const agentResult = await this.chatAgentService.invokeAgent(agentPart.agent.id, requestProps, new Progress(p => { progressCallback(p); }), history, token); - rawResponse = { session: model.session!, errorDetails: agentResult.errorDetails, timings: agentResult.timings }; + rawResponse = { + session: model.session!, + errorDetails: agentResult.errorDetails, + timings: agentResult.timings + }; agentOrCommandFollowups = agentResult?.followUp ? Promise.resolve(agentResult.followUp) : this.chatAgentService.getFollowups(agentPart.agent.id, sessionId, CancellationToken.None); } else if (commandPart && typeof message === 'string' && this.chatSlashCommandService.hasCommand(commandPart.slashCommand.command)) { diff --git a/src/vs/workbench/contrib/chat/common/chatViewModel.ts b/src/vs/workbench/contrib/chat/common/chatViewModel.ts index 09aa29178b2..a80685c88fa 100644 --- a/src/vs/workbench/contrib/chat/common/chatViewModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatViewModel.ts @@ -10,6 +10,7 @@ import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ILogService } from 'vs/platform/log/common/log'; +import { IChatAgent } from 'vs/workbench/contrib/chat/common/chatAgents'; import { ChatModelInitState, IChatModel, IChatRequestModel, IChatResponseModel, IChatWelcomeMessageContent, IResponse, Response } from 'vs/workbench/contrib/chat/common/chatModel'; import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChatReplyFollowup, IChatResponseCommandFollowup, IChatResponseErrorDetails, IChatResponseProgressFileTreeData, InteractiveSessionVoteDirection } from 'vs/workbench/contrib/chat/common/chatService'; @@ -83,6 +84,7 @@ export interface IChatResponseViewModel { readonly providerResponseId: string | undefined; readonly username: string; readonly avatarIconUri?: URI; + readonly agent?: IChatAgent; readonly response: IResponse; readonly isComplete: boolean; readonly isCanceled: boolean; @@ -259,6 +261,10 @@ export class ChatResponseViewModel extends Disposable implements IChatResponseVi return this._model.avatarIconUri; } + get agent() { + return this._model.agent; + } + get response(): IResponse { if (this._isPlaceholder) { return new Response(new MarkdownString(localize('thinking', "Thinking") + '\u2026')); diff --git a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts index 3724be62923..3ebf8b558b3 100644 --- a/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts +++ b/src/vs/workbench/services/extensions/common/extensionsApiProposals.ts @@ -13,6 +13,7 @@ export const allApiProposals = Object.freeze({ 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', chatRequestAccess: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatRequestAccess.d.ts', chatVariables: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatVariables.d.ts', diff --git a/src/vscode-dts/vscode.proposed.chatAgents2.d.ts b/src/vscode-dts/vscode.proposed.chatAgents2.d.ts index 830cb37a67c..879832237ec 100644 --- a/src/vscode-dts/vscode.proposed.chatAgents2.d.ts +++ b/src/vscode-dts/vscode.proposed.chatAgents2.d.ts @@ -22,6 +22,16 @@ declare module 'vscode' { errorDetails?: ChatAgentErrorDetails; } + export enum ChatAgentResultFeedbackKind { + Unhelpful = 0, + Helpful = 1, + } + + export interface ChatAgentResult2Feedback { + readonly result: ChatAgentResult2; + readonly kind: ChatAgentResultFeedbackKind; + } + export interface ChatAgentSlashCommand { /** @@ -51,6 +61,8 @@ declare module 'vscode' { provideSlashCommands(token: CancellationToken): ProviderResult; } + // TODO@API is this just a vscode.Command? + // TODO@API what's the when-property for? how about not returning it in the first place? export interface ChatAgentCommandFollowup { commandId: string; args?: any[]; @@ -96,9 +108,14 @@ declare module 'vscode' { followupProvider?: FollowupProvider; - // TODO@API We need this- can't handle telemetry on the vscode side yet - // onDidPerformAction: Event<{ action: InteractiveSessionUserAction }>; - + /** + * An event that fires whenever feedback for a result is received, e.g. when a user up- or down-votes + * a result. + * + * The passed {@link ChatAgentResult2Feedback.result result} is guaranteed to be the same instance that was + * previously returned from this chat agent. + */ + onDidReceiveFeedback: Event; // TODO@API Something like prepareSession from the interactive chat provider might be needed.Probably nobody needs it right now. // prepareSession(); diff --git a/src/vscode-dts/vscode.proposed.chatAgents2Additions.d.ts b/src/vscode-dts/vscode.proposed.chatAgents2Additions.d.ts new file mode 100644 index 00000000000..3e132347354 --- /dev/null +++ b/src/vscode-dts/vscode.proposed.chatAgents2Additions.d.ts @@ -0,0 +1,18 @@ +/*--------------------------------------------------------------------------------------------- + * 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 ChatAgentUserActionEvent { + readonly result: ChatAgentResult2; + readonly action: InteractiveSessionCopyAction | InteractiveSessionInsertAction | InteractiveSessionTerminalAction | InteractiveSessionCommandAction; + } + + export interface ChatAgent2 { + + // TODO@API We need this- can't handle telemetry on the vscode side yet + onDidPerformAction: Event; + } +}