Path: blob/main/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.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*--------------------------------------------------------------------------------------------*/45import { DeferredPromise, ITask, Sequencer, SequencerByKey, timeout } from '../../../../../base/common/async.js';6import { CancellationToken } from '../../../../../base/common/cancellation.js';7import { BugIndicatingError } from '../../../../../base/common/errors.js';8import { Emitter } from '../../../../../base/common/event.js';9import { Iterable } from '../../../../../base/common/iterator.js';10import { Disposable, dispose } from '../../../../../base/common/lifecycle.js';11import { ResourceMap } from '../../../../../base/common/map.js';12import { autorun, IObservable, IReader, ITransaction, observableValue, transaction } from '../../../../../base/common/observable.js';13import { isEqual } from '../../../../../base/common/resources.js';14import { URI } from '../../../../../base/common/uri.js';15import { IBulkEditService } from '../../../../../editor/browser/services/bulkEditService.js';16import { TextEdit } from '../../../../../editor/common/languages.js';17import { ILanguageService } from '../../../../../editor/common/languages/language.js';18import { ITextModel } from '../../../../../editor/common/model.js';19import { IModelService } from '../../../../../editor/common/services/model.js';20import { ITextModelService } from '../../../../../editor/common/services/resolverService.js';21import { localize } from '../../../../../nls.js';22import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js';23import { EditorActivation } from '../../../../../platform/editor/common/editor.js';24import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';25import { DiffEditorInput } from '../../../../common/editor/diffEditorInput.js';26import { IEditorGroupsService } from '../../../../services/editor/common/editorGroupsService.js';27import { IEditorService } from '../../../../services/editor/common/editorService.js';28import { MultiDiffEditor } from '../../../multiDiffEditor/browser/multiDiffEditor.js';29import { MultiDiffEditorInput } from '../../../multiDiffEditor/browser/multiDiffEditorInput.js';30import { CellUri, ICellEditOperation } from '../../../notebook/common/notebookCommon.js';31import { INotebookService } from '../../../notebook/common/notebookService.js';32import { ChatEditingSessionState, ChatEditKind, getMultiDiffSourceUri, IChatEditingSession, IModifiedEntryTelemetryInfo, IModifiedFileEntry, ISnapshotEntry, IStreamingEdits, ModifiedFileEntryState } from '../../common/chatEditingService.js';33import { IChatResponseModel } from '../../common/chatModel.js';34import { IChatService } from '../../common/chatService.js';35import { ChatAgentLocation } from '../../common/constants.js';36import { ChatEditingModifiedDocumentEntry } from './chatEditingModifiedDocumentEntry.js';37import { AbstractChatEditingModifiedFileEntry } from './chatEditingModifiedFileEntry.js';38import { ChatEditingModifiedNotebookEntry } from './chatEditingModifiedNotebookEntry.js';39import { ChatEditingSessionStorage, IChatEditingSessionSnapshot, IChatEditingSessionStop, StoredSessionState } from './chatEditingSessionStorage.js';40import { ChatEditingTextModelContentProvider } from './chatEditingTextModelContentProviders.js';41import { ChatEditingTimeline } from './chatEditingTimeline.js';4243const enum NotExistBehavior {44Create,45Abort,46}4748class ThrottledSequencer extends Sequencer {4950private _size = 0;5152constructor(53private readonly _minDuration: number,54private readonly _maxOverallDelay: number55) {56super();57}5859override queue<T>(promiseTask: ITask<Promise<T>>): Promise<T> {6061this._size += 1;6263const noDelay = this._size * this._minDuration > this._maxOverallDelay;6465return super.queue(async () => {66try {67const p1 = promiseTask();68const p2 = noDelay69? Promise.resolve(undefined)70: timeout(this._minDuration, CancellationToken.None);7172const [result] = await Promise.all([p1, p2]);73return result;7475} finally {76this._size -= 1;77}78});79}80}8182function getCurrentAndNextStop(requestId: string, stopId: string | undefined, history: readonly IChatEditingSessionSnapshot[]) {83const snapshotIndex = history.findIndex(s => s.requestId === requestId);84if (snapshotIndex === -1) { return undefined; }85const snapshot = history[snapshotIndex];86const stopIndex = snapshot.stops.findIndex(s => s.stopId === stopId);87if (stopIndex === -1) { return undefined; }8889const current = snapshot.stops[stopIndex].entries;90const next = stopIndex < snapshot.stops.length - 191? snapshot.stops[stopIndex + 1].entries92: history[snapshotIndex + 1]?.stops[0].entries;939495if (!next) {96return undefined;97}9899return { current, next };100}101102export class ChatEditingSession extends Disposable implements IChatEditingSession {103private readonly _state = observableValue<ChatEditingSessionState>(this, ChatEditingSessionState.Initial);104private readonly _timeline: ChatEditingTimeline;105106/**107* Contains the contents of a file when the AI first began doing edits to it.108*/109private readonly _initialFileContents = new ResourceMap<string>();110111private readonly _entriesObs = observableValue<readonly AbstractChatEditingModifiedFileEntry[]>(this, []);112public get entries(): IObservable<readonly IModifiedFileEntry[]> {113this._assertNotDisposed();114return this._entriesObs;115}116117private _editorPane: MultiDiffEditor | undefined;118119get state(): IObservable<ChatEditingSessionState> {120return this._state;121}122123public readonly canUndo: IObservable<boolean>;124public readonly canRedo: IObservable<boolean>;125126private readonly _onDidDispose = new Emitter<void>();127get onDidDispose() {128this._assertNotDisposed();129return this._onDidDispose.event;130}131132constructor(133readonly chatSessionId: string,134readonly isGlobalEditingSession: boolean,135private _lookupExternalEntry: (uri: URI) => AbstractChatEditingModifiedFileEntry | undefined,136@IInstantiationService private readonly _instantiationService: IInstantiationService,137@IModelService private readonly _modelService: IModelService,138@ILanguageService private readonly _languageService: ILanguageService,139@ITextModelService private readonly _textModelService: ITextModelService,140@IBulkEditService public readonly _bulkEditService: IBulkEditService,141@IEditorGroupsService private readonly _editorGroupsService: IEditorGroupsService,142@IEditorService private readonly _editorService: IEditorService,143@IChatService private readonly _chatService: IChatService,144@INotebookService private readonly _notebookService: INotebookService,145@IAccessibilitySignalService private readonly _accessibilitySignalService: IAccessibilitySignalService,146) {147super();148this._timeline = _instantiationService.createInstance(ChatEditingTimeline);149this.canRedo = this._timeline.canRedo.map((hasHistory, reader) =>150hasHistory && this._state.read(reader) === ChatEditingSessionState.Idle);151this.canUndo = this._timeline.canUndo.map((hasHistory, reader) =>152hasHistory && this._state.read(reader) === ChatEditingSessionState.Idle);153154this._register(autorun(reader => {155const disabled = this._timeline.requestDisablement.read(reader);156this._chatService.getSession(this.chatSessionId)?.setDisabledRequests(disabled);157}));158}159160public async init(): Promise<void> {161const restoredSessionState = await this._instantiationService.createInstance(ChatEditingSessionStorage, this.chatSessionId).restoreState();162if (restoredSessionState) {163for (const [uri, content] of restoredSessionState.initialFileContents) {164this._initialFileContents.set(uri, content);165}166await this._restoreSnapshot(restoredSessionState.recentSnapshot, false);167transaction(tx => {168this._pendingSnapshot.set(restoredSessionState.pendingSnapshot, tx);169this._timeline.restoreFromState({ history: restoredSessionState.linearHistory, index: restoredSessionState.linearHistoryIndex }, tx);170this._state.set(ChatEditingSessionState.Idle, tx);171});172} else {173this._state.set(ChatEditingSessionState.Idle, undefined);174}175176this._register(autorun(reader => {177const entries = this.entries.read(reader);178entries.forEach(entry => {179entry.state.read(reader);180});181}));182}183184private _getEntry(uri: URI): AbstractChatEditingModifiedFileEntry | undefined {185uri = CellUri.parse(uri)?.notebook ?? uri;186return this._entriesObs.get().find(e => isEqual(e.modifiedURI, uri));187}188189public getEntry(uri: URI): IModifiedFileEntry | undefined {190return this._getEntry(uri);191}192193public readEntry(uri: URI, reader: IReader | undefined): IModifiedFileEntry | undefined {194uri = CellUri.parse(uri)?.notebook ?? uri;195return this._entriesObs.read(reader).find(e => isEqual(e.modifiedURI, uri));196}197198public storeState(): Promise<void> {199const storage = this._instantiationService.createInstance(ChatEditingSessionStorage, this.chatSessionId);200const timelineState = this._timeline.getStateForPersistence();201const state: StoredSessionState = {202initialFileContents: this._initialFileContents,203pendingSnapshot: this._pendingSnapshot.get(),204recentSnapshot: this._createSnapshot(undefined, undefined),205linearHistoryIndex: timelineState.index,206linearHistory: timelineState.history,207};208return storage.storeState(state);209}210211private _ensurePendingSnapshot() {212const prev = this._pendingSnapshot.get();213if (!prev) {214this._pendingSnapshot.set(this._createSnapshot(undefined, undefined), undefined);215}216}217218public getEntryDiffBetweenStops(uri: URI, requestId: string | undefined, stopId: string | undefined) {219return this._timeline.getEntryDiffBetweenStops(uri, requestId, stopId);220}221222public getEntryDiffBetweenRequests(uri: URI, startRequestId: string, stopRequestId: string) {223return this._timeline.getEntryDiffBetweenRequests(uri, startRequestId, stopRequestId);224}225226public createSnapshot(requestId: string, undoStop: string | undefined, makeEmpty = undoStop !== undefined): void {227this._timeline.pushSnapshot(228requestId,229undoStop,230makeEmpty ? ChatEditingTimeline.createEmptySnapshot(undoStop) : this._createSnapshot(requestId, undoStop),231);232}233234private _createSnapshot(requestId: string | undefined, stopId: string | undefined): IChatEditingSessionStop {235const entries = new ResourceMap<ISnapshotEntry>();236for (const entry of this._entriesObs.get()) {237entries.set(entry.modifiedURI, entry.createSnapshot(requestId, stopId));238}239return { stopId, entries };240}241242public getSnapshot(requestId: string, undoStop: string | undefined, snapshotUri: URI): ISnapshotEntry | undefined {243const stopRef = this._timeline.getSnapshotForRestore(requestId, undoStop);244const entries = stopRef?.stop.entries;245return entries && [...entries.values()].find((e) => isEqual(e.snapshotUri, snapshotUri));246}247248public async getSnapshotModel(requestId: string, undoStop: string | undefined, snapshotUri: URI): Promise<ITextModel | null> {249const snapshotEntry = this.getSnapshot(requestId, undoStop, snapshotUri);250if (!snapshotEntry) {251return null;252}253254return this._modelService.createModel(snapshotEntry.current, this._languageService.createById(snapshotEntry.languageId), snapshotUri, false);255}256257public getSnapshotUri(requestId: string, uri: URI, stopId: string | undefined): URI | undefined {258// This should be encapsulated in the timeline, but for now, fallback to legacy logic if needed.259// TODO: Move this logic into a timeline method if required by the design.260const timelineState = this._timeline.getStateForPersistence();261const stops = getCurrentAndNextStop(requestId, stopId, timelineState.history);262return stops?.next.get(uri)?.snapshotUri;263}264265/**266* A snapshot representing the state of the working set before a new request has been sent267*/268private _pendingSnapshot = observableValue<IChatEditingSessionStop | undefined>(this, undefined);269270public async restoreSnapshot(requestId: string | undefined, stopId: string | undefined): Promise<void> {271if (requestId !== undefined) {272const stopRef = this._timeline.getSnapshotForRestore(requestId, stopId);273if (stopRef) {274this._ensurePendingSnapshot();275await this._restoreSnapshot(stopRef.stop);276stopRef.apply();277}278} else {279const pendingSnapshot = this._pendingSnapshot.get();280if (!pendingSnapshot) {281return; // We don't have a pending snapshot that we can restore282}283this._pendingSnapshot.set(undefined, undefined);284await this._restoreSnapshot(pendingSnapshot, undefined);285}286}287288private async _restoreSnapshot({ entries }: IChatEditingSessionStop, restoreResolvedToDisk = true): Promise<void> {289290// Reset all the files which are modified in this session state291// but which are not found in the snapshot292for (const entry of this._entriesObs.get()) {293const snapshotEntry = entries.get(entry.modifiedURI);294if (!snapshotEntry) {295await entry.resetToInitialContent();296entry.dispose();297}298}299300const entriesArr: AbstractChatEditingModifiedFileEntry[] = [];301// Restore all entries from the snapshot302for (const snapshotEntry of entries.values()) {303const entry = await this._getOrCreateModifiedFileEntry(snapshotEntry.resource, restoreResolvedToDisk ? NotExistBehavior.Create : NotExistBehavior.Abort, snapshotEntry.telemetryInfo);304if (entry) {305const restoreToDisk = snapshotEntry.state === ModifiedFileEntryState.Modified || restoreResolvedToDisk;306await entry.restoreFromSnapshot(snapshotEntry, restoreToDisk);307entriesArr.push(entry);308}309}310311this._entriesObs.set(entriesArr, undefined);312}313314private _assertNotDisposed(): void {315if (this._state.get() === ChatEditingSessionState.Disposed) {316throw new BugIndicatingError(`Cannot access a disposed editing session`);317}318}319320async accept(...uris: URI[]): Promise<void> {321this._assertNotDisposed();322323if (uris.length === 0) {324await Promise.all(this._entriesObs.get().map(entry => entry.accept()));325}326327for (const uri of uris) {328const entry = this._entriesObs.get().find(e => isEqual(e.modifiedURI, uri));329if (entry) {330await entry.accept();331}332}333this._accessibilitySignalService.playSignal(AccessibilitySignal.editsKept, { allowManyInParallel: true });334}335336async reject(...uris: URI[]): Promise<void> {337this._assertNotDisposed();338339if (uris.length === 0) {340await Promise.all(this._entriesObs.get().map(entry => entry.reject()));341}342343for (const uri of uris) {344const entry = this._entriesObs.get().find(e => isEqual(e.modifiedURI, uri));345if (entry) {346await entry.reject();347}348}349this._accessibilitySignalService.playSignal(AccessibilitySignal.editsUndone, { allowManyInParallel: true });350}351352async show(previousChanges?: boolean): Promise<void> {353this._assertNotDisposed();354if (this._editorPane) {355if (this._editorPane.isVisible()) {356return;357} else if (this._editorPane.input) {358await this._editorGroupsService.activeGroup.openEditor(this._editorPane.input, { pinned: true, activation: EditorActivation.ACTIVATE });359return;360}361}362const input = MultiDiffEditorInput.fromResourceMultiDiffEditorInput({363multiDiffSource: getMultiDiffSourceUri(this, previousChanges),364label: localize('multiDiffEditorInput.name', "Suggested Edits")365}, this._instantiationService);366367this._editorPane = await this._editorGroupsService.activeGroup.openEditor(input, { pinned: true, activation: EditorActivation.ACTIVATE }) as MultiDiffEditor | undefined;368}369370private _stopPromise: Promise<void> | undefined;371372async stop(clearState = false): Promise<void> {373this._stopPromise ??= Promise.allSettled([this._performStop(), this.storeState()]).then(() => { });374await this._stopPromise;375if (clearState) {376await this._instantiationService.createInstance(ChatEditingSessionStorage, this.chatSessionId).clearState();377}378}379380private async _performStop(): Promise<void> {381// Close out all open files382const schemes = [AbstractChatEditingModifiedFileEntry.scheme, ChatEditingTextModelContentProvider.scheme];383await Promise.allSettled(this._editorGroupsService.groups.flatMap(async (g) => {384return g.editors.map(async (e) => {385if ((e instanceof MultiDiffEditorInput && e.initialResources?.some(r => r.originalUri && schemes.indexOf(r.originalUri.scheme) !== -1))386|| (e instanceof DiffEditorInput && e.original.resource && schemes.indexOf(e.original.resource.scheme) !== -1)) {387await g.closeEditor(e);388}389});390}));391}392393override dispose() {394this._assertNotDisposed();395396this._chatService.cancelCurrentRequestForSession(this.chatSessionId);397398dispose(this._entriesObs.get());399super.dispose();400this._state.set(ChatEditingSessionState.Disposed, undefined);401this._onDidDispose.fire();402this._onDidDispose.dispose();403}404405private _streamingEditLocks = new SequencerByKey</* URI */ string>();406407private get isDisposed() {408return this._state.get() === ChatEditingSessionState.Disposed;409}410411startStreamingEdits(resource: URI, responseModel: IChatResponseModel, inUndoStop: string | undefined): IStreamingEdits {412const completePromise = new DeferredPromise<void>();413const startPromise = new DeferredPromise<void>();414415// Sequence all edits made this this resource in this streaming edits instance,416// and also sequence the resource overall in the rare (currently invalid?) case417// that edits are made in parallel to the same resource,418const sequencer = new ThrottledSequencer(15, 1000);419sequencer.queue(() => startPromise.p);420421this._streamingEditLocks.queue(resource.toString(), async () => {422if (!this.isDisposed) {423await this._acceptStreamingEditsStart(responseModel, inUndoStop, resource);424}425426startPromise.complete();427return completePromise.p;428});429430431let didComplete = false;432433return {434pushText: (edits, isLastEdits) => {435sequencer.queue(async () => {436if (!this.isDisposed) {437await this._acceptEdits(resource, edits, isLastEdits, responseModel);438}439});440},441pushNotebookCellText: (cell, edits, isLastEdits) => {442sequencer.queue(async () => {443if (!this.isDisposed) {444await this._acceptEdits(cell, edits, isLastEdits, responseModel);445}446});447},448pushNotebook: (edits, isLastEdits) => {449sequencer.queue(async () => {450if (!this.isDisposed) {451await this._acceptEdits(resource, edits, isLastEdits, responseModel);452}453});454},455complete: () => {456if (didComplete) {457return;458}459460didComplete = true;461sequencer.queue(async () => {462if (!this.isDisposed) {463await this._acceptEdits(resource, [], true, responseModel);464await this._resolve(responseModel.requestId, inUndoStop, resource);465completePromise.complete();466}467});468},469};470}471472async undoInteraction(): Promise<void> {473const undo = this._timeline.getUndoSnapshot();474if (!undo) {475return;476}477this._ensurePendingSnapshot();478await this._restoreSnapshot(undo.stop);479undo.apply();480}481482async redoInteraction(): Promise<void> {483const redo = this._timeline.getRedoSnapshot();484const nextSnapshot = redo?.stop || this._pendingSnapshot.get();485if (!nextSnapshot) {486return;487}488await this._restoreSnapshot(nextSnapshot);489if (redo) {490redo.apply();491} else {492this._pendingSnapshot.set(undefined, undefined);493}494}495496private async _acceptStreamingEditsStart(responseModel: IChatResponseModel, undoStop: string | undefined, resource: URI) {497const entry = await this._getOrCreateModifiedFileEntry(resource, NotExistBehavior.Create, this._getTelemetryInfoForModel(responseModel));498transaction((tx) => {499this._state.set(ChatEditingSessionState.StreamingEdits, tx);500entry.acceptStreamingEditsStart(responseModel, tx);501this._timeline.ensureEditInUndoStopMatches(responseModel.requestId, undoStop, entry, false, tx);502});503}504505private async _acceptEdits(resource: URI, textEdits: (TextEdit | ICellEditOperation)[], isLastEdits: boolean, responseModel: IChatResponseModel): Promise<void> {506const entry = await this._getOrCreateModifiedFileEntry(resource, NotExistBehavior.Create, this._getTelemetryInfoForModel(responseModel));507await entry.acceptAgentEdits(resource, textEdits, isLastEdits, responseModel);508}509510private _getTelemetryInfoForModel(responseModel: IChatResponseModel): IModifiedEntryTelemetryInfo {511// Make these getters because the response result is not available when the file first starts to be edited512return new class implements IModifiedEntryTelemetryInfo {513get agentId() { return responseModel.agent?.id; }514get modelId() { return responseModel.request?.modelId; }515get modeId() { return responseModel.request?.modeInfo?.modeId; }516get command() { return responseModel.slashCommand?.name; }517get sessionId() { return responseModel.session.sessionId; }518get requestId() { return responseModel.requestId; }519get result() { return responseModel.result; }520get applyCodeBlockSuggestionId() { return responseModel.request?.modeInfo?.applyCodeBlockSuggestionId; }521522get feature(): string {523if (responseModel.session.initialLocation === ChatAgentLocation.Panel) {524return 'sideBarChat';525} else if (responseModel.session.initialLocation === ChatAgentLocation.Editor) {526return 'inlineChat';527}528return responseModel.session.initialLocation;529}530};531}532533private async _resolve(requestId: string, undoStop: string | undefined, resource: URI): Promise<void> {534const hasOtherTasks = Iterable.some(this._streamingEditLocks.keys(), k => k !== resource.toString());535if (!hasOtherTasks) {536this._state.set(ChatEditingSessionState.Idle, undefined);537}538539const entry = this._getEntry(resource);540if (!entry) {541return;542}543544this._timeline.ensureEditInUndoStopMatches(requestId, undoStop, entry, /* next= */ true, undefined);545return entry.acceptStreamingEditsEnd();546547}548549/**550* Retrieves or creates a modified file entry.551*552* @returns The modified file entry.553*/554private async _getOrCreateModifiedFileEntry(resource: URI, ifNotExists: NotExistBehavior.Create, telemetryInfo: IModifiedEntryTelemetryInfo): Promise<AbstractChatEditingModifiedFileEntry>;555private async _getOrCreateModifiedFileEntry(resource: URI, ifNotExists: NotExistBehavior, telemetryInfo: IModifiedEntryTelemetryInfo): Promise<AbstractChatEditingModifiedFileEntry | undefined>;556private async _getOrCreateModifiedFileEntry(resource: URI, ifNotExists: NotExistBehavior, telemetryInfo: IModifiedEntryTelemetryInfo): Promise<AbstractChatEditingModifiedFileEntry | undefined> {557558resource = CellUri.parse(resource)?.notebook ?? resource;559560const existingEntry = this._entriesObs.get().find(e => isEqual(e.modifiedURI, resource));561if (existingEntry) {562if (telemetryInfo.requestId !== existingEntry.telemetryInfo.requestId) {563existingEntry.updateTelemetryInfo(telemetryInfo);564}565return existingEntry;566}567568let entry: AbstractChatEditingModifiedFileEntry;569const existingExternalEntry = this._lookupExternalEntry(resource);570if (existingExternalEntry) {571entry = existingExternalEntry;572573if (telemetryInfo.requestId !== entry.telemetryInfo.requestId) {574entry.updateTelemetryInfo(telemetryInfo);575}576} else {577const initialContent = this._initialFileContents.get(resource);578// This gets manually disposed in .dispose() or in .restoreSnapshot()579const maybeEntry = await this._createModifiedFileEntry(resource, telemetryInfo, ifNotExists, initialContent);580if (!maybeEntry) {581return undefined;582}583entry = maybeEntry;584if (!initialContent) {585this._initialFileContents.set(resource, entry.initialContent);586}587}588589// If an entry is deleted e.g. reverting a created file,590// remove it from the entries and don't show it in the working set anymore591// so that it can be recreated e.g. through retry592const listener = entry.onDidDelete(() => {593const newEntries = this._entriesObs.get().filter(e => !isEqual(e.modifiedURI, entry.modifiedURI));594this._entriesObs.set(newEntries, undefined);595this._editorService.closeEditors(this._editorService.findEditors(entry.modifiedURI));596597if (!existingExternalEntry) {598// don't dispose entries that are not yours!599entry.dispose();600}601602this._store.delete(listener);603});604this._store.add(listener);605606const entriesArr = [...this._entriesObs.get(), entry];607this._entriesObs.set(entriesArr, undefined);608609return entry;610}611612private async _createModifiedFileEntry(resource: URI, telemetryInfo: IModifiedEntryTelemetryInfo, ifNotExists: NotExistBehavior.Create, initialContent: string | undefined): Promise<AbstractChatEditingModifiedFileEntry>;613private async _createModifiedFileEntry(resource: URI, telemetryInfo: IModifiedEntryTelemetryInfo, ifNotExists: NotExistBehavior, initialContent: string | undefined): Promise<AbstractChatEditingModifiedFileEntry | undefined>;614615private async _createModifiedFileEntry(resource: URI, telemetryInfo: IModifiedEntryTelemetryInfo, ifNotExists: NotExistBehavior, initialContent: string | undefined): Promise<AbstractChatEditingModifiedFileEntry | undefined> {616const multiDiffEntryDelegate = { collapse: (transaction: ITransaction | undefined) => this._collapse(resource, transaction) };617const notebookUri = CellUri.parse(resource)?.notebook || resource;618const doCreate = async (chatKind: ChatEditKind) => {619if (this._notebookService.hasSupportedNotebooks(notebookUri)) {620return await ChatEditingModifiedNotebookEntry.create(notebookUri, multiDiffEntryDelegate, telemetryInfo, chatKind, initialContent, this._instantiationService);621} else {622const ref = await this._textModelService.createModelReference(resource);623return this._instantiationService.createInstance(ChatEditingModifiedDocumentEntry, ref, multiDiffEntryDelegate, telemetryInfo, chatKind, initialContent);624}625};626627try {628return await doCreate(ChatEditKind.Modified);629} catch (err) {630if (ifNotExists === NotExistBehavior.Abort) {631return undefined;632}633634// this file does not exist yet, create it and try again635await this._bulkEditService.apply({ edits: [{ newResource: resource }] });636this._editorService.openEditor({ resource, options: { inactive: true, preserveFocus: true, pinned: true } });637638if (this._notebookService.hasSupportedNotebooks(notebookUri)) {639return await ChatEditingModifiedNotebookEntry.create(resource, multiDiffEntryDelegate, telemetryInfo, ChatEditKind.Created, initialContent, this._instantiationService);640} else {641return await doCreate(ChatEditKind.Created);642}643}644}645646private _collapse(resource: URI, transaction: ITransaction | undefined) {647const multiDiffItem = this._editorPane?.findDocumentDiffItem(resource);648if (multiDiffItem) {649this._editorPane?.viewModel?.items.get().find((documentDiffItem) =>650isEqual(documentDiffItem.originalUri, multiDiffItem.originalUri) &&651isEqual(documentDiffItem.modifiedUri, multiDiffItem.modifiedUri))652?.collapsed.set(true, transaction);653}654}655}656657658