Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { RunOnceScheduler } from '../../../../../base/common/async.js';
7
import { Emitter } from '../../../../../base/common/event.js';
8
import { Disposable, DisposableMap, MutableDisposable, toDisposable } from '../../../../../base/common/lifecycle.js';
9
import { Schemas } from '../../../../../base/common/network.js';
10
import { clamp } from '../../../../../base/common/numbers.js';
11
import { autorun, derived, IObservable, ITransaction, observableValue, observableValueOpts, transaction } from '../../../../../base/common/observable.js';
12
import { URI } from '../../../../../base/common/uri.js';
13
import { TextEdit } from '../../../../../editor/common/languages.js';
14
import { localize } from '../../../../../nls.js';
15
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
16
import { IFileService } from '../../../../../platform/files/common/files.js';
17
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
18
import { observableConfigValue } from '../../../../../platform/observable/common/platformObservableUtils.js';
19
import { editorBackground, registerColor, transparent } from '../../../../../platform/theme/common/colorRegistry.js';
20
import { IUndoRedoElement, IUndoRedoService } from '../../../../../platform/undoRedo/common/undoRedo.js';
21
import { IEditorPane } from '../../../../common/editor.js';
22
import { IFilesConfigurationService } from '../../../../services/filesConfiguration/common/filesConfigurationService.js';
23
import { IAiEditTelemetryService } from '../../../editTelemetry/browser/telemetry/aiEditTelemetry/aiEditTelemetryService.js';
24
import { EditDeltaInfo } from '../../../../../editor/common/textModelEditSource.js';
25
import { ICellEditOperation } from '../../../notebook/common/notebookCommon.js';
26
import { ChatEditKind, IModifiedEntryTelemetryInfo, IModifiedFileEntry, IModifiedFileEntryEditorIntegration, ISnapshotEntry, ModifiedFileEntryState } from '../../common/chatEditingService.js';
27
import { IChatResponseModel } from '../../common/chatModel.js';
28
import { ChatUserAction, IChatService } from '../../common/chatService.js';
29
30
class AutoAcceptControl {
31
constructor(
32
readonly total: number,
33
readonly remaining: number,
34
readonly cancel: () => void
35
) { }
36
}
37
38
export const pendingRewriteMinimap = registerColor('minimap.chatEditHighlight',
39
transparent(editorBackground, 0.6),
40
localize('editorSelectionBackground', "Color of pending edit regions in the minimap"));
41
42
43
export abstract class AbstractChatEditingModifiedFileEntry extends Disposable implements IModifiedFileEntry {
44
45
static readonly scheme = 'modified-file-entry';
46
47
private static lastEntryId = 0;
48
49
readonly entryId = `${AbstractChatEditingModifiedFileEntry.scheme}::${++AbstractChatEditingModifiedFileEntry.lastEntryId}`;
50
51
protected readonly _onDidDelete = this._register(new Emitter<void>());
52
readonly onDidDelete = this._onDidDelete.event;
53
54
protected readonly _stateObs = observableValue<ModifiedFileEntryState>(this, ModifiedFileEntryState.Modified);
55
readonly state: IObservable<ModifiedFileEntryState> = this._stateObs;
56
57
protected readonly _waitsForLastEdits = observableValue<boolean>(this, false);
58
readonly waitsForLastEdits: IObservable<boolean> = this._waitsForLastEdits;
59
60
protected readonly _isCurrentlyBeingModifiedByObs = observableValue<IChatResponseModel | undefined>(this, undefined);
61
readonly isCurrentlyBeingModifiedBy: IObservable<IChatResponseModel | undefined> = this._isCurrentlyBeingModifiedByObs;
62
63
protected readonly _lastModifyingResponseObs = observableValueOpts<IChatResponseModel | undefined>({ equalsFn: (a, b) => a?.requestId === b?.requestId }, undefined);
64
readonly lastModifyingResponse: IObservable<IChatResponseModel | undefined> = this._lastModifyingResponseObs;
65
66
protected readonly _lastModifyingResponseInProgressObs = this._lastModifyingResponseObs.map((value, r) => {
67
return value?.isInProgress.read(r) ?? false;
68
});
69
70
protected readonly _rewriteRatioObs = observableValue<number>(this, 0);
71
readonly rewriteRatio: IObservable<number> = this._rewriteRatioObs;
72
73
private readonly _reviewModeTempObs = observableValue<true | undefined>(this, undefined);
74
readonly reviewMode: IObservable<boolean>;
75
76
private readonly _autoAcceptCtrl = observableValue<AutoAcceptControl | undefined>(this, undefined);
77
readonly autoAcceptController: IObservable<AutoAcceptControl | undefined> = this._autoAcceptCtrl;
78
79
protected readonly _autoAcceptTimeout: IObservable<number>;
80
81
get telemetryInfo(): IModifiedEntryTelemetryInfo {
82
return this._telemetryInfo;
83
}
84
85
readonly createdInRequestId: string | undefined;
86
87
get lastModifyingRequestId() {
88
return this._telemetryInfo.requestId;
89
}
90
91
private _refCounter: number = 1;
92
93
readonly abstract originalURI: URI;
94
95
protected readonly _userEditScheduler = this._register(new RunOnceScheduler(() => this._notifySessionAction('userModified'), 1000));
96
97
constructor(
98
readonly modifiedURI: URI,
99
protected _telemetryInfo: IModifiedEntryTelemetryInfo,
100
kind: ChatEditKind,
101
@IConfigurationService configService: IConfigurationService,
102
@IFilesConfigurationService protected _fileConfigService: IFilesConfigurationService,
103
@IChatService protected readonly _chatService: IChatService,
104
@IFileService protected readonly _fileService: IFileService,
105
@IUndoRedoService private readonly _undoRedoService: IUndoRedoService,
106
@IInstantiationService protected readonly _instantiationService: IInstantiationService,
107
@IAiEditTelemetryService private readonly _aiEditTelemetryService: IAiEditTelemetryService,
108
) {
109
super();
110
111
if (kind === ChatEditKind.Created) {
112
this.createdInRequestId = this._telemetryInfo.requestId;
113
}
114
115
if (this.modifiedURI.scheme !== Schemas.untitled && this.modifiedURI.scheme !== Schemas.vscodeNotebookCell) {
116
this._register(this._fileService.watch(this.modifiedURI));
117
this._register(this._fileService.onDidFilesChange(e => {
118
if (e.affects(this.modifiedURI) && kind === ChatEditKind.Created && e.gotDeleted()) {
119
this._onDidDelete.fire();
120
}
121
}));
122
}
123
124
// review mode depends on setting and temporary override
125
const autoAcceptRaw = observableConfigValue('chat.editing.autoAcceptDelay', 0, configService);
126
this._autoAcceptTimeout = derived(r => {
127
const value = autoAcceptRaw.read(r);
128
return clamp(value, 0, 100);
129
});
130
this.reviewMode = derived(r => {
131
const configuredValue = this._autoAcceptTimeout.read(r);
132
const tempValue = this._reviewModeTempObs.read(r);
133
return tempValue ?? configuredValue === 0;
134
});
135
136
this._store.add(toDisposable(() => this._lastModifyingResponseObs.set(undefined, undefined)));
137
138
const autoSaveOff = this._store.add(new MutableDisposable());
139
this._store.add(autorun(r => {
140
if (this._waitsForLastEdits.read(r)) {
141
autoSaveOff.value = _fileConfigService.disableAutoSave(this.modifiedURI);
142
} else {
143
autoSaveOff.clear();
144
}
145
}));
146
147
this._store.add(autorun(r => {
148
const inProgress = this._lastModifyingResponseInProgressObs.read(r);
149
if (inProgress === false && !this.reviewMode.read(r)) {
150
// AUTO accept mode (when request is done)
151
152
const acceptTimeout = this._autoAcceptTimeout.get() * 1000;
153
const future = Date.now() + acceptTimeout;
154
const update = () => {
155
156
const reviewMode = this.reviewMode.get();
157
if (reviewMode) {
158
// switched back to review mode
159
this._autoAcceptCtrl.set(undefined, undefined);
160
return;
161
}
162
163
const remain = Math.round(future - Date.now());
164
if (remain <= 0) {
165
this.accept();
166
} else {
167
const handle = setTimeout(update, 100);
168
this._autoAcceptCtrl.set(new AutoAcceptControl(acceptTimeout, remain, () => {
169
clearTimeout(handle);
170
this._autoAcceptCtrl.set(undefined, undefined);
171
}), undefined);
172
}
173
};
174
update();
175
}
176
}));
177
}
178
179
override dispose(): void {
180
if (--this._refCounter === 0) {
181
super.dispose();
182
}
183
}
184
185
acquire() {
186
this._refCounter++;
187
return this;
188
}
189
190
enableReviewModeUntilSettled(): void {
191
192
this._reviewModeTempObs.set(true, undefined);
193
194
const cleanup = autorun(r => {
195
// reset config when settled
196
const resetConfig = this.state.read(r) !== ModifiedFileEntryState.Modified;
197
if (resetConfig) {
198
this._store.delete(cleanup);
199
this._reviewModeTempObs.set(undefined, undefined);
200
}
201
});
202
203
this._store.add(cleanup);
204
}
205
206
updateTelemetryInfo(telemetryInfo: IModifiedEntryTelemetryInfo) {
207
this._telemetryInfo = telemetryInfo;
208
}
209
210
async accept(): Promise<void> {
211
if (this._stateObs.get() !== ModifiedFileEntryState.Modified) {
212
// already accepted or rejected
213
return;
214
}
215
216
await this._doAccept();
217
transaction(tx => {
218
this._stateObs.set(ModifiedFileEntryState.Accepted, tx);
219
this._autoAcceptCtrl.set(undefined, tx);
220
});
221
222
this._notifySessionAction('accepted');
223
}
224
225
protected abstract _doAccept(): Promise<void>;
226
227
async reject(): Promise<void> {
228
if (this._stateObs.get() !== ModifiedFileEntryState.Modified) {
229
// already accepted or rejected
230
return;
231
}
232
233
this._notifySessionAction('rejected');
234
await this._doReject();
235
transaction(tx => {
236
this._stateObs.set(ModifiedFileEntryState.Rejected, tx);
237
this._autoAcceptCtrl.set(undefined, tx);
238
});
239
}
240
241
protected abstract _doReject(): Promise<void>;
242
243
protected _notifySessionAction(outcome: 'accepted' | 'rejected' | 'userModified') {
244
this._notifyAction({ kind: 'chatEditingSessionAction', uri: this.modifiedURI, hasRemainingEdits: false, outcome });
245
}
246
247
protected _notifyAction(action: ChatUserAction) {
248
if (action.kind === 'chatEditingHunkAction') {
249
this._aiEditTelemetryService.handleCodeAccepted({
250
suggestionId: undefined, // TODO@hediet try to figure this out
251
acceptanceMethod: 'accept',
252
presentation: 'highlightedEdit',
253
modelId: this._telemetryInfo.modelId,
254
modeId: this._telemetryInfo.modeId,
255
applyCodeBlockSuggestionId: this._telemetryInfo.applyCodeBlockSuggestionId,
256
editDeltaInfo: new EditDeltaInfo(
257
action.linesAdded,
258
action.linesRemoved,
259
-1,
260
-1,
261
),
262
feature: this._telemetryInfo.feature,
263
languageId: action.languageId,
264
});
265
}
266
267
this._chatService.notifyUserAction({
268
action,
269
agentId: this._telemetryInfo.agentId,
270
modelId: this._telemetryInfo.modelId,
271
modeId: this._telemetryInfo.modeId,
272
command: this._telemetryInfo.command,
273
sessionId: this._telemetryInfo.sessionId,
274
requestId: this._telemetryInfo.requestId,
275
result: this._telemetryInfo.result
276
});
277
}
278
279
private readonly _editorIntegrations = this._register(new DisposableMap<IEditorPane, IModifiedFileEntryEditorIntegration>());
280
281
getEditorIntegration(pane: IEditorPane): IModifiedFileEntryEditorIntegration {
282
let value = this._editorIntegrations.get(pane);
283
if (!value) {
284
value = this._createEditorIntegration(pane);
285
this._editorIntegrations.set(pane, value);
286
}
287
return value;
288
}
289
290
/**
291
* Create the editor integration for this entry and the given editor pane. This will only be called
292
* once (and cached) per pane. The integration is meant to be scoped to this entry only and when the
293
* passed pane/editor changes input, then the editor integration must handle that, e.g use default/null
294
* values
295
*/
296
protected abstract _createEditorIntegration(editor: IEditorPane): IModifiedFileEntryEditorIntegration;
297
298
abstract readonly changesCount: IObservable<number>;
299
300
acceptStreamingEditsStart(responseModel: IChatResponseModel, tx: ITransaction) {
301
this._resetEditsState(tx);
302
this._isCurrentlyBeingModifiedByObs.set(responseModel, tx);
303
this._lastModifyingResponseObs.set(responseModel, tx);
304
this._autoAcceptCtrl.get()?.cancel();
305
306
const undoRedoElement = this._createUndoRedoElement(responseModel);
307
if (undoRedoElement) {
308
this._undoRedoService.pushElement(undoRedoElement);
309
}
310
}
311
312
protected abstract _createUndoRedoElement(response: IChatResponseModel): IUndoRedoElement | undefined;
313
314
abstract acceptAgentEdits(uri: URI, edits: (TextEdit | ICellEditOperation)[], isLastEdits: boolean, responseModel: IChatResponseModel): Promise<void>;
315
316
async acceptStreamingEditsEnd() {
317
this._resetEditsState(undefined);
318
319
if (await this._areOriginalAndModifiedIdentical()) {
320
// ACCEPT if identical
321
await this.accept();
322
}
323
}
324
325
protected abstract _areOriginalAndModifiedIdentical(): Promise<boolean>;
326
327
protected _resetEditsState(tx: ITransaction | undefined): void {
328
this._isCurrentlyBeingModifiedByObs.set(undefined, tx);
329
this._rewriteRatioObs.set(0, tx);
330
this._waitsForLastEdits.set(false, tx);
331
}
332
333
// --- snapshot
334
335
abstract createSnapshot(requestId: string | undefined, undoStop: string | undefined): ISnapshotEntry;
336
337
abstract equalsSnapshot(snapshot: ISnapshotEntry | undefined): boolean;
338
339
abstract restoreFromSnapshot(snapshot: ISnapshotEntry, restoreToDisk?: boolean): Promise<void>;
340
341
// --- inital content
342
343
abstract resetToInitialContent(): Promise<void>;
344
345
abstract initialContent: string;
346
}
347
348