Path: blob/main/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts
3296 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*--------------------------------------------------------------------------------------------*/4import { CancellationToken } from '../../../../base/common/cancellation.js';5import { Emitter, Event } from '../../../../base/common/event.js';6import { DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js';7import { ResourceMap } from '../../../../base/common/map.js';8import { Schemas } from '../../../../base/common/network.js';9import { autorun, IObservable, observableFromEvent } from '../../../../base/common/observable.js';10import { isEqual } from '../../../../base/common/resources.js';11import { assertType } from '../../../../base/common/types.js';12import { URI } from '../../../../base/common/uri.js';13import { generateUuid } from '../../../../base/common/uuid.js';14import { IActiveCodeEditor, ICodeEditor, isCodeEditor, isCompositeEditor, isDiffEditor } from '../../../../editor/browser/editorBrowser.js';15import { Range } from '../../../../editor/common/core/range.js';16import { ILanguageService } from '../../../../editor/common/languages/language.js';17import { IValidEditOperation } from '../../../../editor/common/model.js';18import { createTextBufferFactoryFromSnapshot } from '../../../../editor/common/model/textModel.js';19import { IEditorWorkerService } from '../../../../editor/common/services/editorWorker.js';20import { IModelService } from '../../../../editor/common/services/model.js';21import { ITextModelService } from '../../../../editor/common/services/resolverService.js';22import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';23import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';24import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';25import { ILogService } from '../../../../platform/log/common/log.js';26import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js';27import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';28import { DEFAULT_EDITOR_ASSOCIATION } from '../../../common/editor.js';29import { IEditorService } from '../../../services/editor/common/editorService.js';30import { ITextFileService } from '../../../services/textfile/common/textfiles.js';31import { UntitledTextEditorInput } from '../../../services/untitled/common/untitledTextEditorInput.js';32import { IChatWidgetService } from '../../chat/browser/chat.js';33import { IChatAgentService } from '../../chat/common/chatAgents.js';34import { ModifiedFileEntryState } from '../../chat/common/chatEditingService.js';35import { IChatService } from '../../chat/common/chatService.js';36import { ChatAgentLocation } from '../../chat/common/constants.js';37import { CTX_INLINE_CHAT_HAS_AGENT, CTX_INLINE_CHAT_HAS_AGENT2, CTX_INLINE_CHAT_POSSIBLE, InlineChatConfigKeys } from '../common/inlineChat.js';38import { HunkData, Session, SessionWholeRange, StashedSession, TelemetryData, TelemetryDataClassification } from './inlineChatSession.js';39import { IInlineChatSession2, IInlineChatSessionEndEvent, IInlineChatSessionEvent, IInlineChatSessionService, ISessionKeyComputer } from './inlineChatSessionService.js';404142type SessionData = {43editor: ICodeEditor;44session: Session;45store: IDisposable;46};4748export class InlineChatError extends Error {49static readonly code = 'InlineChatError';50constructor(message: string) {51super(message);52this.name = InlineChatError.code;53}54}555657export class InlineChatSessionServiceImpl implements IInlineChatSessionService {5859declare _serviceBrand: undefined;6061private readonly _store = new DisposableStore();6263private readonly _onWillStartSession = this._store.add(new Emitter<IActiveCodeEditor>());64readonly onWillStartSession: Event<IActiveCodeEditor> = this._onWillStartSession.event;6566private readonly _onDidMoveSession = this._store.add(new Emitter<IInlineChatSessionEvent>());67readonly onDidMoveSession: Event<IInlineChatSessionEvent> = this._onDidMoveSession.event;6869private readonly _onDidEndSession = this._store.add(new Emitter<IInlineChatSessionEndEvent>());70readonly onDidEndSession: Event<IInlineChatSessionEndEvent> = this._onDidEndSession.event;7172private readonly _onDidStashSession = this._store.add(new Emitter<IInlineChatSessionEvent>());73readonly onDidStashSession: Event<IInlineChatSessionEvent> = this._onDidStashSession.event;7475private readonly _sessions = new Map<string, SessionData>();76private readonly _keyComputers = new Map<string, ISessionKeyComputer>();7778readonly hideOnRequest: IObservable<boolean>;7980constructor(81@ITelemetryService private readonly _telemetryService: ITelemetryService,82@IModelService private readonly _modelService: IModelService,83@ITextModelService private readonly _textModelService: ITextModelService,84@IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService,85@ILogService private readonly _logService: ILogService,86@IInstantiationService private readonly _instaService: IInstantiationService,87@IEditorService private readonly _editorService: IEditorService,88@ITextFileService private readonly _textFileService: ITextFileService,89@ILanguageService private readonly _languageService: ILanguageService,90@IChatService private readonly _chatService: IChatService,91@IChatAgentService private readonly _chatAgentService: IChatAgentService,92@IChatWidgetService private readonly _chatWidgetService: IChatWidgetService,93@IConfigurationService private readonly _configurationService: IConfigurationService,94) {9596const v2 = observableConfigValue(InlineChatConfigKeys.EnableV2, false, this._configurationService);9798this.hideOnRequest = observableConfigValue(InlineChatConfigKeys.HideOnRequest, false, this._configurationService)99.map((value, r) => v2.read(r) && value);100}101102dispose() {103this._store.dispose();104this._sessions.forEach(x => x.store.dispose());105this._sessions.clear();106}107108async createSession(editor: IActiveCodeEditor, options: { headless?: boolean; wholeRange?: Range; session?: Session }, token: CancellationToken): Promise<Session | undefined> {109110const agent = this._chatAgentService.getDefaultAgent(ChatAgentLocation.Editor);111112if (!agent) {113this._logService.trace('[IE] NO agent found');114return undefined;115}116117this._onWillStartSession.fire(editor);118119const textModel = editor.getModel();120const selection = editor.getSelection();121122const store = new DisposableStore();123this._logService.trace(`[IE] creating NEW session for ${editor.getId()}, ${agent.extensionId}`);124125const chatModel = options.session?.chatModel ?? this._chatService.startSession(ChatAgentLocation.Editor, token);126if (!chatModel) {127this._logService.trace('[IE] NO chatModel found');128return undefined;129}130131store.add(toDisposable(() => {132const doesOtherSessionUseChatModel = [...this._sessions.values()].some(data => data.session !== session && data.session.chatModel === chatModel);133134if (!doesOtherSessionUseChatModel) {135this._chatService.clearSession(chatModel.sessionId);136chatModel.dispose();137}138}));139140const lastResponseListener = store.add(new MutableDisposable());141store.add(chatModel.onDidChange(e => {142if (e.kind !== 'addRequest' || !e.request.response) {143return;144}145146const { response } = e.request;147148session.markModelVersion(e.request);149lastResponseListener.value = response.onDidChange(() => {150151if (!response.isComplete) {152return;153}154155lastResponseListener.clear(); // ONCE156157// special handling for untitled files158for (const part of response.response.value) {159if (part.kind !== 'textEditGroup' || part.uri.scheme !== Schemas.untitled || isEqual(part.uri, session.textModelN.uri)) {160continue;161}162const langSelection = this._languageService.createByFilepathOrFirstLine(part.uri, undefined);163const untitledTextModel = this._textFileService.untitled.create({164associatedResource: part.uri,165languageId: langSelection.languageId166});167untitledTextModel.resolve();168this._textModelService.createModelReference(part.uri).then(ref => {169store.add(ref);170});171}172173});174}));175176store.add(this._chatAgentService.onDidChangeAgents(e => {177if (e === undefined && (!this._chatAgentService.getAgent(agent.id) || !this._chatAgentService.getActivatedAgents().map(agent => agent.id).includes(agent.id))) {178this._logService.trace(`[IE] provider GONE for ${editor.getId()}, ${agent.extensionId}`);179this._releaseSession(session, true);180}181}));182183const id = generateUuid();184const targetUri = textModel.uri;185186// AI edits happen in the actual model, keep a reference but make no copy187store.add((await this._textModelService.createModelReference(textModel.uri)));188const textModelN = textModel;189190// create: keep a snapshot of the "actual" model191const textModel0 = store.add(this._modelService.createModel(192createTextBufferFactoryFromSnapshot(textModel.createSnapshot()),193{ languageId: textModel.getLanguageId(), onDidChange: Event.None },194targetUri.with({ scheme: Schemas.vscode, authority: 'inline-chat', path: '', query: new URLSearchParams({ id, 'textModel0': '' }).toString() }), true195));196197// untitled documents are special and we are releasing their session when their last editor closes198if (targetUri.scheme === Schemas.untitled) {199store.add(this._editorService.onDidCloseEditor(() => {200if (!this._editorService.isOpened({ resource: targetUri, typeId: UntitledTextEditorInput.ID, editorId: DEFAULT_EDITOR_ASSOCIATION.id })) {201this._releaseSession(session, true);202}203}));204}205206let wholeRange = options.wholeRange;207if (!wholeRange) {208wholeRange = new Range(selection.selectionStartLineNumber, selection.selectionStartColumn, selection.positionLineNumber, selection.positionColumn);209}210211if (token.isCancellationRequested) {212store.dispose();213return undefined;214}215216const session = new Session(217options.headless ?? false,218targetUri,219textModel0,220textModelN,221agent,222store.add(new SessionWholeRange(textModelN, wholeRange)),223store.add(new HunkData(this._editorWorkerService, textModel0, textModelN)),224chatModel,225options.session?.versionsByRequest,226);227228// store: key -> session229const key = this._key(editor, session.targetUri);230if (this._sessions.has(key)) {231store.dispose();232throw new Error(`Session already stored for ${key}`);233}234this._sessions.set(key, { session, editor, store });235return session;236}237238moveSession(session: Session, target: ICodeEditor): void {239const newKey = this._key(target, session.targetUri);240const existing = this._sessions.get(newKey);241if (existing) {242if (existing.session !== session) {243throw new Error(`Cannot move session because the target editor already/still has one`);244} else {245// noop246return;247}248}249250let found = false;251for (const [oldKey, data] of this._sessions) {252if (data.session === session) {253found = true;254this._sessions.delete(oldKey);255this._sessions.set(newKey, { ...data, editor: target });256this._logService.trace(`[IE] did MOVE session for ${data.editor.getId()} to NEW EDITOR ${target.getId()}, ${session.agent.extensionId}`);257this._onDidMoveSession.fire({ session, editor: target });258break;259}260}261if (!found) {262throw new Error(`Cannot move session because it is not stored`);263}264}265266releaseSession(session: Session): void {267this._releaseSession(session, false);268}269270private _releaseSession(session: Session, byServer: boolean): void {271272let tuple: [string, SessionData] | undefined;273274// cleanup275for (const candidate of this._sessions) {276if (candidate[1].session === session) {277// if (value.session === session) {278tuple = candidate;279break;280}281}282283if (!tuple) {284// double remove285return;286}287288this._telemetryService.publicLog2<TelemetryData, TelemetryDataClassification>('interactiveEditor/session', session.asTelemetryData());289290const [key, value] = tuple;291this._sessions.delete(key);292this._logService.trace(`[IE] did RELEASED session for ${value.editor.getId()}, ${session.agent.extensionId}`);293294this._onDidEndSession.fire({ editor: value.editor, session, endedByExternalCause: byServer });295value.store.dispose();296}297298stashSession(session: Session, editor: ICodeEditor, undoCancelEdits: IValidEditOperation[]): StashedSession {299const result = this._instaService.createInstance(StashedSession, editor, session, undoCancelEdits);300this._onDidStashSession.fire({ editor, session });301this._logService.trace(`[IE] did STASH session for ${editor.getId()}, ${session.agent.extensionId}`);302return result;303}304305getCodeEditor(session: Session): ICodeEditor {306for (const [, data] of this._sessions) {307if (data.session === session) {308return data.editor;309}310}311throw new Error('session not found');312}313314getSession(editor: ICodeEditor, uri: URI): Session | undefined {315const key = this._key(editor, uri);316return this._sessions.get(key)?.session;317}318319private _key(editor: ICodeEditor, uri: URI): string {320const item = this._keyComputers.get(uri.scheme);321return item322? item.getComparisonKey(editor, uri)323: `${editor.getId()}@${uri.toString()}`;324325}326327registerSessionKeyComputer(scheme: string, value: ISessionKeyComputer): IDisposable {328this._keyComputers.set(scheme, value);329return toDisposable(() => this._keyComputers.delete(scheme));330}331332// ---- NEW333334private readonly _sessions2 = new ResourceMap<IInlineChatSession2>();335336private readonly _onDidChangeSessions = this._store.add(new Emitter<this>());337readonly onDidChangeSessions: Event<this> = this._onDidChangeSessions.event;338339340async createSession2(editor: ICodeEditor, uri: URI, token: CancellationToken): Promise<IInlineChatSession2> {341342assertType(editor.hasModel());343344if (this._sessions2.has(uri)) {345throw new Error('Session already exists');346}347348this._onWillStartSession.fire(editor as IActiveCodeEditor);349350const chatModel = this._chatService.startSession(ChatAgentLocation.Panel, token, false);351352const editingSession = await chatModel.editingSessionObs?.promise!;353const widget = this._chatWidgetService.getWidgetBySessionId(chatModel.sessionId);354await widget?.attachmentModel.addFile(uri);355356const store = new DisposableStore();357store.add(toDisposable(() => {358this._chatService.cancelCurrentRequestForSession(chatModel.sessionId);359editingSession.reject();360this._sessions2.delete(uri);361this._onDidChangeSessions.fire(this);362}));363store.add(chatModel);364365store.add(autorun(r => {366367const entries = editingSession.entries.read(r);368if (entries.length === 0) {369return;370}371372const allSettled = entries.every(entry => {373const state = entry.state.read(r);374return (state === ModifiedFileEntryState.Accepted || state === ModifiedFileEntryState.Rejected)375&& !entry.isCurrentlyBeingModifiedBy.read(r);376});377378if (allSettled && !chatModel.requestInProgress) {379// self terminate380store.dispose();381}382}));383384const result: IInlineChatSession2 = {385uri,386initialPosition: editor.getPosition().delta(-1),387chatModel,388editingSession,389dispose: store.dispose.bind(store)390};391this._sessions2.set(uri, result);392this._onDidChangeSessions.fire(this);393return result;394}395396getSession2(uri: URI): IInlineChatSession2 | undefined {397let result = this._sessions2.get(uri);398if (!result) {399// no direct session, try to find an editing session which has a file entry for the uri400for (const [_, candidate] of this._sessions2) {401const entry = candidate.editingSession.getEntry(uri);402if (entry) {403result = candidate;404break;405}406}407}408409return result;410}411}412413export class InlineChatEnabler {414415static Id = 'inlineChat.enabler';416417private readonly _ctxHasProvider: IContextKey<boolean>;418private readonly _ctxHasProvider2: IContextKey<boolean>;419private readonly _ctxPossible: IContextKey<boolean>;420421private readonly _store = new DisposableStore();422423constructor(424@IContextKeyService contextKeyService: IContextKeyService,425@IChatAgentService chatAgentService: IChatAgentService,426@IEditorService editorService: IEditorService,427@IConfigurationService configService: IConfigurationService,428) {429this._ctxHasProvider = CTX_INLINE_CHAT_HAS_AGENT.bindTo(contextKeyService);430this._ctxHasProvider2 = CTX_INLINE_CHAT_HAS_AGENT2.bindTo(contextKeyService);431this._ctxPossible = CTX_INLINE_CHAT_POSSIBLE.bindTo(contextKeyService);432433const agentObs = observableFromEvent(this, chatAgentService.onDidChangeAgents, () => chatAgentService.getDefaultAgent(ChatAgentLocation.Editor));434const inlineChat2Obs = observableConfigValue(InlineChatConfigKeys.EnableV2, false, configService);435436this._store.add(autorun(r => {437const v2 = inlineChat2Obs.read(r);438const agent = agentObs.read(r);439if (!agent) {440this._ctxHasProvider.reset();441this._ctxHasProvider2.reset();442} else if (v2) {443this._ctxHasProvider.reset();444this._ctxHasProvider2.set(true);445} else {446this._ctxHasProvider.set(true);447this._ctxHasProvider2.reset();448}449}));450451const updateEditor = () => {452const ctrl = editorService.activeEditorPane?.getControl();453const isCodeEditorLike = isCodeEditor(ctrl) || isDiffEditor(ctrl) || isCompositeEditor(ctrl);454this._ctxPossible.set(isCodeEditorLike);455};456457this._store.add(editorService.onDidActiveEditorChange(updateEditor));458updateEditor();459}460461dispose() {462this._ctxPossible.reset();463this._ctxHasProvider.reset();464this._store.dispose();465}466}467468469