Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/common/editing/chatEditingService.ts
4780 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 { decodeHex, encodeHex, VSBuffer } from '../../../../../base/common/buffer.js';
7
import { CancellationToken } from '../../../../../base/common/cancellation.js';
8
import { CancellationError } from '../../../../../base/common/errors.js';
9
import { Event } from '../../../../../base/common/event.js';
10
import { IDisposable } from '../../../../../base/common/lifecycle.js';
11
import { autorunSelfDisposable, IObservable, IReader } from '../../../../../base/common/observable.js';
12
import { hasKey } from '../../../../../base/common/types.js';
13
import { URI } from '../../../../../base/common/uri.js';
14
import { IDocumentDiff } from '../../../../../editor/common/diff/documentDiffProvider.js';
15
import { Location, TextEdit } from '../../../../../editor/common/languages.js';
16
import { ITextModel } from '../../../../../editor/common/model.js';
17
import { EditSuggestionId } from '../../../../../editor/common/textModelEditSource.js';
18
import { localize } from '../../../../../nls.js';
19
import { RawContextKey } from '../../../../../platform/contextkey/common/contextkey.js';
20
import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js';
21
import { IEditorPane } from '../../../../common/editor.js';
22
import { ICellEditOperation } from '../../../notebook/common/notebookCommon.js';
23
import { IChatAgentResult } from '../participants/chatAgents.js';
24
import { ChatModel, IChatRequestDisablement, IChatResponseModel } from '../model/chatModel.js';
25
import { IChatMultiDiffData, IChatProgress } from '../chatService/chatService.js';
26
27
export const IChatEditingService = createDecorator<IChatEditingService>('chatEditingService');
28
29
export interface IChatEditingService {
30
31
_serviceBrand: undefined;
32
33
startOrContinueGlobalEditingSession(chatModel: ChatModel): IChatEditingSession;
34
35
getEditingSession(chatSessionResource: URI): IChatEditingSession | undefined;
36
37
/**
38
* All editing sessions, sorted by recency, e.g the last created session comes first.
39
*/
40
readonly editingSessionsObs: IObservable<readonly IChatEditingSession[]>;
41
42
/**
43
* Creates a new short lived editing session
44
*/
45
createEditingSession(chatModel: ChatModel): IChatEditingSession;
46
47
/**
48
* Creates an editing session with state transferred from the provided session.
49
*/
50
transferEditingSession(chatModel: ChatModel, session: IChatEditingSession): IChatEditingSession;
51
52
//#region related files
53
54
hasRelatedFilesProviders(): boolean;
55
registerRelatedFilesProvider(handle: number, provider: IChatRelatedFilesProvider): IDisposable;
56
getRelatedFiles(chatSessionResource: URI, prompt: string, files: URI[], token: CancellationToken): Promise<{ group: string; files: IChatRelatedFile[] }[] | undefined>;
57
58
//#endregion
59
}
60
61
export interface IChatRequestDraft {
62
readonly prompt: string;
63
readonly files: readonly URI[];
64
}
65
66
export interface IChatRelatedFileProviderMetadata {
67
readonly description: string;
68
}
69
70
export interface IChatRelatedFile {
71
readonly uri: URI;
72
readonly description: string;
73
}
74
75
export interface IChatRelatedFilesProvider {
76
readonly description: string;
77
provideRelatedFiles(chatRequest: IChatRequestDraft, token: CancellationToken): Promise<IChatRelatedFile[] | undefined>;
78
}
79
80
export interface WorkingSetDisplayMetadata {
81
state: ModifiedFileEntryState;
82
description?: string;
83
}
84
85
export interface IStreamingEdits {
86
pushText(edits: TextEdit[], isLastEdits: boolean): void;
87
pushNotebookCellText(cell: URI, edits: TextEdit[], isLastEdits: boolean): void;
88
pushNotebook(edits: ICellEditOperation[], isLastEdits: boolean): void;
89
/** Marks edits as done, idempotent */
90
complete(): void;
91
}
92
93
export interface IModifiedEntryTelemetryInfo {
94
readonly agentId: string | undefined;
95
readonly command: string | undefined;
96
readonly sessionResource: URI;
97
readonly requestId: string;
98
readonly result: IChatAgentResult | undefined;
99
readonly modelId: string | undefined;
100
readonly modeId: 'ask' | 'edit' | 'agent' | 'custom' | 'applyCodeBlock' | undefined;
101
readonly applyCodeBlockSuggestionId: EditSuggestionId | undefined;
102
readonly feature: 'sideBarChat' | 'inlineChat' | undefined;
103
}
104
105
export interface ISnapshotEntry {
106
readonly resource: URI;
107
readonly languageId: string;
108
readonly snapshotUri: URI;
109
readonly original: string;
110
readonly current: string;
111
readonly state: ModifiedFileEntryState;
112
telemetryInfo: IModifiedEntryTelemetryInfo;
113
}
114
115
export interface IChatEditingSession extends IDisposable {
116
readonly isGlobalEditingSession: boolean;
117
readonly chatSessionResource: URI;
118
readonly onDidDispose: Event<void>;
119
readonly state: IObservable<ChatEditingSessionState>;
120
readonly entries: IObservable<readonly IModifiedFileEntry[]>;
121
/** Requests disabled by undo/redo in the session */
122
readonly requestDisablement: IObservable<IChatRequestDisablement[]>;
123
124
show(previousChanges?: boolean): Promise<void>;
125
accept(...uris: URI[]): Promise<void>;
126
reject(...uris: URI[]): Promise<void>;
127
getEntry(uri: URI): IModifiedFileEntry | undefined;
128
readEntry(uri: URI, reader: IReader): IModifiedFileEntry | undefined;
129
130
restoreSnapshot(requestId: string, stopId: string | undefined): Promise<void>;
131
132
/**
133
* Marks all edits to the given resources as agent edits until
134
* {@link stopExternalEdits} is called with the same ID. This is used for
135
* agents that make changes on-disk rather than streaming edits through the
136
* chat session.
137
*/
138
startExternalEdits(responseModel: IChatResponseModel, operationId: number, resources: URI[], undoStopId: string): Promise<IChatProgress[]>;
139
stopExternalEdits(responseModel: IChatResponseModel, operationId: number): Promise<IChatProgress[]>;
140
141
/**
142
* Gets the snapshot URI of a file at the request and _after_ changes made in the undo stop.
143
* @param uri File in the workspace
144
*/
145
getSnapshotUri(requestId: string, uri: URI, stopId: string | undefined): URI | undefined;
146
147
getSnapshotContents(requestId: string, uri: URI, stopId: string | undefined): Promise<VSBuffer | undefined>;
148
getSnapshotModel(requestId: string, undoStop: string | undefined, snapshotUri: URI): Promise<ITextModel | null>;
149
150
/**
151
* Will lead to this object getting disposed
152
*/
153
stop(clearState?: boolean): Promise<void>;
154
155
/**
156
* Starts making edits to the resource.
157
* @param resource URI that's being edited
158
* @param responseModel The response model making the edits
159
* @param inUndoStop The undo stop the edits will be grouped in
160
*/
161
startStreamingEdits(resource: URI, responseModel: IChatResponseModel, inUndoStop: string | undefined): IStreamingEdits;
162
163
/**
164
* Gets the document diff of a change made to a URI between one undo stop and
165
* the next one.
166
* @returns The observable or undefined if there is no diff between the stops.
167
*/
168
getEntryDiffBetweenStops(uri: URI, requestId: string | undefined, stopId: string | undefined): IObservable<IEditSessionEntryDiff | undefined> | undefined;
169
170
/**
171
* Gets the document diff of a change made to a URI between one request to another one.
172
* @returns The observable or undefined if there is no diff between the requests.
173
*/
174
getEntryDiffBetweenRequests(uri: URI, startRequestIs: string, stopRequestId: string): IObservable<IEditSessionEntryDiff | undefined>;
175
176
/**
177
* Gets the diff of each file modified in this session, comparing the initial
178
* baseline to the current state.
179
*/
180
getDiffsForFilesInSession(): IObservable<readonly IEditSessionEntryDiff[]>;
181
182
/**
183
* Gets the diff of each file modified in the request.
184
*/
185
getDiffsForFilesInRequest(requestId: string): IObservable<readonly IEditSessionEntryDiff[]>;
186
187
/**
188
* Whether there are any edits made in the given request.
189
*/
190
hasEditsInRequest(requestId: string, reader?: IReader): boolean;
191
192
/**
193
* Gets the aggregated diff stats for all files modified in this session.
194
*/
195
getDiffForSession(): IObservable<IEditSessionDiffStats>;
196
197
readonly canUndo: IObservable<boolean>;
198
readonly canRedo: IObservable<boolean>;
199
undoInteraction(): Promise<void>;
200
redoInteraction(): Promise<void>;
201
}
202
203
export function chatEditingSessionIsReady(session: IChatEditingSession): Promise<void> {
204
return new Promise<void>(resolve => {
205
autorunSelfDisposable(reader => {
206
const state = session.state.read(reader);
207
if (state !== ChatEditingSessionState.Initial) {
208
reader.dispose();
209
resolve();
210
}
211
});
212
});
213
}
214
215
export function editEntriesToMultiDiffData(entriesObs: IObservable<readonly IEditSessionEntryDiff[]>): IChatMultiDiffData {
216
return {
217
kind: 'multiDiffData',
218
collapsed: true,
219
multiDiffData: entriesObs.map(entries => ({
220
title: localize('chatMultidiff.autoGenerated', 'Changes to {0} files', entries.length),
221
resources: entries.map(entry => ({
222
originalUri: entry.originalURI,
223
modifiedUri: entry.modifiedURI,
224
goToFileUri: entry.modifiedURI,
225
added: entry.added,
226
removed: entry.removed,
227
}))
228
})),
229
};
230
}
231
232
export function awaitCompleteChatEditingDiff(diff: IObservable<IEditSessionEntryDiff>, token?: CancellationToken): Promise<IEditSessionEntryDiff>;
233
export function awaitCompleteChatEditingDiff(diff: IObservable<readonly IEditSessionEntryDiff[]>, token?: CancellationToken): Promise<readonly IEditSessionEntryDiff[]>;
234
export function awaitCompleteChatEditingDiff(diff: IObservable<readonly IEditSessionEntryDiff[] | IEditSessionEntryDiff>, token?: CancellationToken): Promise<readonly IEditSessionEntryDiff[] | IEditSessionEntryDiff> {
235
return new Promise<readonly IEditSessionEntryDiff[] | IEditSessionEntryDiff>((resolve, reject) => {
236
autorunSelfDisposable(reader => {
237
if (token) {
238
if (token.isCancellationRequested) {
239
reader.dispose();
240
return reject(new CancellationError());
241
}
242
reader.store.add(token.onCancellationRequested(() => {
243
reader.dispose();
244
reject(new CancellationError());
245
}));
246
}
247
248
const current = diff.read(reader);
249
if (current instanceof Array) {
250
if (!current.some(c => c.isBusy)) {
251
reader.dispose();
252
resolve(current);
253
}
254
} else if (!current.isBusy) {
255
reader.dispose();
256
resolve(current);
257
}
258
});
259
});
260
}
261
262
export interface IEditSessionDiffStats {
263
/** Added data (e.g. line numbers) to show in the UI */
264
added: number;
265
/** Removed data (e.g. line numbers) to show in the UI */
266
removed: number;
267
}
268
269
export interface IEditSessionEntryDiff extends IEditSessionDiffStats {
270
/** LHS and RHS of a diff editor, if opened: */
271
originalURI: URI;
272
modifiedURI: URI;
273
274
/** Diff state information: */
275
quitEarly: boolean;
276
identical: boolean;
277
278
/** True if nothing else will be added to this diff. */
279
isFinal: boolean;
280
281
/** True if the diff is currently being computed or updated. */
282
isBusy: boolean;
283
}
284
285
export function emptySessionEntryDiff(originalURI: URI, modifiedURI: URI): IEditSessionEntryDiff {
286
return {
287
originalURI,
288
modifiedURI,
289
added: 0,
290
removed: 0,
291
quitEarly: false,
292
identical: false,
293
isFinal: false,
294
isBusy: false,
295
};
296
}
297
298
export const enum ModifiedFileEntryState {
299
Modified,
300
Accepted,
301
Rejected,
302
}
303
304
/**
305
* Represents a part of a change
306
*/
307
export interface IModifiedFileEntryChangeHunk {
308
accept(): Promise<boolean>;
309
reject(): Promise<boolean>;
310
}
311
312
export interface IModifiedFileEntryEditorIntegration extends IDisposable {
313
314
/**
315
* The index of a change
316
*/
317
currentIndex: IObservable<number>;
318
319
/**
320
* Reveal the first (`true`) or last (`false`) change
321
*/
322
reveal(firstOrLast: boolean, preserveFocus?: boolean): void;
323
324
/**
325
* Go to next change and increate `currentIndex`
326
* @param wrap When at the last, start over again or not
327
* @returns If it went next
328
*/
329
next(wrap: boolean): boolean;
330
331
/**
332
* @see `next`
333
*/
334
previous(wrap: boolean): boolean;
335
336
/**
337
* Enable the accessible diff viewer for this editor
338
*/
339
enableAccessibleDiffView(): void;
340
341
/**
342
* Accept the change given or the nearest
343
* @param change An opaque change object
344
*/
345
acceptNearestChange(change?: IModifiedFileEntryChangeHunk): Promise<void>;
346
347
/**
348
* @see `acceptNearestChange`
349
*/
350
rejectNearestChange(change?: IModifiedFileEntryChangeHunk): Promise<void>;
351
352
/**
353
* Toggle between diff-editor and normal editor
354
* @param change An opaque change object
355
* @param show Optional boolean to control if the diff should show
356
*/
357
toggleDiff(change: IModifiedFileEntryChangeHunk | undefined, show?: boolean): Promise<void>;
358
}
359
360
export interface IModifiedFileEntry {
361
readonly entryId: string;
362
readonly originalURI: URI;
363
readonly modifiedURI: URI;
364
365
readonly lastModifyingRequestId: string;
366
367
readonly state: IObservable<ModifiedFileEntryState>;
368
readonly isCurrentlyBeingModifiedBy: IObservable<{ responseModel: IChatResponseModel; undoStopId: string | undefined } | undefined>;
369
readonly lastModifyingResponse: IObservable<IChatResponseModel | undefined>;
370
readonly rewriteRatio: IObservable<number>;
371
372
readonly waitsForLastEdits: IObservable<boolean>;
373
374
accept(): Promise<void>;
375
reject(): Promise<void>;
376
377
reviewMode: IObservable<boolean>;
378
autoAcceptController: IObservable<{ total: number; remaining: number; cancel(): void } | undefined>;
379
enableReviewModeUntilSettled(): void;
380
381
/**
382
* Number of changes for this file
383
*/
384
readonly changesCount: IObservable<number>;
385
386
/**
387
* Diff information for this entry
388
*/
389
readonly diffInfo?: IObservable<IDocumentDiff>;
390
391
/**
392
* Number of lines added in this entry.
393
*/
394
readonly linesAdded?: IObservable<number>;
395
396
/**
397
* Number of lines removed in this entry
398
*/
399
readonly linesRemoved?: IObservable<number>;
400
401
getEditorIntegration(editor: IEditorPane): IModifiedFileEntryEditorIntegration;
402
hasModificationAt(location: Location): boolean;
403
/**
404
* Gets the document diff info, waiting for any ongoing promises to flush.
405
*/
406
getDiffInfo?(): Promise<IDocumentDiff>;
407
}
408
409
export interface IChatEditingSessionStream {
410
textEdits(resource: URI, textEdits: TextEdit[], isLastEdits: boolean, responseModel: IChatResponseModel): void;
411
notebookEdits(resource: URI, edits: ICellEditOperation[], isLastEdits: boolean, responseModel: IChatResponseModel): void;
412
}
413
414
export const enum ChatEditingSessionState {
415
Initial = 0,
416
StreamingEdits = 1,
417
Idle = 2,
418
Disposed = 3
419
}
420
421
export const CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME = 'chat-editing-multi-diff-source';
422
423
export const chatEditingWidgetFileStateContextKey = new RawContextKey<ModifiedFileEntryState>('chatEditingWidgetFileState', undefined, localize('chatEditingWidgetFileState', "The current state of the file in the chat editing widget"));
424
export const chatEditingAgentSupportsReadonlyReferencesContextKey = new RawContextKey<boolean>('chatEditingAgentSupportsReadonlyReferences', undefined, localize('chatEditingAgentSupportsReadonlyReferences', "Whether the chat editing agent supports readonly references (temporary)"));
425
export const decidedChatEditingResourceContextKey = new RawContextKey<string[]>('decidedChatEditingResource', []);
426
export const chatEditingResourceContextKey = new RawContextKey<string | undefined>('chatEditingResource', undefined);
427
export const inChatEditingSessionContextKey = new RawContextKey<boolean | undefined>('inChatEditingSession', undefined);
428
export const hasUndecidedChatEditingResourceContextKey = new RawContextKey<boolean | undefined>('hasUndecidedChatEditingResource', false);
429
export const hasAppliedChatEditsContextKey = new RawContextKey<boolean | undefined>('hasAppliedChatEdits', false);
430
export const applyingChatEditsFailedContextKey = new RawContextKey<boolean | undefined>('applyingChatEditsFailed', false);
431
432
export const chatEditingMaxFileAssignmentName = 'chatEditingSessionFileLimit';
433
export const defaultChatEditingMaxFileLimit = 10;
434
435
export const enum ChatEditKind {
436
Created,
437
Modified,
438
}
439
440
export interface IChatEditingActionContext {
441
// The chat session that this editing session is associated with
442
sessionResource: URI;
443
}
444
445
export function isChatEditingActionContext(thing: unknown): thing is IChatEditingActionContext {
446
return typeof thing === 'object' && !!thing && hasKey(thing, { sessionResource: true });
447
}
448
449
export function getMultiDiffSourceUri(session: IChatEditingSession, showPreviousChanges?: boolean): URI {
450
return URI.from({
451
scheme: CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME,
452
authority: encodeHex(VSBuffer.fromString(session.chatSessionResource.toString())),
453
query: showPreviousChanges ? 'previous' : undefined,
454
});
455
}
456
457
export function parseChatMultiDiffUri(uri: URI): { chatSessionResource: URI; showPreviousChanges: boolean } {
458
const chatSessionResource = URI.parse(decodeHex(uri.authority).toString());
459
const showPreviousChanges = uri.query === 'previous';
460
461
return { chatSessionResource, showPreviousChanges };
462
}
463
464