From e41b3d31fa484dfefc23f1aea3d9032481a8b551 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Fri, 22 Nov 2024 11:19:11 +1100 Subject: [PATCH 1/7] Separate IModifiedFileEntry into text and notebook files --- .../chatEditingModifiedFileEntry.ts | 149 ++++++++++++++---- .../browser/chatEditing/chatEditingSession.ts | 102 +++++------- .../chat/browser/chatEditorController.ts | 8 +- .../contrib/chat/common/chatEditingService.ts | 92 ++++++++++- .../chatEdit/notebookChatEditController.ts | 4 +- .../notebookOriginalModelRefFactory.ts | 8 +- .../contrib/chatEdit/notebookSynchronizer.ts | 25 ++- 7 files changed, 266 insertions(+), 122 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts index defe157b7b7fc..997688b8aa4f9 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts @@ -4,13 +4,16 @@ *--------------------------------------------------------------------------------------------*/ import { RunOnceScheduler, timeout } from '../../../../../base/common/async.js'; +import { VSBuffer } from '../../../../../base/common/buffer.js'; import { Emitter } from '../../../../../base/common/event.js'; +import { StringSHA1 } from '../../../../../base/common/hash.js'; import { Disposable, IReference, toDisposable } from '../../../../../base/common/lifecycle.js'; import { IObservable, ITransaction, observableValue, transaction } from '../../../../../base/common/observable.js'; +import { joinPath } from '../../../../../base/common/resources.js'; import { themeColorFromId } from '../../../../../base/common/themables.js'; import { URI } from '../../../../../base/common/uri.js'; import { EditOperation, ISingleEditOperation } from '../../../../../editor/common/core/editOperation.js'; -import { OffsetEdit } from '../../../../../editor/common/core/offsetEdit.js'; +import { ISingleOffsetEdit, OffsetEdit } from '../../../../../editor/common/core/offsetEdit.js'; import { IDocumentDiff, nullDocumentDiff } from '../../../../../editor/common/diff/documentDiffProvider.js'; import { TextEdit } from '../../../../../editor/common/languages.js'; import { ILanguageService } from '../../../../../editor/common/languages/language.js'; @@ -23,18 +26,20 @@ import { IModelService } from '../../../../../editor/common/services/model.js'; import { IResolvedTextEditorModel, ITextModelService } from '../../../../../editor/common/services/resolverService.js'; import { IModelContentChangedEvent } from '../../../../../editor/common/textModelEvents.js'; import { localize } from '../../../../../nls.js'; +import { IEnvironmentService } from '../../../../../platform/environment/common/environment.js'; import { IFileService } from '../../../../../platform/files/common/files.js'; +import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; import { editorSelectionBackground } from '../../../../../platform/theme/common/colorRegistry.js'; import { IUndoRedoService } from '../../../../../platform/undoRedo/common/undoRedo.js'; +import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; import { SaveReason } from '../../../../common/editor.js'; import { IResolvedTextFileEditorModel, stringToSnapshot } from '../../../../services/textfile/common/textfiles.js'; -import { IChatAgentResult } from '../../common/chatAgents.js'; -import { ChatEditKind, IModifiedFileEntry, WorkingSetEntryState } from '../../common/chatEditingService.js'; +import { ChatEditKind, IModifiedEntryTelemetryInfo, IModifiedTextFileEntry, ITextSnapshotEntry, ITextSnapshotEntryDTO, STORAGE_CONTENTS_FOLDER, WorkingSetEntryState } from '../../common/chatEditingService.js'; import { IChatService } from '../../common/chatService.js'; import { ChatEditingSnapshotTextModelContentProvider, ChatEditingTextModelContentProvider } from './chatEditingTextModelContentProviders.js'; -export class ChatEditingModifiedFileEntry extends Disposable implements IModifiedFileEntry { - +export class ChatEditingModifiedFileEntry extends Disposable implements IModifiedTextFileEntry { + public readonly kind = 'text'; public static readonly scheme = 'modified-file-entry'; private static lastEntryId = 0; public readonly entryId = `${ChatEditingModifiedFileEntry.scheme}::${++ChatEditingModifiedFileEntry.lastEntryId}`; @@ -129,6 +134,7 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie @IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService, @IUndoRedoService private readonly _undoRedoService: IUndoRedoService, @IFileService private readonly _fileService: IFileService, + @IInstantiationService private readonly instantiationService: IInstantiationService, ) { super(); if (kind === ChatEditKind.Created) { @@ -179,20 +185,12 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie this._telemetryInfo = telemetryInfo; } - createSnapshot(requestId: string | undefined): ISnapshotEntry { + createSnapshot(requestId: string | undefined): ITextSnapshotEntry { this._isFirstEditAfterStartOrSnapshot = true; - return { - resource: this.modifiedURI, - languageId: this.modifiedModel.getLanguageId(), - snapshotUri: ChatEditingSnapshotTextModelContentProvider.getSnapshotFileURI(requestId, this.modifiedURI.path), - original: this.originalModel.getValue(), - current: this.modifiedModel.getValue(), - originalToCurrentEdit: this._edit, - state: this.state.get(), - telemetryInfo: this._telemetryInfo - }; - } - restoreFromSnapshot(snapshot: ISnapshotEntry) { + return TextSnapshotEntry.create(this, this._telemetryInfo.sessionId, requestId, this._edit, this.instantiationService); + } + + restoreFromSnapshot(snapshot: ITextSnapshotEntry) { this._stateObs.set(snapshot.state, undefined); this.docSnapshot.setValue(snapshot.original); this._setDocValue(snapshot.current); @@ -425,21 +423,104 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie } } -export interface IModifiedEntryTelemetryInfo { - readonly agentId: string | undefined; - readonly command: string | undefined; - readonly sessionId: string; - readonly requestId: string; - readonly result: IChatAgentResult | undefined; -} -export interface ISnapshotEntry { - readonly resource: URI; - readonly languageId: string; - readonly snapshotUri: URI; - readonly original: string; - readonly current: string; - readonly originalToCurrentEdit: OffsetEdit; - readonly state: WorkingSetEntryState; - telemetryInfo: IModifiedEntryTelemetryInfo; +export class TextSnapshotEntry implements ITextSnapshotEntry { + public readonly kind = 'text'; + constructor( + public readonly languageId: string, + public readonly original: string, + public readonly current: string, + public readonly originalToCurrentEdit: OffsetEdit, + public readonly resource: URI, + public readonly snapshotUri: URI, + public readonly state: WorkingSetEntryState, + public readonly telemetryInfo: IModifiedEntryTelemetryInfo, + @IFileService private readonly _fileService: IFileService, + @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService, + @IEnvironmentService private readonly _environmentService: IEnvironmentService, + ) { + } + + public static create(entry: IModifiedTextFileEntry, chatSessionId: string, + requestId: string | undefined, + edit: OffsetEdit, instantiationService: IInstantiationService): TextSnapshotEntry { + + return instantiationService.createInstance(TextSnapshotEntry, + entry.modifiedModel.getLanguageId(), + entry.originalModel.getValue(), + entry.modifiedModel.getValue(), + edit, + entry.modifiedURI, + ChatEditingSnapshotTextModelContentProvider.getSnapshotFileURI(requestId, entry.modifiedURI.path), + entry.state.get(), + entry.telemetryInfo); + } + + public static async deserialize(entry: ITextSnapshotEntryDTO, chatSessionId: string, instantiationService: IInstantiationService): Promise { + return instantiationService.invokeFunction(async accessor => { + const workspaceContextService = accessor.get(IWorkspaceContextService); + const environmentService = accessor.get(IEnvironmentService); + const fileService = accessor.get(IFileService); + const storageLocation = TextSnapshotEntry._getStorageLocation(chatSessionId, workspaceContextService, environmentService); + + const [original, current] = await Promise.all([ + TextSnapshotEntry.getFileContent(entry.originalHash, fileService, storageLocation), + TextSnapshotEntry.getFileContent(entry.currentHash, fileService, storageLocation) + ]); + + return instantiationService.createInstance(TextSnapshotEntry, + entry.languageId, + original, + current, + OffsetEdit.fromJson(entry.originalToCurrentEdit), + URI.parse(entry.resource), + URI.parse(entry.snapshotUri), + entry.state, + { requestId: entry.telemetryInfo.requestId, agentId: entry.telemetryInfo.agentId, command: entry.telemetryInfo.command, sessionId: chatSessionId, result: undefined } + ); + }); + } + + async serialize(): Promise { + const fileContents = new Map(); + const serialized = { + kind: 'text', + resource: this.resource.toString(), + languageId: this.languageId, + originalHash: this.computeContentHash(this.original), + currentHash: this.computeContentHash(this.current), + originalToCurrentEdit: this.originalToCurrentEdit.edits.map(edit => ({ pos: edit.replaceRange.start, len: edit.replaceRange.length, txt: edit.newText } satisfies ISingleOffsetEdit)), + state: this.state, + snapshotUri: this.snapshotUri.toString(), + telemetryInfo: { requestId: this.telemetryInfo.requestId, agentId: this.telemetryInfo.agentId, command: this.telemetryInfo.command } + } satisfies ITextSnapshotEntryDTO; + + const storageFolder = TextSnapshotEntry._getStorageLocation(this.telemetryInfo.sessionId, this._workspaceContextService, this._environmentService); + const contentsFolder = URI.joinPath(storageFolder, STORAGE_CONTENTS_FOLDER); + + await Promise.all(Array.from(fileContents.entries()).map(async ([hash, content]) => { + const file = joinPath(contentsFolder, hash); + if (!(await this._fileService.exists(file))) { + await this._fileService.writeFile(joinPath(contentsFolder, hash), VSBuffer.fromString(content)); + } + })); + + return serialized; + } + private computeContentHash(content: string): string { + const shaComputer = new StringSHA1(); + shaComputer.update(content); + return shaComputer.digest().substring(0, 7); + } + private static _getStorageLocation(chatSessionId: string, + _workspaceContextService: IWorkspaceContextService, + _environmentService: IEnvironmentService, + ): URI { + const workspaceId = _workspaceContextService.getWorkspace().id; + return joinPath(_environmentService.workspaceStorageHome, workspaceId, 'chatEditingSessions', chatSessionId); + } + private static getFileContent(hash: string, fileService: IFileService, storageLocation: URI) { + return fileService.readFile(joinPath(storageLocation, STORAGE_CONTENTS_FOLDER, hash)).then(content => content.value.toString()); + } } + diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts index d82246020c116..01aee33d61bc7 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts @@ -30,23 +30,19 @@ import { IEditorService } from '../../../../services/editor/common/editorService import { MultiDiffEditor } from '../../../multiDiffEditor/browser/multiDiffEditor.js'; import { MultiDiffEditorInput } from '../../../multiDiffEditor/browser/multiDiffEditorInput.js'; import { ChatAgentLocation, IChatAgentService } from '../../common/chatAgents.js'; -import { ChatEditingSessionChangeType, ChatEditingSessionState, ChatEditKind, IChatEditingSession, WorkingSetDisplayMetadata, WorkingSetEntryRemovalReason, WorkingSetEntryState } from '../../common/chatEditingService.js'; +import { ChatEditingSessionChangeType, ChatEditingSessionState, ChatEditKind, IChatEditingSession, IModifiedEntryTelemetryInfo, IModifiedFileEntry, ISnapshotEntry, ISnapshotEntryDTO, ITextSnapshotEntry, STORAGE_CONTENTS_FOLDER, STORAGE_STATE_FILE, WorkingSetDisplayMetadata, WorkingSetEntryRemovalReason, WorkingSetEntryState } from '../../common/chatEditingService.js'; import { IChatResponseModel } from '../../common/chatModel.js'; import { ChatEditingMultiDiffSourceResolver } from './chatEditingService.js'; -import { ChatEditingModifiedFileEntry, IModifiedEntryTelemetryInfo, ISnapshotEntry } from './chatEditingModifiedFileEntry.js'; +import { ChatEditingModifiedFileEntry, TextSnapshotEntry } from './chatEditingModifiedFileEntry.js'; import { ChatEditingTextModelContentProvider } from './chatEditingTextModelContentProviders.js'; import { Schemas } from '../../../../../base/common/network.js'; import { isEqual, joinPath } from '../../../../../base/common/resources.js'; import { StringSHA1 } from '../../../../../base/common/hash.js'; import { IEnvironmentService } from '../../../../../platform/environment/common/environment.js'; import { VSBuffer } from '../../../../../base/common/buffer.js'; -import { IOffsetEdit, ISingleOffsetEdit, OffsetEdit } from '../../../../../editor/common/core/offsetEdit.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; import { IChatService } from '../../common/chatService.js'; -const STORAGE_CONTENTS_FOLDER = 'contents'; -const STORAGE_STATE_FILE = 'state.json'; - export class ChatEditingSession extends Disposable implements IChatEditingSession { private readonly _state = observableValue(this, ChatEditingSessionState.Initial); private readonly _linearHistory = observableValue(this, []); @@ -58,8 +54,8 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio private readonly _initialFileContents = new ResourceMap(); private readonly _filesToSkipCreating = new ResourceSet(); - private readonly _entriesObs = observableValue(this, []); - public get entries(): IObservable { + private readonly _entriesObs = observableValue(this, []); + public get entries(): IObservable { this._assertNotDisposed(); return this._entriesObs; } @@ -233,9 +229,11 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio for (const [file, state] of this._workingSet) { workingSet.set(file, state); } - const entries = new ResourceMap(); + const entries = new ResourceMap(); for (const entry of this._entriesObs.get()) { - entries.set(entry.modifiedURI, entry.createSnapshot(requestId)); + if (entry.kind !== 'notebook') { + entries.set(entry.modifiedURI, entry.createSnapshot(requestId)); + } } return { requestId, @@ -251,7 +249,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio } const snapshotEntry = [...entries.values()].find((e) => isEqual(e.snapshotUri, snapshotUri)); - if (!snapshotEntry) { + if (!snapshotEntry || snapshotEntry.kind !== 'text') { return null; } @@ -303,11 +301,17 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio } } - const entriesArr: ChatEditingModifiedFileEntry[] = []; + const entriesArr: IModifiedFileEntry[] = []; // Restore all entries from the snapshot for (const snapshotEntry of snapshot.entries.values()) { const entry = await this._getOrCreateModifiedFileEntry(snapshotEntry.resource, snapshotEntry.telemetryInfo); - entry.restoreFromSnapshot(snapshotEntry); + if (entry.kind === 'text' && snapshotEntry.kind === 'text') { + entry.restoreFromSnapshot(snapshotEntry); + } else if (entry.kind === 'notebook' && snapshotEntry.kind === 'notebook') { + // entry.restoreFromSnapshot(snapshotEntry); + } else { + throw new Error('Unexpected snapshot entry kind'); + } entriesArr.push(entry); } @@ -448,7 +452,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio this._assertNotDisposed(); const entry = this._entriesObs.get().find(e => e.entryId === documentId); - return entry?.originalModel ?? null; + return entry?.kind === 'text' ? entry?.originalModel ?? null : null; } acceptStreamingEditsStart(): void { @@ -581,7 +585,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio this._onDidChange.fire(ChatEditingSessionChangeType.Other); } - private async _getOrCreateModifiedFileEntry(resource: URI, responseModel: IModifiedEntryTelemetryInfo): Promise { + private async _getOrCreateModifiedFileEntry(resource: URI, responseModel: IModifiedEntryTelemetryInfo): Promise { const existingEntry = this._entriesObs.get().find(e => isEqual(e.modifiedURI, resource)); if (existingEntry) { if (responseModel.requestId !== existingEntry.telemetryInfo.requestId) { @@ -592,7 +596,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio const initialContent = this._initialFileContents.get(resource); // This gets manually disposed in .dispose() or in .restoreSnapshot() const entry = await this._createModifiedFileEntry(resource, responseModel, false, initialContent); - if (!initialContent) { + if (!initialContent && entry.kind === 'text') { this._initialFileContents.set(resource, entry.initialContent); } // If an entry is deleted e.g. reverting a created file, @@ -655,10 +659,12 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio return result; }; const deserializeChatEditingSessionSnapshot = async (snapshot: IChatEditingSessionSnapshotDTO) => { - const entriesMap = new ResourceMap(); + const entriesMap = new ResourceMap(); for (const entryDTO of snapshot.entries) { const entry = await deserializeSnapshotEntry(entryDTO); - entriesMap.set(entry.resource, entry); + if (entry) { + entriesMap.set(entry.resource, entry); + } } return ({ requestId: snapshot.requestId, @@ -666,17 +672,11 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio entries: entriesMap } satisfies IChatEditingSessionSnapshot); }; - const deserializeSnapshotEntry = async (entry: ISnapshotEntryDTO) => { - return { - resource: URI.parse(entry.resource), - languageId: entry.languageId, - original: await getFileContent(entry.originalHash), - current: await getFileContent(entry.currentHash), - originalToCurrentEdit: OffsetEdit.fromJson(entry.originalToCurrentEdit), - state: entry.state, - snapshotUri: URI.parse(entry.snapshotUri), - telemetryInfo: { requestId: entry.telemetryInfo.requestId, agentId: entry.telemetryInfo.agentId, command: entry.telemetryInfo.command, sessionId: this.chatSessionId, result: undefined } - } satisfies ISnapshotEntry; + const deserializeSnapshotEntry = (entry: ISnapshotEntryDTO) => { + if (entry.kind === 'notebook') { + return; + } + return TextSnapshotEntry.deserialize(entry, this.chatSessionId, this._instantiationService,); }; try { const stateFilePath = joinPath(storageLocation, STORAGE_STATE_FILE); @@ -750,35 +750,28 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio const serializeResourceMap = (resourceMap: ResourceMap, serialize: (value: T) => any): ResourceMapDTO => { return Array.from(resourceMap.entries()).map(([resourceURI, value]) => [resourceURI.toString(), serialize(value)]); }; - const serializeChatEditingSessionSnapshot = (snapshot: IChatEditingSessionSnapshot) => { + const serializeChatEditingSessionSnapshot = async (snapshot: IChatEditingSessionSnapshot) => { return ({ requestId: snapshot.requestId, workingSet: serializeResourceMap(snapshot.workingSet, value => value), - entries: Array.from(snapshot.entries.values()).map(serializeSnapshotEntry) + entries: await Promise.all(Array.from(snapshot.entries.values()).map(e => e.serialize())) } satisfies IChatEditingSessionSnapshotDTO); }; - const serializeSnapshotEntry = (entry: ISnapshotEntry) => { - return { - resource: entry.resource.toString(), - languageId: entry.languageId, - originalHash: addFileContent(entry.original), - currentHash: addFileContent(entry.current), - originalToCurrentEdit: entry.originalToCurrentEdit.edits.map(edit => ({ pos: edit.replaceRange.start, len: edit.replaceRange.length, txt: edit.newText } satisfies ISingleOffsetEdit)), - state: entry.state, - snapshotUri: entry.snapshotUri.toString(), - telemetryInfo: { requestId: entry.telemetryInfo.requestId, agentId: entry.telemetryInfo.agentId, command: entry.telemetryInfo.command } - } satisfies ISnapshotEntryDTO; - }; try { + const [linearHistory, pendingSnapshot, recentSnapshot] = await Promise.all([ + Promise.all(this._linearHistory.get().map(serializeChatEditingSessionSnapshot)), + this._pendingSnapshot ? serializeChatEditingSessionSnapshot(this._pendingSnapshot) : undefined, + serializeChatEditingSessionSnapshot(this._createSnapshot(undefined)) + ]); const data = { version: STORAGE_VERSION, sessionId: this.chatSessionId, - linearHistory: this._linearHistory.get().map(serializeChatEditingSessionSnapshot), + linearHistory, linearHistoryIndex: this._linearHistoryIndex.get(), initialFileContents: serializeResourceMap(this._initialFileContents, value => addFileContent(value)), - pendingSnapshot: this._pendingSnapshot ? serializeChatEditingSessionSnapshot(this._pendingSnapshot) : undefined, - recentSnapshot: serializeChatEditingSessionSnapshot(this._createSnapshot(undefined)), + pendingSnapshot, + recentSnapshot, filesToSkipCreating: Array.from(this._filesToSkipCreating.keys()).map(uri => uri.toString()), } satisfies IChatEditingSessionDTO; @@ -819,23 +812,6 @@ interface IChatEditingSessionSnapshotDTO { readonly entries: ISnapshotEntryDTO[]; } -interface ISnapshotEntryDTO { - readonly resource: string; - readonly languageId: string; - readonly originalHash: string; - readonly currentHash: string; - readonly originalToCurrentEdit: IOffsetEdit; - readonly state: WorkingSetEntryState; - readonly snapshotUri: string; - readonly telemetryInfo: IModifiedEntryTelemetryInfoDTO; -} - -interface IModifiedEntryTelemetryInfoDTO { - readonly requestId: string; - readonly agentId?: string; - readonly command?: string; -} - type ResourceMapDTO = [string, T][]; const STORAGE_VERSION = 1; diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorController.ts b/src/vs/workbench/contrib/chat/browser/chatEditorController.ts index fb684a083b59d..79d060d48132e 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditorController.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditorController.ts @@ -24,7 +24,7 @@ import { InlineDecoration, InlineDecorationType } from '../../../../editor/commo import { localize } from '../../../../nls.js'; import { IContextKey, IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; import { minimapGutterAddedBackground, minimapGutterDeletedBackground, minimapGutterModifiedBackground, overviewRulerAddedForeground, overviewRulerDeletedForeground, overviewRulerModifiedForeground } from '../../scm/browser/dirtydiffDecorator.js'; -import { ChatEditingSessionState, IChatEditingService, IModifiedFileEntry, WorkingSetEntryState } from '../common/chatEditingService.js'; +import { ChatEditingSessionState, IChatEditingService, IModifiedTextFileEntry, WorkingSetEntryState } from '../common/chatEditingService.js'; import { Event } from '../../../../base/common/event.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { MenuId } from '../../../../platform/actions/common/actions.js'; @@ -81,7 +81,7 @@ export class ChatEditorController extends Disposable implements IEditorContribut const model = modelObs.read(r); const session = this._chatEditingService.currentEditingSessionObs.read(r); - const entry = session?.entries.read(r).find(e => isEqual(e.modifiedURI, model?.uri)); + const entry = session?.entries.read(r).find(e => isEqual(e.modifiedURI, model?.uri) && e.kind === 'text') as IModifiedTextFileEntry | undefined; if (!entry || entry.state.read(r) !== WorkingSetEntryState.Modified) { this._clearRendering(); @@ -144,7 +144,7 @@ export class ChatEditorController extends Disposable implements IEditorContribut this._ctxHasEditorModification.reset(); } - private _updateWithDiff(entry: IModifiedFileEntry, diff: IDocumentDiff | null | undefined): void { + private _updateWithDiff(entry: IModifiedTextFileEntry, diff: IDocumentDiff | null | undefined): void { if (!diff) { this._clearRendering(); return; @@ -491,7 +491,7 @@ class DiffHunkWidget implements IOverlayWidget { constructor( - readonly entry: IModifiedFileEntry, + readonly entry: IModifiedTextFileEntry, private readonly _undoEdits: ISingleEditOperation[], private readonly _versionId: number, private readonly _editor: ICodeEditor, diff --git a/src/vs/workbench/contrib/chat/common/chatEditingService.ts b/src/vs/workbench/contrib/chat/common/chatEditingService.ts index 74f7d6cd270d3..520f2bc2a0f4c 100644 --- a/src/vs/workbench/contrib/chat/common/chatEditingService.ts +++ b/src/vs/workbench/contrib/chat/common/chatEditingService.ts @@ -9,14 +9,21 @@ import { IDisposable } from '../../../../base/common/lifecycle.js'; import { ResourceMap } from '../../../../base/common/map.js'; import { IObservable, ITransaction } from '../../../../base/common/observable.js'; import { URI } from '../../../../base/common/uri.js'; +import { IOffsetEdit, OffsetEdit } from '../../../../editor/common/core/offsetEdit.js'; import { IDocumentDiff } from '../../../../editor/common/diff/documentDiffProvider.js'; import { TextEdit } from '../../../../editor/common/languages.js'; import { ITextModel } from '../../../../editor/common/model.js'; import { localize } from '../../../../nls.js'; import { RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; +import { ICellEditOperation } from '../../notebook/common/notebookCommon.js'; +import { IChatAgentResult } from './chatAgents.js'; import { IChatResponseModel } from './chatModel.js'; +export const STORAGE_CONTENTS_FOLDER = 'contents'; +export const STORAGE_STATE_FILE = 'state.json'; + + export const IChatEditingService = createDecorator('chatEditingService'); export interface IChatEditingService { @@ -110,19 +117,100 @@ export const enum ChatEditingSessionChangeType { Other, } -export interface IModifiedFileEntry { + +export interface IBaseSnapshotEntry { + readonly resource: URI; + readonly snapshotUri: URI; + readonly state: WorkingSetEntryState; + telemetryInfo: IModifiedEntryTelemetryInfo; +} + +export interface ITextSnapshotEntry extends IBaseSnapshotEntry { + kind: 'text'; + readonly languageId: string; + readonly original: string; + readonly current: string; + readonly originalToCurrentEdit: OffsetEdit; + serialize(): Promise; +} + +export interface INotebookSnapshotEntry extends IBaseSnapshotEntry { + kind: 'notebook'; + serialize(): Promise; +} +export type ISnapshotEntry = ITextSnapshotEntry | INotebookSnapshotEntry; + +export interface IBaseSnapshotEntryDTO { + readonly resource: string; + readonly state: WorkingSetEntryState; + readonly snapshotUri: string; + readonly telemetryInfo: IModifiedEntryTelemetryInfoDTO; +} +export interface ITextSnapshotEntryDTO extends IBaseSnapshotEntryDTO { + readonly kind: 'text'; + readonly languageId: string; + readonly originalHash: string; + readonly currentHash: string; + readonly originalToCurrentEdit: IOffsetEdit; +} + +export interface INotebookSnapshotEntryDTO extends IBaseSnapshotEntryDTO { + readonly kind: 'notebook'; +} +export type ISnapshotEntryDTO = ITextSnapshotEntryDTO | INotebookSnapshotEntryDTO; + +interface IModifiedEntryTelemetryInfoDTO { + readonly requestId: string; + readonly agentId?: string; + readonly command?: string; +} + + +export interface IModifiedEntryTelemetryInfo { + readonly agentId: string | undefined; + readonly command: string | undefined; + readonly sessionId: string; + readonly requestId: string; + readonly result: IChatAgentResult | undefined; +} + +interface IModifiedAnyFileEntry extends IDisposable { + readonly entryId: string; readonly originalURI: URI; - readonly originalModel: ITextModel; readonly modifiedURI: URI; + readonly onDidDelete: Event; readonly state: IObservable; readonly isCurrentlyBeingModified: IObservable; readonly rewriteRatio: IObservable; readonly diffInfo: IObservable; readonly lastModifyingRequestId: string; + readonly telemetryInfo: IModifiedEntryTelemetryInfo; accept(transaction: ITransaction | undefined): Promise; reject(transaction: ITransaction | undefined): Promise; + acceptAgentEdits(textEdits: TextEdit[], isLastEdits: boolean): void; + acceptStreamingEditsStart(tx: ITransaction): void; + acceptStreamingEditsEnd(tx: ITransaction): void; + updateTelemetryInfo(telemetryInfo: IModifiedEntryTelemetryInfo): void; + resetToInitialValue(): void; } +export interface IModifiedTextFileEntry extends IModifiedAnyFileEntry { + readonly kind: 'text'; + readonly originalModel: ITextModel; + readonly modifiedModel: ITextModel; + createSnapshot(requestId: string | undefined): ITextSnapshotEntry; + restoreFromSnapshot(snapshot: ITextSnapshotEntry): void; +} + +export interface IModifiedNotebookFileEntry extends IModifiedAnyFileEntry { + readonly kind: 'notebook'; + acceptAgentEdits(textEdits: (TextEdit | ICellEditOperation)[], isLastEdits: boolean): void; + createSnapshot(requestId: string | undefined): INotebookSnapshotEntry; + restoreFromSnapshot(snapshot: INotebookSnapshotEntry): void; +} + +export type IModifiedFileEntry = IModifiedTextFileEntry | IModifiedNotebookFileEntry; + export interface IChatEditingSessionStream { textEdits(resource: URI, textEdits: TextEdit[], isLastEdits: boolean, responseModel: IChatResponseModel): void; } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookChatEditController.ts b/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookChatEditController.ts index 03c38c2e40b87..5e5aafb957afb 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookChatEditController.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookChatEditController.ts @@ -6,7 +6,7 @@ import { isEqual } from '../../../../../../base/common/resources.js'; import { Disposable, dispose, IReference, toDisposable } from '../../../../../../base/common/lifecycle.js'; import { autorun, derived, derivedWithStore, observableFromEvent, observableValue } from '../../../../../../base/common/observable.js'; -import { IChatEditingService, WorkingSetEntryState } from '../../../../chat/common/chatEditingService.js'; +import { IChatEditingService, IModifiedTextFileEntry, WorkingSetEntryState } from '../../../../chat/common/chatEditingService.js'; import { NotebookTextModel } from '../../../common/model/notebookTextModel.js'; import { INotebookEditor, INotebookEditorContribution } from '../../notebookBrowser.js'; import { IInstantiationService } from '../../../../../../platform/instantiation/common/instantiation.js'; @@ -86,7 +86,7 @@ class NotebookChatEditorController extends Disposable { if (!model || !session) { return; } - return session.entries.read(r).find(e => isEqual(e.modifiedURI, model.uri)); + return session.entries.read(r).find(e => isEqual(e.modifiedURI, model.uri) && e.kind === 'text') as IModifiedTextFileEntry | undefined; }).recomputeInitiallyAndOnChange(this._store); diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookOriginalModelRefFactory.ts b/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookOriginalModelRefFactory.ts index ab45778c08967..b5e6743a3e169 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookOriginalModelRefFactory.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookOriginalModelRefFactory.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { AsyncReferenceCollection, IReference, ReferenceCollection } from '../../../../../../base/common/lifecycle.js'; -import { IModifiedFileEntry } from '../../../../chat/common/chatEditingService.js'; +import { IModifiedTextFileEntry } from '../../../../chat/common/chatEditingService.js'; import { INotebookService } from '../../../common/notebookService.js'; import { bufferToStream, VSBuffer } from '../../../../../../base/common/buffer.js'; import { NotebookTextModel } from '../../../common/model/notebookTextModel.js'; @@ -15,7 +15,7 @@ export const INotebookOriginalModelReferenceFactory = createDecorator>; + getOrCreate(fileEntry: IModifiedTextFileEntry, viewType: string): Promise>; } @@ -25,7 +25,7 @@ export class OriginalNotebookModelReferenceCollection extends ReferenceCollectio super(); } - protected override async createReferencedObject(key: string, fileEntry: IModifiedFileEntry, viewType: string): Promise { + protected override async createReferencedObject(key: string, fileEntry: IModifiedTextFileEntry, viewType: string): Promise { this.modelsToDispose.delete(key); const uri = fileEntry.originalURI; const model = this.notebookService.getNotebookTextModel(uri); @@ -83,7 +83,7 @@ export class NotebookOriginalModelReferenceFactory implements INotebookOriginalM constructor(@IInstantiationService private readonly instantiationService: IInstantiationService) { } - getOrCreate(fileEntry: IModifiedFileEntry, viewType: string): Promise> { + getOrCreate(fileEntry: IModifiedTextFileEntry, viewType: string): Promise> { return this.asyncModelCollection.acquire(fileEntry.originalURI.toString(), fileEntry, viewType); } } diff --git a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookSynchronizer.ts b/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookSynchronizer.ts index d2a1325348451..bbab4b6ac417b 100644 --- a/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookSynchronizer.ts +++ b/src/vs/workbench/contrib/notebook/browser/contrib/chatEdit/notebookSynchronizer.ts @@ -5,7 +5,7 @@ import { isEqual } from '../../../../../../base/common/resources.js'; import { Disposable, IReference, ReferenceCollection } from '../../../../../../base/common/lifecycle.js'; -import { IChatEditingService, IModifiedFileEntry, WorkingSetEntryState } from '../../../../chat/common/chatEditingService.js'; +import { IChatEditingService, IModifiedTextFileEntry, WorkingSetEntryState } from '../../../../chat/common/chatEditingService.js'; import { INotebookService } from '../../../common/notebookService.js'; import { bufferToStream, VSBuffer } from '../../../../../../base/common/buffer.js'; import { NotebookTextModel } from '../../../common/model/notebookTextModel.js'; @@ -13,7 +13,6 @@ import { raceCancellation, ThrottledDelayer } from '../../../../../../base/commo import { CellDiffInfo, computeDiff, prettyChanges } from '../../diff/notebookDiffViewModel.js'; import { CancellationToken, CancellationTokenSource } from '../../../../../../base/common/cancellation.js'; import { INotebookEditorWorkerService } from '../../../common/services/notebookWorkerService.js'; -import { ChatEditingModifiedFileEntry } from '../../../../chat/browser/chatEditing/chatEditingModifiedFileEntry.js'; import { CellEditType, ICellDto2, ICellReplaceEdit, NotebookData, NotebookSetting } from '../../../common/notebookCommon.js'; import { URI } from '../../../../../../base/common/uri.js'; import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; @@ -91,7 +90,7 @@ export class NotebookModelSynchronizer extends Disposable { if (!session) { return; } - return session.entries.read(r).find(e => isEqual(e.modifiedURI, model.uri)); + return session.entries.read(r).find(e => isEqual(e.modifiedURI, model.uri) && e.kind === 'text') as IModifiedTextFileEntry | undefined; }).recomputeInitiallyAndOnChange(this._store); @@ -112,7 +111,7 @@ export class NotebookModelSynchronizer extends Disposable { } })); - const updateNotebookModel = (entry: IModifiedFileEntry, token: CancellationToken) => { + const updateNotebookModel = (entry: IModifiedTextFileEntry, token: CancellationToken) => { this.throttledUpdateNotebookModel.trigger(() => this.updateNotebookModel(entry, token)); }; @@ -137,7 +136,7 @@ export class NotebookModelSynchronizer extends Disposable { snapshotCreated = true; } - const modifiedModel = (entry as ChatEditingModifiedFileEntry).modifiedModel; + const modifiedModel = entry.modifiedModel; let cancellationToken = store.add(new CancellationTokenSource()); store.add(modifiedModel.onDidChangeContent(async () => { if (!modifiedModel.isDisposed() && !entry.originalModel.isDisposed() && modifiedModel.getValue() !== entry.originalModel.getValue()) { @@ -235,8 +234,8 @@ export class NotebookModelSynchronizer extends Disposable { } } - private async accept(entry: IModifiedFileEntry) { - const modifiedModel = (entry as ChatEditingModifiedFileEntry).modifiedModel; + private async accept(entry: IModifiedTextFileEntry) { + const modifiedModel = entry.modifiedModel; const content = modifiedModel.getValue(); await this.updateNotebook(VSBuffer.fromString(content), false); } @@ -247,14 +246,14 @@ export class NotebookModelSynchronizer extends Disposable { } private _originalModel?: Promise; - private async getOriginalModel(entry: IModifiedFileEntry): Promise { + private async getOriginalModel(entry: IModifiedTextFileEntry): Promise { if (!this._originalModel) { this._originalModel = this.originalModelRefFactory.getOrCreate(entry, this.model.viewType).then(ref => this._register(ref).object); } return this._originalModel; } - private async updateNotebookModel(entry: IModifiedFileEntry, token: CancellationToken) { - const modifiedModelVersion = (entry as ChatEditingModifiedFileEntry).modifiedModel.getVersionId(); + private async updateNotebookModel(entry: IModifiedTextFileEntry, token: CancellationToken) { + const modifiedModelVersion = entry.modifiedModel.getVersionId(); const currentModel = this.model; const modelVersion = currentModel?.versionId ?? 0; const modelWithChatEdits = await this.getModifiedModelForDiff(entry, token); @@ -266,7 +265,7 @@ export class NotebookModelSynchronizer extends Disposable { const cellDiffInfo = (await this.computeDiff(originalModel, modelWithChatEdits, token))?.cellDiffInfo; // This is the diff from the current model to the model with chat edits. const cellDiffInfoToApplyEdits = (await this.computeDiff(currentModel, modelWithChatEdits, token))?.cellDiffInfo; - const currentVersion = (entry as ChatEditingModifiedFileEntry).modifiedModel.getVersionId(); + const currentVersion = entry.modifiedModel.getVersionId(); if (!cellDiffInfo || !cellDiffInfoToApplyEdits || token.isCancellationRequested || currentVersion !== modifiedModelVersion || modelVersion !== currentModel.versionId) { return; } @@ -365,8 +364,8 @@ export class NotebookModelSynchronizer extends Disposable { await Event.toPromise(this.modelService.onModelAdded); return this.waitForCellModelToBeAvailable(cell); } - private async getModifiedModelForDiff(entry: IModifiedFileEntry, token: CancellationToken): Promise { - const text = (entry as ChatEditingModifiedFileEntry).modifiedModel.getValue(); + private async getModifiedModelForDiff(entry: IModifiedTextFileEntry, token: CancellationToken): Promise { + const text = entry.modifiedModel.getValue(); const bytes = VSBuffer.fromString(text); const uri = entry.modifiedURI.with({ scheme: `NotebookChatEditorController.modifiedScheme${Date.now().toString()}` }); const stream = bufferToStream(bytes); From 205c1a79cefd0b0466119450c20970ffaa903dea Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Fri, 22 Nov 2024 11:23:18 +1100 Subject: [PATCH 2/7] Misc --- .../chat/browser/chatEditing/chatEditingSession.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts index 01aee33d61bc7..41c372415c757 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts @@ -308,7 +308,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio if (entry.kind === 'text' && snapshotEntry.kind === 'text') { entry.restoreFromSnapshot(snapshotEntry); } else if (entry.kind === 'notebook' && snapshotEntry.kind === 'notebook') { - // entry.restoreFromSnapshot(snapshotEntry); + throw new Error('Not implemented'); } else { throw new Error('Unexpected snapshot entry kind'); } @@ -659,12 +659,10 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio return result; }; const deserializeChatEditingSessionSnapshot = async (snapshot: IChatEditingSessionSnapshotDTO) => { - const entriesMap = new ResourceMap(); + const entriesMap = new ResourceMap(); for (const entryDTO of snapshot.entries) { const entry = await deserializeSnapshotEntry(entryDTO); - if (entry) { - entriesMap.set(entry.resource, entry); - } + entriesMap.set(entry.resource, entry); } return ({ requestId: snapshot.requestId, @@ -674,7 +672,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio }; const deserializeSnapshotEntry = (entry: ISnapshotEntryDTO) => { if (entry.kind === 'notebook') { - return; + throw new Error('Not implemented'); } return TextSnapshotEntry.deserialize(entry, this.chatSessionId, this._instantiationService,); }; From 8cd4bfdd3fce6eb2978205111813dfe6f84f0519 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Fri, 22 Nov 2024 11:24:51 +1100 Subject: [PATCH 3/7] more fixes --- .../chat/browser/chatEditing/chatEditingSession.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts index 41c372415c757..ba11043d76f35 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts @@ -30,7 +30,7 @@ import { IEditorService } from '../../../../services/editor/common/editorService import { MultiDiffEditor } from '../../../multiDiffEditor/browser/multiDiffEditor.js'; import { MultiDiffEditorInput } from '../../../multiDiffEditor/browser/multiDiffEditorInput.js'; import { ChatAgentLocation, IChatAgentService } from '../../common/chatAgents.js'; -import { ChatEditingSessionChangeType, ChatEditingSessionState, ChatEditKind, IChatEditingSession, IModifiedEntryTelemetryInfo, IModifiedFileEntry, ISnapshotEntry, ISnapshotEntryDTO, ITextSnapshotEntry, STORAGE_CONTENTS_FOLDER, STORAGE_STATE_FILE, WorkingSetDisplayMetadata, WorkingSetEntryRemovalReason, WorkingSetEntryState } from '../../common/chatEditingService.js'; +import { ChatEditingSessionChangeType, ChatEditingSessionState, ChatEditKind, IChatEditingSession, IModifiedEntryTelemetryInfo, IModifiedFileEntry, ISnapshotEntry, ISnapshotEntryDTO, STORAGE_CONTENTS_FOLDER, STORAGE_STATE_FILE, WorkingSetDisplayMetadata, WorkingSetEntryRemovalReason, WorkingSetEntryState } from '../../common/chatEditingService.js'; import { IChatResponseModel } from '../../common/chatModel.js'; import { ChatEditingMultiDiffSourceResolver } from './chatEditingService.js'; import { ChatEditingModifiedFileEntry, TextSnapshotEntry } from './chatEditingModifiedFileEntry.js'; @@ -229,11 +229,9 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio for (const [file, state] of this._workingSet) { workingSet.set(file, state); } - const entries = new ResourceMap(); + const entries = new ResourceMap(); for (const entry of this._entriesObs.get()) { - if (entry.kind !== 'notebook') { - entries.set(entry.modifiedURI, entry.createSnapshot(requestId)); - } + entries.set(entry.modifiedURI, entry.createSnapshot(requestId)); } return { requestId, From bf1f48b6b92e6320ce5b8f8aa7fb37cd96bd03af Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Fri, 22 Nov 2024 11:32:42 +1100 Subject: [PATCH 4/7] misc --- .../contrib/chat/browser/chatEditing/chatEditingSession.ts | 2 +- src/vs/workbench/contrib/chat/browser/chatEditorSaving.ts | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts index ba11043d76f35..6e0a082b419f3 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts @@ -614,7 +614,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio return entry; } - private async _createModifiedFileEntry(resource: URI, responseModel: IModifiedEntryTelemetryInfo, mustExist = false, initialContent: string | undefined): Promise { + private async _createModifiedFileEntry(resource: URI, responseModel: IModifiedEntryTelemetryInfo, mustExist = false, initialContent: string | undefined): Promise { try { const ref = await this._textModelService.createModelReference(resource); diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorSaving.ts b/src/vs/workbench/contrib/chat/browser/chatEditorSaving.ts index c5c62968a66c0..53b4f2e2c8217 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditorSaving.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditorSaving.ts @@ -184,8 +184,6 @@ export class ChatEditorSaving extends Disposable implements IWorkbenchContributi } private _reportSaved(entry: IModifiedFileEntry) { - assertType(entry instanceof ChatEditingModifiedFileEntry); - this._chatService.notifyUserAction({ action: { kind: 'chatEditingSessionAction', uri: entry.modifiedURI, hasRemainingEdits: false, outcome: 'saved' }, agentId: entry.telemetryInfo.agentId, From 0c25f00956a64a625163b8f5207d259be3326f7c Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Fri, 22 Nov 2024 14:01:17 +1100 Subject: [PATCH 5/7] Oops --- .../chat/browser/chatEditing/chatEditingModifiedFileEntry.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts index 997688b8aa4f9..5d78720fe8fc5 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts @@ -441,8 +441,7 @@ export class TextSnapshotEntry implements ITextSnapshotEntry { ) { } - public static create(entry: IModifiedTextFileEntry, chatSessionId: string, - requestId: string | undefined, + public static create(entry: IModifiedTextFileEntry, requestId: string | undefined, edit: OffsetEdit, instantiationService: IInstantiationService): TextSnapshotEntry { return instantiationService.createInstance(TextSnapshotEntry, From b485915e5fdc76e097e3e60a27a4068d9cc44ac7 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Fri, 22 Nov 2024 14:11:19 +1100 Subject: [PATCH 6/7] Misc --- .../chatEditingModifiedFileEntry.ts | 27 +++++++++---------- .../contrib/chat/common/chatEditingService.ts | 13 ++++++--- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts index 5d78720fe8fc5..0204761e5e029 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts @@ -460,11 +460,11 @@ export class TextSnapshotEntry implements ITextSnapshotEntry { const workspaceContextService = accessor.get(IWorkspaceContextService); const environmentService = accessor.get(IEnvironmentService); const fileService = accessor.get(IFileService); - const storageLocation = TextSnapshotEntry._getStorageLocation(chatSessionId, workspaceContextService, environmentService); + const storageLocation = getStorageLocation(chatSessionId, workspaceContextService, environmentService); const [original, current] = await Promise.all([ - TextSnapshotEntry.getFileContent(entry.originalHash, fileService, storageLocation), - TextSnapshotEntry.getFileContent(entry.currentHash, fileService, storageLocation) + getFileContent(entry.originalHash, fileService, storageLocation), + getFileContent(entry.currentHash, fileService, storageLocation) ]); return instantiationService.createInstance(TextSnapshotEntry, @@ -494,7 +494,7 @@ export class TextSnapshotEntry implements ITextSnapshotEntry { telemetryInfo: { requestId: this.telemetryInfo.requestId, agentId: this.telemetryInfo.agentId, command: this.telemetryInfo.command } } satisfies ITextSnapshotEntryDTO; - const storageFolder = TextSnapshotEntry._getStorageLocation(this.telemetryInfo.sessionId, this._workspaceContextService, this._environmentService); + const storageFolder = getStorageLocation(this.telemetryInfo.sessionId, this._workspaceContextService, this._environmentService); const contentsFolder = URI.joinPath(storageFolder, STORAGE_CONTENTS_FOLDER); await Promise.all(Array.from(fileContents.entries()).map(async ([hash, content]) => { @@ -511,15 +511,14 @@ export class TextSnapshotEntry implements ITextSnapshotEntry { shaComputer.update(content); return shaComputer.digest().substring(0, 7); } - private static _getStorageLocation(chatSessionId: string, - _workspaceContextService: IWorkspaceContextService, - _environmentService: IEnvironmentService, - ): URI { - const workspaceId = _workspaceContextService.getWorkspace().id; - return joinPath(_environmentService.workspaceStorageHome, workspaceId, 'chatEditingSessions', chatSessionId); - } - private static getFileContent(hash: string, fileService: IFileService, storageLocation: URI) { - return fileService.readFile(joinPath(storageLocation, STORAGE_CONTENTS_FOLDER, hash)).then(content => content.value.toString()); - } +} + +function getStorageLocation(chatSessionId: string, workspaceContextService: IWorkspaceContextService, environmentService: IEnvironmentService): URI { + const workspaceId = workspaceContextService.getWorkspace().id; + return joinPath(environmentService.workspaceStorageHome, workspaceId, 'chatEditingSessions', chatSessionId); +} + +function getFileContent(hash: string, fileService: IFileService, storageLocation: URI) { + return fileService.readFile(joinPath(storageLocation, STORAGE_CONTENTS_FOLDER, hash)).then(content => content.value.toString()); } diff --git a/src/vs/workbench/contrib/chat/common/chatEditingService.ts b/src/vs/workbench/contrib/chat/common/chatEditingService.ts index 520f2bc2a0f4c..5a5a85f34aff4 100644 --- a/src/vs/workbench/contrib/chat/common/chatEditingService.ts +++ b/src/vs/workbench/contrib/chat/common/chatEditingService.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { VSBuffer } from '../../../../base/common/buffer.js'; import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; import { Event } from '../../../../base/common/event.js'; import { IDisposable } from '../../../../base/common/lifecycle.js'; @@ -16,7 +17,7 @@ import { ITextModel } from '../../../../editor/common/model.js'; import { localize } from '../../../../nls.js'; import { RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -import { ICellEditOperation } from '../../notebook/common/notebookCommon.js'; +import { ICellEditOperation, INotebookTextModel } from '../../notebook/common/notebookCommon.js'; import { IChatAgentResult } from './chatAgents.js'; import { IChatResponseModel } from './chatModel.js'; @@ -127,21 +128,25 @@ export interface IBaseSnapshotEntry { export interface ITextSnapshotEntry extends IBaseSnapshotEntry { kind: 'text'; - readonly languageId: string; readonly original: string; readonly current: string; + readonly languageId: string; readonly originalToCurrentEdit: OffsetEdit; serialize(): Promise; } export interface INotebookSnapshotEntry extends IBaseSnapshotEntry { kind: 'notebook'; + readonly original: VSBuffer; + readonly current: VSBuffer; serialize(): Promise; } export type ISnapshotEntry = ITextSnapshotEntry | INotebookSnapshotEntry; export interface IBaseSnapshotEntryDTO { readonly resource: string; + readonly originalHash: string; + readonly currentHash: string; readonly state: WorkingSetEntryState; readonly snapshotUri: string; readonly telemetryInfo: IModifiedEntryTelemetryInfoDTO; @@ -149,8 +154,6 @@ export interface IBaseSnapshotEntryDTO { export interface ITextSnapshotEntryDTO extends IBaseSnapshotEntryDTO { readonly kind: 'text'; readonly languageId: string; - readonly originalHash: string; - readonly currentHash: string; readonly originalToCurrentEdit: IOffsetEdit; } @@ -204,6 +207,8 @@ export interface IModifiedTextFileEntry extends IModifiedAnyFileEntry { export interface IModifiedNotebookFileEntry extends IModifiedAnyFileEntry { readonly kind: 'notebook'; + readonly originalModel: INotebookTextModel; + readonly modifiedModel: INotebookTextModel; acceptAgentEdits(textEdits: (TextEdit | ICellEditOperation)[], isLastEdits: boolean): void; createSnapshot(requestId: string | undefined): INotebookSnapshotEntry; restoreFromSnapshot(snapshot: INotebookSnapshotEntry): void; From 58bc2dcc4113572b005e82d14e1589cd5682e3d4 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Mon, 25 Nov 2024 09:38:26 +1100 Subject: [PATCH 7/7] Misc --- .../chat/browser/chatEditing/chatEditingModifiedFileEntry.ts | 4 ++-- src/vs/workbench/contrib/chat/browser/chatEditorSaving.ts | 2 -- src/vs/workbench/contrib/chat/common/chatEditingService.ts | 1 + 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts index 0204761e5e029..6c78170c4f28a 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts @@ -187,7 +187,7 @@ export class ChatEditingModifiedFileEntry extends Disposable implements IModifie createSnapshot(requestId: string | undefined): ITextSnapshotEntry { this._isFirstEditAfterStartOrSnapshot = true; - return TextSnapshotEntry.create(this, this._telemetryInfo.sessionId, requestId, this._edit, this.instantiationService); + return TextSnapshotEntry.create(this, requestId, this._edit, this.instantiationService); } restoreFromSnapshot(snapshot: ITextSnapshotEntry) { @@ -513,7 +513,7 @@ export class TextSnapshotEntry implements ITextSnapshotEntry { } } -function getStorageLocation(chatSessionId: string, workspaceContextService: IWorkspaceContextService, environmentService: IEnvironmentService): URI { +export function getStorageLocation(chatSessionId: string, workspaceContextService: IWorkspaceContextService, environmentService: IEnvironmentService): URI { const workspaceId = workspaceContextService.getWorkspace().id; return joinPath(environmentService.workspaceStorageHome, workspaceId, 'chatEditingSessions', chatSessionId); } diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorSaving.ts b/src/vs/workbench/contrib/chat/browser/chatEditorSaving.ts index 53b4f2e2c8217..f50719dfd0b40 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditorSaving.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditorSaving.ts @@ -12,7 +12,6 @@ import { Disposable, DisposableMap, DisposableStore, MutableDisposable } from '. import { ResourceSet } from '../../../../base/common/map.js'; import { autorunWithStore } from '../../../../base/common/observable.js'; import { isEqual } from '../../../../base/common/resources.js'; -import { assertType } from '../../../../base/common/types.js'; import { URI } from '../../../../base/common/uri.js'; import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js'; import { localize } from '../../../../nls.js'; @@ -32,7 +31,6 @@ import { ChatContextKeys } from '../common/chatContextKeys.js'; import { applyingChatEditsFailedContextKey, CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME, hasAppliedChatEditsContextKey, hasUndecidedChatEditingResourceContextKey, IChatEditingService, IChatEditingSession, IModifiedFileEntry, WorkingSetEntryState } from '../common/chatEditingService.js'; import { IChatModel } from '../common/chatModel.js'; import { IChatService } from '../common/chatService.js'; -import { ChatEditingModifiedFileEntry } from './chatEditing/chatEditingModifiedFileEntry.js'; export class ChatEditorSaving extends Disposable implements IWorkbenchContribution { diff --git a/src/vs/workbench/contrib/chat/common/chatEditingService.ts b/src/vs/workbench/contrib/chat/common/chatEditingService.ts index 5a5a85f34aff4..0e776e279e9b5 100644 --- a/src/vs/workbench/contrib/chat/common/chatEditingService.ts +++ b/src/vs/workbench/contrib/chat/common/chatEditingService.ts @@ -201,6 +201,7 @@ export interface IModifiedTextFileEntry extends IModifiedAnyFileEntry { readonly kind: 'text'; readonly originalModel: ITextModel; readonly modifiedModel: ITextModel; + readonly initialContent: string; createSnapshot(requestId: string | undefined): ITextSnapshotEntry; restoreFromSnapshot(snapshot: ITextSnapshotEntry): void; }