Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.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
import { CancellationToken } from '../../../../base/common/cancellation.js';
6
import { Emitter, Event } from '../../../../base/common/event.js';
7
import { DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
8
import { ResourceMap } from '../../../../base/common/map.js';
9
import { Schemas } from '../../../../base/common/network.js';
10
import { autorun, IObservable, observableFromEvent } from '../../../../base/common/observable.js';
11
import { isEqual } from '../../../../base/common/resources.js';
12
import { assertType } from '../../../../base/common/types.js';
13
import { URI } from '../../../../base/common/uri.js';
14
import { generateUuid } from '../../../../base/common/uuid.js';
15
import { IActiveCodeEditor, ICodeEditor, isCodeEditor, isCompositeEditor, isDiffEditor } from '../../../../editor/browser/editorBrowser.js';
16
import { Range } from '../../../../editor/common/core/range.js';
17
import { ILanguageService } from '../../../../editor/common/languages/language.js';
18
import { IValidEditOperation } from '../../../../editor/common/model.js';
19
import { createTextBufferFactoryFromSnapshot } from '../../../../editor/common/model/textModel.js';
20
import { IEditorWorkerService } from '../../../../editor/common/services/editorWorker.js';
21
import { IModelService } from '../../../../editor/common/services/model.js';
22
import { ITextModelService } from '../../../../editor/common/services/resolverService.js';
23
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
24
import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
25
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
26
import { ILogService } from '../../../../platform/log/common/log.js';
27
import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js';
28
import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js';
29
import { DEFAULT_EDITOR_ASSOCIATION } from '../../../common/editor.js';
30
import { IEditorService } from '../../../services/editor/common/editorService.js';
31
import { ITextFileService } from '../../../services/textfile/common/textfiles.js';
32
import { UntitledTextEditorInput } from '../../../services/untitled/common/untitledTextEditorInput.js';
33
import { IChatWidgetService } from '../../chat/browser/chat.js';
34
import { IChatAgentService } from '../../chat/common/chatAgents.js';
35
import { ModifiedFileEntryState } from '../../chat/common/chatEditingService.js';
36
import { IChatService } from '../../chat/common/chatService.js';
37
import { ChatAgentLocation } from '../../chat/common/constants.js';
38
import { CTX_INLINE_CHAT_HAS_AGENT, CTX_INLINE_CHAT_HAS_AGENT2, CTX_INLINE_CHAT_POSSIBLE, InlineChatConfigKeys } from '../common/inlineChat.js';
39
import { HunkData, Session, SessionWholeRange, StashedSession, TelemetryData, TelemetryDataClassification } from './inlineChatSession.js';
40
import { IInlineChatSession2, IInlineChatSessionEndEvent, IInlineChatSessionEvent, IInlineChatSessionService, ISessionKeyComputer } from './inlineChatSessionService.js';
41
42
43
type SessionData = {
44
editor: ICodeEditor;
45
session: Session;
46
store: IDisposable;
47
};
48
49
export class InlineChatError extends Error {
50
static readonly code = 'InlineChatError';
51
constructor(message: string) {
52
super(message);
53
this.name = InlineChatError.code;
54
}
55
}
56
57
58
export class InlineChatSessionServiceImpl implements IInlineChatSessionService {
59
60
declare _serviceBrand: undefined;
61
62
private readonly _store = new DisposableStore();
63
64
private readonly _onWillStartSession = this._store.add(new Emitter<IActiveCodeEditor>());
65
readonly onWillStartSession: Event<IActiveCodeEditor> = this._onWillStartSession.event;
66
67
private readonly _onDidMoveSession = this._store.add(new Emitter<IInlineChatSessionEvent>());
68
readonly onDidMoveSession: Event<IInlineChatSessionEvent> = this._onDidMoveSession.event;
69
70
private readonly _onDidEndSession = this._store.add(new Emitter<IInlineChatSessionEndEvent>());
71
readonly onDidEndSession: Event<IInlineChatSessionEndEvent> = this._onDidEndSession.event;
72
73
private readonly _onDidStashSession = this._store.add(new Emitter<IInlineChatSessionEvent>());
74
readonly onDidStashSession: Event<IInlineChatSessionEvent> = this._onDidStashSession.event;
75
76
private readonly _sessions = new Map<string, SessionData>();
77
private readonly _keyComputers = new Map<string, ISessionKeyComputer>();
78
79
readonly hideOnRequest: IObservable<boolean>;
80
81
constructor(
82
@ITelemetryService private readonly _telemetryService: ITelemetryService,
83
@IModelService private readonly _modelService: IModelService,
84
@ITextModelService private readonly _textModelService: ITextModelService,
85
@IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService,
86
@ILogService private readonly _logService: ILogService,
87
@IInstantiationService private readonly _instaService: IInstantiationService,
88
@IEditorService private readonly _editorService: IEditorService,
89
@ITextFileService private readonly _textFileService: ITextFileService,
90
@ILanguageService private readonly _languageService: ILanguageService,
91
@IChatService private readonly _chatService: IChatService,
92
@IChatAgentService private readonly _chatAgentService: IChatAgentService,
93
@IChatWidgetService private readonly _chatWidgetService: IChatWidgetService,
94
@IConfigurationService private readonly _configurationService: IConfigurationService,
95
) {
96
97
const v2 = observableConfigValue(InlineChatConfigKeys.EnableV2, false, this._configurationService);
98
99
this.hideOnRequest = observableConfigValue(InlineChatConfigKeys.HideOnRequest, false, this._configurationService)
100
.map((value, r) => v2.read(r) && value);
101
}
102
103
dispose() {
104
this._store.dispose();
105
this._sessions.forEach(x => x.store.dispose());
106
this._sessions.clear();
107
}
108
109
async createSession(editor: IActiveCodeEditor, options: { headless?: boolean; wholeRange?: Range; session?: Session }, token: CancellationToken): Promise<Session | undefined> {
110
111
const agent = this._chatAgentService.getDefaultAgent(ChatAgentLocation.Editor);
112
113
if (!agent) {
114
this._logService.trace('[IE] NO agent found');
115
return undefined;
116
}
117
118
this._onWillStartSession.fire(editor);
119
120
const textModel = editor.getModel();
121
const selection = editor.getSelection();
122
123
const store = new DisposableStore();
124
this._logService.trace(`[IE] creating NEW session for ${editor.getId()}, ${agent.extensionId}`);
125
126
const chatModel = options.session?.chatModel ?? this._chatService.startSession(ChatAgentLocation.Editor, token);
127
if (!chatModel) {
128
this._logService.trace('[IE] NO chatModel found');
129
return undefined;
130
}
131
132
store.add(toDisposable(() => {
133
const doesOtherSessionUseChatModel = [...this._sessions.values()].some(data => data.session !== session && data.session.chatModel === chatModel);
134
135
if (!doesOtherSessionUseChatModel) {
136
this._chatService.clearSession(chatModel.sessionId);
137
chatModel.dispose();
138
}
139
}));
140
141
const lastResponseListener = store.add(new MutableDisposable());
142
store.add(chatModel.onDidChange(e => {
143
if (e.kind !== 'addRequest' || !e.request.response) {
144
return;
145
}
146
147
const { response } = e.request;
148
149
session.markModelVersion(e.request);
150
lastResponseListener.value = response.onDidChange(() => {
151
152
if (!response.isComplete) {
153
return;
154
}
155
156
lastResponseListener.clear(); // ONCE
157
158
// special handling for untitled files
159
for (const part of response.response.value) {
160
if (part.kind !== 'textEditGroup' || part.uri.scheme !== Schemas.untitled || isEqual(part.uri, session.textModelN.uri)) {
161
continue;
162
}
163
const langSelection = this._languageService.createByFilepathOrFirstLine(part.uri, undefined);
164
const untitledTextModel = this._textFileService.untitled.create({
165
associatedResource: part.uri,
166
languageId: langSelection.languageId
167
});
168
untitledTextModel.resolve();
169
this._textModelService.createModelReference(part.uri).then(ref => {
170
store.add(ref);
171
});
172
}
173
174
});
175
}));
176
177
store.add(this._chatAgentService.onDidChangeAgents(e => {
178
if (e === undefined && (!this._chatAgentService.getAgent(agent.id) || !this._chatAgentService.getActivatedAgents().map(agent => agent.id).includes(agent.id))) {
179
this._logService.trace(`[IE] provider GONE for ${editor.getId()}, ${agent.extensionId}`);
180
this._releaseSession(session, true);
181
}
182
}));
183
184
const id = generateUuid();
185
const targetUri = textModel.uri;
186
187
// AI edits happen in the actual model, keep a reference but make no copy
188
store.add((await this._textModelService.createModelReference(textModel.uri)));
189
const textModelN = textModel;
190
191
// create: keep a snapshot of the "actual" model
192
const textModel0 = store.add(this._modelService.createModel(
193
createTextBufferFactoryFromSnapshot(textModel.createSnapshot()),
194
{ languageId: textModel.getLanguageId(), onDidChange: Event.None },
195
targetUri.with({ scheme: Schemas.vscode, authority: 'inline-chat', path: '', query: new URLSearchParams({ id, 'textModel0': '' }).toString() }), true
196
));
197
198
// untitled documents are special and we are releasing their session when their last editor closes
199
if (targetUri.scheme === Schemas.untitled) {
200
store.add(this._editorService.onDidCloseEditor(() => {
201
if (!this._editorService.isOpened({ resource: targetUri, typeId: UntitledTextEditorInput.ID, editorId: DEFAULT_EDITOR_ASSOCIATION.id })) {
202
this._releaseSession(session, true);
203
}
204
}));
205
}
206
207
let wholeRange = options.wholeRange;
208
if (!wholeRange) {
209
wholeRange = new Range(selection.selectionStartLineNumber, selection.selectionStartColumn, selection.positionLineNumber, selection.positionColumn);
210
}
211
212
if (token.isCancellationRequested) {
213
store.dispose();
214
return undefined;
215
}
216
217
const session = new Session(
218
options.headless ?? false,
219
targetUri,
220
textModel0,
221
textModelN,
222
agent,
223
store.add(new SessionWholeRange(textModelN, wholeRange)),
224
store.add(new HunkData(this._editorWorkerService, textModel0, textModelN)),
225
chatModel,
226
options.session?.versionsByRequest,
227
);
228
229
// store: key -> session
230
const key = this._key(editor, session.targetUri);
231
if (this._sessions.has(key)) {
232
store.dispose();
233
throw new Error(`Session already stored for ${key}`);
234
}
235
this._sessions.set(key, { session, editor, store });
236
return session;
237
}
238
239
moveSession(session: Session, target: ICodeEditor): void {
240
const newKey = this._key(target, session.targetUri);
241
const existing = this._sessions.get(newKey);
242
if (existing) {
243
if (existing.session !== session) {
244
throw new Error(`Cannot move session because the target editor already/still has one`);
245
} else {
246
// noop
247
return;
248
}
249
}
250
251
let found = false;
252
for (const [oldKey, data] of this._sessions) {
253
if (data.session === session) {
254
found = true;
255
this._sessions.delete(oldKey);
256
this._sessions.set(newKey, { ...data, editor: target });
257
this._logService.trace(`[IE] did MOVE session for ${data.editor.getId()} to NEW EDITOR ${target.getId()}, ${session.agent.extensionId}`);
258
this._onDidMoveSession.fire({ session, editor: target });
259
break;
260
}
261
}
262
if (!found) {
263
throw new Error(`Cannot move session because it is not stored`);
264
}
265
}
266
267
releaseSession(session: Session): void {
268
this._releaseSession(session, false);
269
}
270
271
private _releaseSession(session: Session, byServer: boolean): void {
272
273
let tuple: [string, SessionData] | undefined;
274
275
// cleanup
276
for (const candidate of this._sessions) {
277
if (candidate[1].session === session) {
278
// if (value.session === session) {
279
tuple = candidate;
280
break;
281
}
282
}
283
284
if (!tuple) {
285
// double remove
286
return;
287
}
288
289
this._telemetryService.publicLog2<TelemetryData, TelemetryDataClassification>('interactiveEditor/session', session.asTelemetryData());
290
291
const [key, value] = tuple;
292
this._sessions.delete(key);
293
this._logService.trace(`[IE] did RELEASED session for ${value.editor.getId()}, ${session.agent.extensionId}`);
294
295
this._onDidEndSession.fire({ editor: value.editor, session, endedByExternalCause: byServer });
296
value.store.dispose();
297
}
298
299
stashSession(session: Session, editor: ICodeEditor, undoCancelEdits: IValidEditOperation[]): StashedSession {
300
const result = this._instaService.createInstance(StashedSession, editor, session, undoCancelEdits);
301
this._onDidStashSession.fire({ editor, session });
302
this._logService.trace(`[IE] did STASH session for ${editor.getId()}, ${session.agent.extensionId}`);
303
return result;
304
}
305
306
getCodeEditor(session: Session): ICodeEditor {
307
for (const [, data] of this._sessions) {
308
if (data.session === session) {
309
return data.editor;
310
}
311
}
312
throw new Error('session not found');
313
}
314
315
getSession(editor: ICodeEditor, uri: URI): Session | undefined {
316
const key = this._key(editor, uri);
317
return this._sessions.get(key)?.session;
318
}
319
320
private _key(editor: ICodeEditor, uri: URI): string {
321
const item = this._keyComputers.get(uri.scheme);
322
return item
323
? item.getComparisonKey(editor, uri)
324
: `${editor.getId()}@${uri.toString()}`;
325
326
}
327
328
registerSessionKeyComputer(scheme: string, value: ISessionKeyComputer): IDisposable {
329
this._keyComputers.set(scheme, value);
330
return toDisposable(() => this._keyComputers.delete(scheme));
331
}
332
333
// ---- NEW
334
335
private readonly _sessions2 = new ResourceMap<IInlineChatSession2>();
336
337
private readonly _onDidChangeSessions = this._store.add(new Emitter<this>());
338
readonly onDidChangeSessions: Event<this> = this._onDidChangeSessions.event;
339
340
341
async createSession2(editor: ICodeEditor, uri: URI, token: CancellationToken): Promise<IInlineChatSession2> {
342
343
assertType(editor.hasModel());
344
345
if (this._sessions2.has(uri)) {
346
throw new Error('Session already exists');
347
}
348
349
this._onWillStartSession.fire(editor as IActiveCodeEditor);
350
351
const chatModel = this._chatService.startSession(ChatAgentLocation.Panel, token, false);
352
353
const editingSession = await chatModel.editingSessionObs?.promise!;
354
const widget = this._chatWidgetService.getWidgetBySessionId(chatModel.sessionId);
355
await widget?.attachmentModel.addFile(uri);
356
357
const store = new DisposableStore();
358
store.add(toDisposable(() => {
359
this._chatService.cancelCurrentRequestForSession(chatModel.sessionId);
360
editingSession.reject();
361
this._sessions2.delete(uri);
362
this._onDidChangeSessions.fire(this);
363
}));
364
store.add(chatModel);
365
366
store.add(autorun(r => {
367
368
const entries = editingSession.entries.read(r);
369
if (entries.length === 0) {
370
return;
371
}
372
373
const allSettled = entries.every(entry => {
374
const state = entry.state.read(r);
375
return (state === ModifiedFileEntryState.Accepted || state === ModifiedFileEntryState.Rejected)
376
&& !entry.isCurrentlyBeingModifiedBy.read(r);
377
});
378
379
if (allSettled && !chatModel.requestInProgress) {
380
// self terminate
381
store.dispose();
382
}
383
}));
384
385
const result: IInlineChatSession2 = {
386
uri,
387
initialPosition: editor.getPosition().delta(-1),
388
chatModel,
389
editingSession,
390
dispose: store.dispose.bind(store)
391
};
392
this._sessions2.set(uri, result);
393
this._onDidChangeSessions.fire(this);
394
return result;
395
}
396
397
getSession2(uri: URI): IInlineChatSession2 | undefined {
398
let result = this._sessions2.get(uri);
399
if (!result) {
400
// no direct session, try to find an editing session which has a file entry for the uri
401
for (const [_, candidate] of this._sessions2) {
402
const entry = candidate.editingSession.getEntry(uri);
403
if (entry) {
404
result = candidate;
405
break;
406
}
407
}
408
}
409
410
return result;
411
}
412
}
413
414
export class InlineChatEnabler {
415
416
static Id = 'inlineChat.enabler';
417
418
private readonly _ctxHasProvider: IContextKey<boolean>;
419
private readonly _ctxHasProvider2: IContextKey<boolean>;
420
private readonly _ctxPossible: IContextKey<boolean>;
421
422
private readonly _store = new DisposableStore();
423
424
constructor(
425
@IContextKeyService contextKeyService: IContextKeyService,
426
@IChatAgentService chatAgentService: IChatAgentService,
427
@IEditorService editorService: IEditorService,
428
@IConfigurationService configService: IConfigurationService,
429
) {
430
this._ctxHasProvider = CTX_INLINE_CHAT_HAS_AGENT.bindTo(contextKeyService);
431
this._ctxHasProvider2 = CTX_INLINE_CHAT_HAS_AGENT2.bindTo(contextKeyService);
432
this._ctxPossible = CTX_INLINE_CHAT_POSSIBLE.bindTo(contextKeyService);
433
434
const agentObs = observableFromEvent(this, chatAgentService.onDidChangeAgents, () => chatAgentService.getDefaultAgent(ChatAgentLocation.Editor));
435
const inlineChat2Obs = observableConfigValue(InlineChatConfigKeys.EnableV2, false, configService);
436
437
this._store.add(autorun(r => {
438
const v2 = inlineChat2Obs.read(r);
439
const agent = agentObs.read(r);
440
if (!agent) {
441
this._ctxHasProvider.reset();
442
this._ctxHasProvider2.reset();
443
} else if (v2) {
444
this._ctxHasProvider.reset();
445
this._ctxHasProvider2.set(true);
446
} else {
447
this._ctxHasProvider.set(true);
448
this._ctxHasProvider2.reset();
449
}
450
}));
451
452
const updateEditor = () => {
453
const ctrl = editorService.activeEditorPane?.getControl();
454
const isCodeEditorLike = isCodeEditor(ctrl) || isDiffEditor(ctrl) || isCompositeEditor(ctrl);
455
this._ctxPossible.set(isCodeEditorLike);
456
};
457
458
this._store.add(editorService.onDidActiveEditorChange(updateEditor));
459
updateEditor();
460
}
461
462
dispose() {
463
this._ctxPossible.reset();
464
this._ctxHasProvider.reset();
465
this._store.dispose();
466
}
467
}
468
469