Path: blob/main/src/vs/workbench/contrib/chat/browser/agentSessions/agentSessionApprovalModel.ts
13406 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { renderAsPlaintext } from '../../../../../base/browser/markdownRenderer.js';6import { Disposable, DisposableResourceMap, IDisposable } from '../../../../../base/common/lifecycle.js';7import { autorun, autorunIterableDelta, IObservable, ISettableObservable, observableValue } from '../../../../../base/common/observable.js';8import { URI } from '../../../../../base/common/uri.js';9import { migrateLegacyTerminalToolSpecificData } from '../../common/chat.js';10import { IChatModel } from '../../common/model/chatModel.js';11import { IChatService, IChatToolInvocation, ToolConfirmKind } from '../../common/chatService/chatService.js';12import { ILanguageService } from '../../../../../editor/common/languages/language.js';1314export interface IAgentSessionApprovalInfo {15readonly label: string;16readonly languageId: string | undefined;17readonly since: Date;18confirm(): void;19}2021/**22* Tracks approval state for all live chat sessions. For each session,23* exposes an observable that emits {@link IAgentSessionApprovalInfo}24* when a tool invocation is waiting for user confirmation, or `undefined`25* when no approval is needed.26*/27export class AgentSessionApprovalModel extends Disposable {2829private readonly _approvals = new Map<string, ISettableObservable<IAgentSessionApprovalInfo | undefined>>();30private readonly _modelTrackers = this._register(new DisposableResourceMap());3132constructor(33@IChatService private readonly _chatService: IChatService,34@ILanguageService private readonly _languageService: ILanguageService,35) {36super();3738this._register(autorunIterableDelta(39reader => this._chatService.chatModels.read(reader),40({ addedValues, removedValues }) => {41for (const model of addedValues) {42this._modelTrackers.set(model.sessionResource, this._trackModel(model));43}44for (const model of removedValues) {45this._modelTrackers.deleteAndDispose(model.sessionResource);46this._approvals.get(model.sessionResource.toString())?.set(undefined, undefined);47}48}49));50}5152getApproval(sessionResource: URI): IObservable<IAgentSessionApprovalInfo | undefined> {53return this._getOrCreateApproval(sessionResource.toString());54}5556private _getOrCreateApproval(key: string): ISettableObservable<IAgentSessionApprovalInfo | undefined> {57let obs = this._approvals.get(key);58if (!obs) {59obs = observableValue<IAgentSessionApprovalInfo | undefined>(`sessionApproval.${key}`, undefined);60this._approvals.set(key, obs);61}62return obs;63}6465private _trackModel(model: IChatModel): IDisposable {66const settable = this._getOrCreateApproval(model.sessionResource.toString());6768const setIfChanged = (value: IAgentSessionApprovalInfo | undefined) => {69const current = settable.get();70if (current === value) {71return;72}73if (current !== undefined && value !== undefined && current.label === value.label && current.languageId === value.languageId) {74return;75}76settable.set(value, undefined);77};7879return autorun(reader => {80const needsInput = model.requestNeedsInput.read(reader);81if (!needsInput) {82setIfChanged(undefined);83return;84}8586const lastResponse = model.lastRequest?.response;87if (!lastResponse?.response?.value) {88setIfChanged(undefined);89return;90}9192for (const part of lastResponse.response.value) {93if (part.kind !== 'toolInvocation' || part.toolSpecificData?.kind === 'modifiedFilesConfirmation') {94continue; // unsupported95}96const state = part.state.read(reader);97if (state.type === IChatToolInvocation.StateKind.WaitingForConfirmation || state.type === IChatToolInvocation.StateKind.WaitingForPostApproval) {98let label: string;99let languageId: string | undefined;100if (part.toolSpecificData?.kind === 'terminal') {101const terminalData = migrateLegacyTerminalToolSpecificData(part.toolSpecificData);102label = terminalData.presentationOverrides?.commandLine ?? terminalData.commandLine.forDisplay ?? terminalData.commandLine.userEdited ?? terminalData.commandLine.toolEdited ?? terminalData.commandLine.original;103languageId = this._languageService.getLanguageIdByLanguageName(terminalData.presentationOverrides?.language ?? terminalData.language) ?? undefined;104} else if (needsInput.detail) {105label = needsInput.detail;106} else {107const msg = part.invocationMessage;108label = typeof msg === 'string' ? msg : renderAsPlaintext(msg);109}110111const confirmState = state;112setIfChanged({113label,114languageId,115since: new Date(),116confirm: () => confirmState.confirm({ type: ToolConfirmKind.UserAction }),117});118return;119}120}121122setIfChanged(undefined);123});124}125}126127128