Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/inlineChat/browser/inlineChatController.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 * as aria from '../../../../base/browser/ui/aria/aria.js';
7
import { Barrier, DeferredPromise, Queue, raceCancellation } from '../../../../base/common/async.js';
8
import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js';
9
import { toErrorMessage } from '../../../../base/common/errorMessage.js';
10
import { onUnexpectedError } from '../../../../base/common/errors.js';
11
import { Emitter, Event } from '../../../../base/common/event.js';
12
import { Lazy } from '../../../../base/common/lazy.js';
13
import { DisposableStore, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
14
import { Schemas } from '../../../../base/common/network.js';
15
import { MovingAverage } from '../../../../base/common/numbers.js';
16
import { autorun, autorunWithStore, derived, IObservable, observableFromEvent, observableSignalFromEvent, observableValue, transaction, waitForState } from '../../../../base/common/observable.js';
17
import { isEqual } from '../../../../base/common/resources.js';
18
import { StopWatch } from '../../../../base/common/stopwatch.js';
19
import { assertType } from '../../../../base/common/types.js';
20
import { URI } from '../../../../base/common/uri.js';
21
import { generateUuid } from '../../../../base/common/uuid.js';
22
import { ICodeEditor, isCodeEditor } from '../../../../editor/browser/editorBrowser.js';
23
import { observableCodeEditor } from '../../../../editor/browser/observableCodeEditor.js';
24
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
25
import { EditorOption } from '../../../../editor/common/config/editorOptions.js';
26
import { IPosition, Position } from '../../../../editor/common/core/position.js';
27
import { IRange, Range } from '../../../../editor/common/core/range.js';
28
import { ISelection, Selection, SelectionDirection } from '../../../../editor/common/core/selection.js';
29
import { IEditorContribution } from '../../../../editor/common/editorCommon.js';
30
import { TextEdit, VersionedExtensionId } from '../../../../editor/common/languages.js';
31
import { IValidEditOperation } from '../../../../editor/common/model.js';
32
import { IEditorWorkerService } from '../../../../editor/common/services/editorWorker.js';
33
import { DefaultModelSHA1Computer } from '../../../../editor/common/services/modelService.js';
34
import { InlineCompletionsController } from '../../../../editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.js';
35
import { MessageController } from '../../../../editor/contrib/message/browser/messageController.js';
36
import { localize } from '../../../../nls.js';
37
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
38
import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
39
import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';
40
import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
41
import { ILogService } from '../../../../platform/log/common/log.js';
42
import { IEditorService, SIDE_GROUP } from '../../../services/editor/common/editorService.js';
43
import { IViewsService } from '../../../services/views/common/viewsService.js';
44
import { showChatView } from '../../chat/browser/chat.js';
45
import { IChatWidgetLocationOptions } from '../../chat/browser/chatWidget.js';
46
import { ChatModel, ChatRequestRemovalReason, IChatRequestModel, IChatTextEditGroup, IChatTextEditGroupState, IResponse } from '../../chat/common/chatModel.js';
47
import { IChatRequestVariableEntry } from '../../chat/common/chatVariableEntries.js';
48
import { IChatService } from '../../chat/common/chatService.js';
49
import { INotebookEditorService } from '../../notebook/browser/services/notebookEditorService.js';
50
import { CTX_INLINE_CHAT_EDITING, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, CTX_INLINE_CHAT_RESPONSE_TYPE, CTX_INLINE_CHAT_VISIBLE, INLINE_CHAT_ID, InlineChatConfigKeys, InlineChatResponseType } from '../common/inlineChat.js';
51
import { HunkInformation, Session, StashedSession } from './inlineChatSession.js';
52
import { IInlineChatSession2, IInlineChatSessionService } from './inlineChatSessionService.js';
53
import { InlineChatError } from './inlineChatSessionServiceImpl.js';
54
import { HunkAction, IEditObserver, IInlineChatMetadata, LiveStrategy, ProgressingEditsOptions } from './inlineChatStrategies.js';
55
import { EditorBasedInlineChatWidget } from './inlineChatWidget.js';
56
import { InlineChatZoneWidget } from './inlineChatZoneWidget.js';
57
import { ChatAgentLocation } from '../../chat/common/constants.js';
58
import { ChatContextKeys } from '../../chat/common/chatContextKeys.js';
59
import { ModifiedFileEntryState } from '../../chat/common/chatEditingService.js';
60
import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js';
61
import { ISharedWebContentExtractorService } from '../../../../platform/webContentExtractor/common/webContentExtractor.js';
62
import { IFileService } from '../../../../platform/files/common/files.js';
63
import { IChatAttachmentResolveService } from '../../chat/browser/chatAttachmentResolveService.js';
64
import { INotebookService } from '../../notebook/common/notebookService.js';
65
import { ICellEditOperation } from '../../notebook/common/notebookCommon.js';
66
import { INotebookEditor } from '../../notebook/browser/notebookBrowser.js';
67
import { isNotebookContainingCellEditor as isNotebookWithCellEditor } from '../../notebook/browser/notebookEditor.js';
68
import { EditSuggestionId } from '../../../../editor/common/textModelEditSource.js';
69
70
export const enum State {
71
CREATE_SESSION = 'CREATE_SESSION',
72
INIT_UI = 'INIT_UI',
73
WAIT_FOR_INPUT = 'WAIT_FOR_INPUT',
74
SHOW_REQUEST = 'SHOW_REQUEST',
75
PAUSE = 'PAUSE',
76
CANCEL = 'CANCEL',
77
ACCEPT = 'DONE',
78
}
79
80
const enum Message {
81
NONE = 0,
82
ACCEPT_SESSION = 1 << 0,
83
CANCEL_SESSION = 1 << 1,
84
PAUSE_SESSION = 1 << 2,
85
CANCEL_REQUEST = 1 << 3,
86
CANCEL_INPUT = 1 << 4,
87
ACCEPT_INPUT = 1 << 5,
88
}
89
90
export abstract class InlineChatRunOptions {
91
initialSelection?: ISelection;
92
initialRange?: IRange;
93
message?: string;
94
attachments?: URI[];
95
autoSend?: boolean;
96
existingSession?: Session;
97
position?: IPosition;
98
99
static isInlineChatRunOptions(options: any): options is InlineChatRunOptions {
100
const { initialSelection, initialRange, message, autoSend, position, existingSession, attachments: attachments } = <InlineChatRunOptions>options;
101
if (
102
typeof message !== 'undefined' && typeof message !== 'string'
103
|| typeof autoSend !== 'undefined' && typeof autoSend !== 'boolean'
104
|| typeof initialRange !== 'undefined' && !Range.isIRange(initialRange)
105
|| typeof initialSelection !== 'undefined' && !Selection.isISelection(initialSelection)
106
|| typeof position !== 'undefined' && !Position.isIPosition(position)
107
|| typeof existingSession !== 'undefined' && !(existingSession instanceof Session)
108
|| typeof attachments !== 'undefined' && (!Array.isArray(attachments) || !attachments.every(item => item instanceof URI))
109
) {
110
return false;
111
}
112
return true;
113
}
114
}
115
116
export class InlineChatController implements IEditorContribution {
117
118
static ID = 'editor.contrib.inlineChatController';
119
120
static get(editor: ICodeEditor) {
121
return editor.getContribution<InlineChatController>(InlineChatController.ID);
122
}
123
124
private readonly _delegate: IObservable<InlineChatController1 | InlineChatController2>;
125
126
constructor(
127
editor: ICodeEditor,
128
@IConfigurationService configurationService: IConfigurationService,
129
) {
130
131
const inlineChat2 = observableConfigValue(InlineChatConfigKeys.EnableV2, false, configurationService);
132
133
this._delegate = derived(r => {
134
if (inlineChat2.read(r)) {
135
return InlineChatController2.get(editor)!;
136
} else {
137
return InlineChatController1.get(editor)!;
138
}
139
});
140
}
141
142
dispose(): void {
143
144
}
145
146
get isActive(): boolean {
147
return this._delegate.get().isActive;
148
}
149
150
async run(arg?: InlineChatRunOptions): Promise<boolean> {
151
return this._delegate.get().run(arg);
152
}
153
154
focus() {
155
return this._delegate.get().focus();
156
}
157
158
get widget(): EditorBasedInlineChatWidget {
159
return this._delegate.get().widget;
160
}
161
162
getWidgetPosition() {
163
return this._delegate.get().getWidgetPosition();
164
}
165
166
acceptSession() {
167
return this._delegate.get().acceptSession();
168
}
169
}
170
171
/**
172
* @deprecated
173
*/
174
export class InlineChatController1 implements IEditorContribution {
175
176
static get(editor: ICodeEditor) {
177
return editor.getContribution<InlineChatController1>(INLINE_CHAT_ID);
178
}
179
180
private _isDisposed: boolean = false;
181
private readonly _store = new DisposableStore();
182
183
private readonly _ui: Lazy<InlineChatZoneWidget>;
184
185
private readonly _ctxVisible: IContextKey<boolean>;
186
private readonly _ctxEditing: IContextKey<boolean>;
187
private readonly _ctxResponseType: IContextKey<undefined | InlineChatResponseType>;
188
private readonly _ctxRequestInProgress: IContextKey<boolean>;
189
190
private readonly _ctxResponse: IContextKey<boolean>;
191
192
private readonly _messages = this._store.add(new Emitter<Message>());
193
protected readonly _onDidEnterState = this._store.add(new Emitter<State>());
194
195
get chatWidget() {
196
return this._ui.value.widget.chatWidget;
197
}
198
199
private readonly _sessionStore = this._store.add(new DisposableStore());
200
private readonly _stashedSession = this._store.add(new MutableDisposable<StashedSession>());
201
private _session?: Session;
202
private _strategy?: LiveStrategy;
203
204
constructor(
205
private readonly _editor: ICodeEditor,
206
@IInstantiationService private readonly _instaService: IInstantiationService,
207
@IInlineChatSessionService private readonly _inlineChatSessionService: IInlineChatSessionService,
208
@IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService,
209
@ILogService private readonly _logService: ILogService,
210
@IConfigurationService private readonly _configurationService: IConfigurationService,
211
@IDialogService private readonly _dialogService: IDialogService,
212
@IContextKeyService contextKeyService: IContextKeyService,
213
@IChatService private readonly _chatService: IChatService,
214
@IEditorService private readonly _editorService: IEditorService,
215
@INotebookEditorService notebookEditorService: INotebookEditorService,
216
@ISharedWebContentExtractorService private readonly _webContentExtractorService: ISharedWebContentExtractorService,
217
@IFileService private readonly _fileService: IFileService,
218
@IChatAttachmentResolveService private readonly _chatAttachmentResolveService: IChatAttachmentResolveService
219
) {
220
this._ctxVisible = CTX_INLINE_CHAT_VISIBLE.bindTo(contextKeyService);
221
this._ctxEditing = CTX_INLINE_CHAT_EDITING.bindTo(contextKeyService);
222
this._ctxResponseType = CTX_INLINE_CHAT_RESPONSE_TYPE.bindTo(contextKeyService);
223
this._ctxRequestInProgress = CTX_INLINE_CHAT_REQUEST_IN_PROGRESS.bindTo(contextKeyService);
224
225
this._ctxResponse = ChatContextKeys.isResponse.bindTo(contextKeyService);
226
ChatContextKeys.responseHasError.bindTo(contextKeyService);
227
228
this._ui = new Lazy(() => {
229
230
const location: IChatWidgetLocationOptions = {
231
location: ChatAgentLocation.Editor,
232
resolveData: () => {
233
assertType(this._editor.hasModel());
234
assertType(this._session);
235
return {
236
type: ChatAgentLocation.Editor,
237
selection: this._editor.getSelection(),
238
document: this._session.textModelN.uri,
239
wholeRange: this._session?.wholeRange.trackedInitialRange,
240
};
241
}
242
};
243
244
// inline chat in notebooks
245
// check if this editor is part of a notebook editor
246
// and iff so, use the notebook location but keep the resolveData
247
// talk about editor data
248
let notebookEditor: INotebookEditor | undefined;
249
for (const editor of notebookEditorService.listNotebookEditors()) {
250
for (const [, codeEditor] of editor.codeEditors) {
251
if (codeEditor === this._editor) {
252
notebookEditor = editor;
253
location.location = ChatAgentLocation.Notebook;
254
break;
255
}
256
}
257
}
258
259
const zone = _instaService.createInstance(InlineChatZoneWidget, location, undefined, { editor: this._editor, notebookEditor });
260
this._store.add(zone);
261
this._store.add(zone.widget.chatWidget.onDidClear(async () => {
262
const r = this.joinCurrentRun();
263
this.cancelSession();
264
await r;
265
this.run();
266
}));
267
268
return zone;
269
});
270
271
this._store.add(this._editor.onDidChangeModel(async e => {
272
if (this._session || !e.newModelUrl) {
273
return;
274
}
275
276
const existingSession = this._inlineChatSessionService.getSession(this._editor, e.newModelUrl);
277
if (!existingSession) {
278
return;
279
}
280
281
this._log('session RESUMING after model change', e);
282
await this.run({ existingSession });
283
}));
284
285
this._store.add(this._inlineChatSessionService.onDidEndSession(e => {
286
if (e.session === this._session && e.endedByExternalCause) {
287
this._log('session ENDED by external cause');
288
this.acceptSession();
289
}
290
}));
291
292
this._store.add(this._inlineChatSessionService.onDidMoveSession(async e => {
293
if (e.editor === this._editor) {
294
this._log('session RESUMING after move', e);
295
await this.run({ existingSession: e.session });
296
}
297
}));
298
299
this._log(`NEW controller`);
300
}
301
302
dispose(): void {
303
if (this._currentRun) {
304
this._messages.fire(this._session?.chatModel.hasRequests
305
? Message.PAUSE_SESSION
306
: Message.CANCEL_SESSION);
307
}
308
this._store.dispose();
309
this._isDisposed = true;
310
this._log('DISPOSED controller');
311
}
312
313
private _log(message: string | Error, ...more: any[]): void {
314
if (message instanceof Error) {
315
this._logService.error(message, ...more);
316
} else {
317
this._logService.trace(`[IE] (editor:${this._editor.getId()}) ${message}`, ...more);
318
}
319
}
320
321
get widget(): EditorBasedInlineChatWidget {
322
return this._ui.value.widget;
323
}
324
325
getId(): string {
326
return INLINE_CHAT_ID;
327
}
328
329
getWidgetPosition(): Position | undefined {
330
return this._ui.value.position;
331
}
332
333
private _currentRun?: Promise<void>;
334
335
async run(options: InlineChatRunOptions | undefined = {}): Promise<boolean> {
336
337
let lastState: State | undefined;
338
const d = this._onDidEnterState.event(e => lastState = e);
339
340
try {
341
this.acceptSession();
342
if (this._currentRun) {
343
await this._currentRun;
344
}
345
if (options.initialSelection) {
346
this._editor.setSelection(options.initialSelection);
347
}
348
this._stashedSession.clear();
349
this._currentRun = this._nextState(State.CREATE_SESSION, options);
350
await this._currentRun;
351
352
} catch (error) {
353
// this should not happen but when it does make sure to tear down the UI and everything
354
this._log('error during run', error);
355
onUnexpectedError(error);
356
if (this._session) {
357
this._inlineChatSessionService.releaseSession(this._session);
358
}
359
this[State.PAUSE]();
360
361
} finally {
362
this._currentRun = undefined;
363
d.dispose();
364
}
365
366
return lastState !== State.CANCEL;
367
}
368
369
// ---- state machine
370
371
protected async _nextState(state: State, options: InlineChatRunOptions): Promise<void> {
372
let nextState: State | void = state;
373
while (nextState && !this._isDisposed) {
374
this._log('setState to ', nextState);
375
const p: State | Promise<State> | Promise<void> = this[nextState](options);
376
this._onDidEnterState.fire(nextState);
377
nextState = await p;
378
}
379
}
380
381
private async [State.CREATE_SESSION](options: InlineChatRunOptions): Promise<State.CANCEL | State.INIT_UI> {
382
assertType(this._session === undefined);
383
assertType(this._editor.hasModel());
384
385
let session: Session | undefined = options.existingSession;
386
387
let initPosition: Position | undefined;
388
if (options.position) {
389
initPosition = Position.lift(options.position).delta(-1);
390
delete options.position;
391
}
392
393
const widgetPosition = this._showWidget(session?.headless, true, initPosition);
394
395
// this._updatePlaceholder();
396
let errorMessage = localize('create.fail', "Failed to start editor chat");
397
398
if (!session) {
399
const createSessionCts = new CancellationTokenSource();
400
const msgListener = Event.once(this._messages.event)(m => {
401
this._log('state=_createSession) message received', m);
402
if (m === Message.ACCEPT_INPUT) {
403
// user accepted the input before having a session
404
options.autoSend = true;
405
this._ui.value.widget.updateInfo(localize('welcome.2', "Getting ready..."));
406
} else {
407
createSessionCts.cancel();
408
}
409
});
410
411
try {
412
session = await this._inlineChatSessionService.createSession(
413
this._editor,
414
{ wholeRange: options.initialRange },
415
createSessionCts.token
416
);
417
} catch (error) {
418
// Inline chat errors are from the provider and have their error messages shown to the user
419
if (error instanceof InlineChatError || error?.name === InlineChatError.code) {
420
errorMessage = error.message;
421
}
422
}
423
424
createSessionCts.dispose();
425
msgListener.dispose();
426
427
if (createSessionCts.token.isCancellationRequested) {
428
if (session) {
429
this._inlineChatSessionService.releaseSession(session);
430
}
431
return State.CANCEL;
432
}
433
}
434
435
delete options.initialRange;
436
delete options.existingSession;
437
438
if (!session) {
439
MessageController.get(this._editor)?.showMessage(errorMessage, widgetPosition);
440
this._log('Failed to start editor chat');
441
return State.CANCEL;
442
}
443
444
// create a new strategy
445
this._strategy = this._instaService.createInstance(LiveStrategy, session, this._editor, this._ui.value, session.headless);
446
447
this._session = session;
448
return State.INIT_UI;
449
}
450
451
private async [State.INIT_UI](options: InlineChatRunOptions): Promise<State.WAIT_FOR_INPUT | State.SHOW_REQUEST> {
452
assertType(this._session);
453
assertType(this._strategy);
454
455
// hide/cancel inline completions when invoking IE
456
InlineCompletionsController.get(this._editor)?.reject();
457
458
this._sessionStore.clear();
459
460
const wholeRangeDecoration = this._editor.createDecorationsCollection();
461
const handleWholeRangeChange = () => {
462
const newDecorations = this._strategy?.getWholeRangeDecoration() ?? [];
463
wholeRangeDecoration.set(newDecorations);
464
465
this._ctxEditing.set(!this._session?.wholeRange.trackedInitialRange.isEmpty());
466
};
467
this._sessionStore.add(toDisposable(() => {
468
wholeRangeDecoration.clear();
469
this._ctxEditing.reset();
470
}));
471
this._sessionStore.add(this._session.wholeRange.onDidChange(handleWholeRangeChange));
472
handleWholeRangeChange();
473
474
this._ui.value.widget.setChatModel(this._session.chatModel);
475
this._updatePlaceholder();
476
477
const isModelEmpty = !this._session.chatModel.hasRequests;
478
this._ui.value.widget.updateToolbar(true);
479
this._ui.value.widget.toggleStatus(!isModelEmpty);
480
this._showWidget(this._session.headless, isModelEmpty);
481
482
this._sessionStore.add(this._editor.onDidChangeModel((e) => {
483
const msg = this._session?.chatModel.hasRequests
484
? Message.PAUSE_SESSION // pause when switching models/tabs and when having a previous exchange
485
: Message.CANCEL_SESSION;
486
this._log('model changed, pause or cancel session', msg, e);
487
this._messages.fire(msg);
488
}));
489
490
491
this._sessionStore.add(this._editor.onDidChangeModelContent(e => {
492
493
494
if (this._session?.hunkData.ignoreTextModelNChanges || this._ui.value.widget.hasFocus()) {
495
return;
496
}
497
498
const wholeRange = this._session!.wholeRange;
499
let shouldFinishSession = false;
500
if (this._configurationService.getValue<boolean>(InlineChatConfigKeys.FinishOnType)) {
501
for (const { range } of e.changes) {
502
shouldFinishSession = !Range.areIntersectingOrTouching(range, wholeRange.value);
503
}
504
}
505
506
this._session!.recordExternalEditOccurred(shouldFinishSession);
507
508
if (shouldFinishSession) {
509
this._log('text changed outside of whole range, FINISH session');
510
this.acceptSession();
511
}
512
}));
513
514
this._sessionStore.add(this._session.chatModel.onDidChange(async e => {
515
if (e.kind === 'removeRequest') {
516
// TODO@jrieken there is still some work left for when a request "in the middle"
517
// is removed. We will undo all changes till that point but not remove those
518
// later request
519
await this._session!.undoChangesUntil(e.requestId);
520
}
521
}));
522
523
// apply edits from completed requests that haven't been applied yet
524
const editState = this._createChatTextEditGroupState();
525
let didEdit = false;
526
for (const request of this._session.chatModel.getRequests()) {
527
if (!request.response || request.response.result?.errorDetails) {
528
// done when seeing the first request that is still pending (no response).
529
break;
530
}
531
for (const part of request.response.response.value) {
532
if (part.kind !== 'textEditGroup' || !isEqual(part.uri, this._session.textModelN.uri)) {
533
continue;
534
}
535
if (part.state?.applied) {
536
continue;
537
}
538
for (const edit of part.edits) {
539
this._makeChanges(edit, undefined, !didEdit);
540
didEdit = true;
541
}
542
part.state ??= editState;
543
}
544
}
545
if (didEdit) {
546
const diff = await this._editorWorkerService.computeDiff(this._session.textModel0.uri, this._session.textModelN.uri, { computeMoves: false, maxComputationTimeMs: Number.MAX_SAFE_INTEGER, ignoreTrimWhitespace: false }, 'advanced');
547
this._session.wholeRange.fixup(diff?.changes ?? []);
548
await this._session.hunkData.recompute(editState, diff);
549
550
this._updateCtxResponseType();
551
}
552
options.position = await this._strategy.renderChanges();
553
554
if (this._session.chatModel.requestInProgress) {
555
return State.SHOW_REQUEST;
556
} else {
557
return State.WAIT_FOR_INPUT;
558
}
559
}
560
561
private async [State.WAIT_FOR_INPUT](options: InlineChatRunOptions): Promise<State.ACCEPT | State.CANCEL | State.PAUSE | State.WAIT_FOR_INPUT | State.SHOW_REQUEST> {
562
assertType(this._session);
563
assertType(this._strategy);
564
565
this._updatePlaceholder();
566
567
if (options.message) {
568
this._updateInput(options.message);
569
aria.alert(options.message);
570
delete options.message;
571
this._showWidget(this._session.headless, false);
572
}
573
574
let message = Message.NONE;
575
let request: IChatRequestModel | undefined;
576
577
const barrier = new Barrier();
578
const store = new DisposableStore();
579
store.add(this._session.chatModel.onDidChange(e => {
580
if (e.kind === 'addRequest') {
581
request = e.request;
582
message = Message.ACCEPT_INPUT;
583
barrier.open();
584
}
585
}));
586
store.add(this._strategy.onDidAccept(() => this.acceptSession()));
587
store.add(this._strategy.onDidDiscard(() => this.cancelSession()));
588
store.add(Event.once(this._messages.event)(m => {
589
this._log('state=_waitForInput) message received', m);
590
message = m;
591
barrier.open();
592
}));
593
594
if (options.attachments) {
595
await Promise.all(options.attachments.map(async attachment => {
596
await this._ui.value.widget.chatWidget.attachmentModel.addFile(attachment);
597
}));
598
delete options.attachments;
599
}
600
if (options.autoSend) {
601
delete options.autoSend;
602
this._showWidget(this._session.headless, false);
603
this._ui.value.widget.chatWidget.acceptInput();
604
}
605
606
await barrier.wait();
607
store.dispose();
608
609
610
if (message & (Message.CANCEL_INPUT | Message.CANCEL_SESSION)) {
611
return State.CANCEL;
612
}
613
614
if (message & Message.PAUSE_SESSION) {
615
return State.PAUSE;
616
}
617
618
if (message & Message.ACCEPT_SESSION) {
619
this._ui.value.widget.selectAll();
620
return State.ACCEPT;
621
}
622
623
if (!request?.message.text) {
624
return State.WAIT_FOR_INPUT;
625
}
626
627
628
return State.SHOW_REQUEST;
629
}
630
631
632
private async [State.SHOW_REQUEST](options: InlineChatRunOptions): Promise<State.WAIT_FOR_INPUT | State.CANCEL | State.PAUSE | State.ACCEPT> {
633
assertType(this._session);
634
assertType(this._strategy);
635
assertType(this._session.chatModel.requestInProgress);
636
637
this._ctxRequestInProgress.set(true);
638
639
const { chatModel } = this._session;
640
const request = chatModel.lastRequest;
641
642
assertType(request);
643
assertType(request.response);
644
645
this._showWidget(this._session.headless, false);
646
this._ui.value.widget.selectAll();
647
this._ui.value.widget.updateInfo('');
648
this._ui.value.widget.toggleStatus(true);
649
650
const { response } = request;
651
const responsePromise = new DeferredPromise<void>();
652
653
const store = new DisposableStore();
654
655
const progressiveEditsCts = store.add(new CancellationTokenSource());
656
const progressiveEditsAvgDuration = new MovingAverage();
657
const progressiveEditsClock = StopWatch.create();
658
const progressiveEditsQueue = new Queue();
659
660
// disable typing and squiggles while streaming a reply
661
const origDeco = this._editor.getOption(EditorOption.renderValidationDecorations);
662
this._editor.updateOptions({
663
renderValidationDecorations: 'off'
664
});
665
store.add(toDisposable(() => {
666
this._editor.updateOptions({
667
renderValidationDecorations: origDeco
668
});
669
}));
670
671
672
let next: State.WAIT_FOR_INPUT | State.SHOW_REQUEST | State.CANCEL | State.PAUSE | State.ACCEPT = State.WAIT_FOR_INPUT;
673
store.add(Event.once(this._messages.event)(message => {
674
this._log('state=_makeRequest) message received', message);
675
this._chatService.cancelCurrentRequestForSession(chatModel.sessionId);
676
if (message & Message.CANCEL_SESSION) {
677
next = State.CANCEL;
678
} else if (message & Message.PAUSE_SESSION) {
679
next = State.PAUSE;
680
} else if (message & Message.ACCEPT_SESSION) {
681
next = State.ACCEPT;
682
}
683
}));
684
685
store.add(chatModel.onDidChange(async e => {
686
if (e.kind === 'removeRequest' && e.requestId === request.id) {
687
progressiveEditsCts.cancel();
688
responsePromise.complete();
689
if (e.reason === ChatRequestRemovalReason.Resend) {
690
next = State.SHOW_REQUEST;
691
} else {
692
next = State.CANCEL;
693
}
694
return;
695
}
696
if (e.kind === 'move') {
697
assertType(this._session);
698
const log: typeof this._log = (msg: string, ...args: any[]) => this._log('state=_showRequest) moving inline chat', msg, ...args);
699
700
log('move was requested', e.target, e.range);
701
702
// if there's already a tab open for targetUri, show it and move inline chat to that tab
703
// otherwise, open the tab to the side
704
const initialSelection = Selection.fromRange(Range.lift(e.range), SelectionDirection.LTR);
705
const editorPane = await this._editorService.openEditor({ resource: e.target, options: { selection: initialSelection } }, SIDE_GROUP);
706
707
if (!editorPane) {
708
log('opening editor failed');
709
return;
710
}
711
712
const newEditor = editorPane.getControl();
713
if (!isCodeEditor(newEditor) || !newEditor.hasModel()) {
714
log('new editor is either missing or not a code editor or does not have a model');
715
return;
716
}
717
718
if (this._inlineChatSessionService.getSession(newEditor, e.target)) {
719
log('new editor ALREADY has a session');
720
return;
721
}
722
723
const newSession = await this._inlineChatSessionService.createSession(
724
newEditor,
725
{
726
session: this._session,
727
},
728
CancellationToken.None); // TODO@ulugbekna: add proper cancellation?
729
730
731
InlineChatController1.get(newEditor)?.run({ existingSession: newSession });
732
733
next = State.CANCEL;
734
responsePromise.complete();
735
736
return;
737
}
738
}));
739
740
// cancel the request when the user types
741
store.add(this._ui.value.widget.chatWidget.inputEditor.onDidChangeModelContent(() => {
742
this._chatService.cancelCurrentRequestForSession(chatModel.sessionId);
743
}));
744
745
let lastLength = 0;
746
let isFirstChange = true;
747
748
const editState = this._createChatTextEditGroupState();
749
let localEditGroup: IChatTextEditGroup | undefined;
750
751
// apply edits
752
const handleResponse = () => {
753
754
this._updateCtxResponseType();
755
756
if (!localEditGroup) {
757
localEditGroup = <IChatTextEditGroup | undefined>response.response.value.find(part => part.kind === 'textEditGroup' && isEqual(part.uri, this._session?.textModelN.uri));
758
}
759
760
if (localEditGroup) {
761
762
localEditGroup.state ??= editState;
763
764
const edits = localEditGroup.edits;
765
const newEdits = edits.slice(lastLength);
766
if (newEdits.length > 0) {
767
768
this._log(`${this._session?.textModelN.uri.toString()} received ${newEdits.length} edits`);
769
770
// NEW changes
771
lastLength = edits.length;
772
progressiveEditsAvgDuration.update(progressiveEditsClock.elapsed());
773
progressiveEditsClock.reset();
774
775
progressiveEditsQueue.queue(async () => {
776
777
const startThen = this._session!.wholeRange.value.getStartPosition();
778
779
// making changes goes into a queue because otherwise the async-progress time will
780
// influence the time it takes to receive the changes and progressive typing will
781
// become infinitely fast
782
for (const edits of newEdits) {
783
await this._makeChanges(edits, {
784
duration: progressiveEditsAvgDuration.value,
785
token: progressiveEditsCts.token
786
}, isFirstChange);
787
788
isFirstChange = false;
789
}
790
791
// reshow the widget if the start position changed or shows at the wrong position
792
const startNow = this._session!.wholeRange.value.getStartPosition();
793
if (!startNow.equals(startThen) || !this._ui.value.position?.equals(startNow)) {
794
this._showWidget(this._session!.headless, false, startNow.delta(-1));
795
}
796
});
797
}
798
}
799
800
if (response.isCanceled) {
801
progressiveEditsCts.cancel();
802
responsePromise.complete();
803
804
} else if (response.isComplete) {
805
responsePromise.complete();
806
}
807
};
808
store.add(response.onDidChange(handleResponse));
809
handleResponse();
810
811
// (1) we must wait for the request to finish
812
// (2) we must wait for all edits that came in via progress to complete
813
await responsePromise.p;
814
await progressiveEditsQueue.whenIdle();
815
816
if (response.result?.errorDetails && !response.result.errorDetails.responseIsFiltered) {
817
await this._session.undoChangesUntil(response.requestId);
818
}
819
820
store.dispose();
821
822
const diff = await this._editorWorkerService.computeDiff(this._session.textModel0.uri, this._session.textModelN.uri, { computeMoves: false, maxComputationTimeMs: Number.MAX_SAFE_INTEGER, ignoreTrimWhitespace: false }, 'advanced');
823
this._session.wholeRange.fixup(diff?.changes ?? []);
824
await this._session.hunkData.recompute(editState, diff);
825
826
this._ctxRequestInProgress.set(false);
827
828
829
let newPosition: Position | undefined;
830
831
if (response.result?.errorDetails) {
832
// error -> no message, errors are shown with the request
833
834
} else if (response.response.value.length === 0) {
835
// empty -> show message
836
const status = localize('empty', "No results, please refine your input and try again");
837
this._ui.value.widget.updateStatus(status, { classes: ['warn'] });
838
839
} else {
840
// real response -> no message
841
this._ui.value.widget.updateStatus('');
842
}
843
844
const position = await this._strategy.renderChanges();
845
if (position) {
846
// if the selection doesn't start far off we keep the widget at its current position
847
// because it makes reading this nicer
848
const selection = this._editor.getSelection();
849
if (selection?.containsPosition(position)) {
850
if (position.lineNumber - selection.startLineNumber > 8) {
851
newPosition = position;
852
}
853
} else {
854
newPosition = position;
855
}
856
}
857
this._showWidget(this._session.headless, false, newPosition);
858
859
return next;
860
}
861
862
private async[State.PAUSE]() {
863
864
this._resetWidget();
865
866
this._strategy?.dispose?.();
867
this._session = undefined;
868
}
869
870
private async[State.ACCEPT]() {
871
assertType(this._session);
872
assertType(this._strategy);
873
this._sessionStore.clear();
874
875
try {
876
await this._strategy.apply();
877
} catch (err) {
878
this._dialogService.error(localize('err.apply', "Failed to apply changes.", toErrorMessage(err)));
879
this._log('FAILED to apply changes');
880
this._log(err);
881
}
882
883
this._resetWidget();
884
this._inlineChatSessionService.releaseSession(this._session);
885
886
887
this._strategy?.dispose();
888
this._strategy = undefined;
889
this._session = undefined;
890
}
891
892
private async[State.CANCEL]() {
893
894
this._resetWidget();
895
896
if (this._session) {
897
// assertType(this._session);
898
assertType(this._strategy);
899
this._sessionStore.clear();
900
901
// only stash sessions that were not unstashed, not "empty", and not interacted with
902
const shouldStash = !this._session.isUnstashed && this._session.chatModel.hasRequests && this._session.hunkData.size === this._session.hunkData.pending;
903
let undoCancelEdits: IValidEditOperation[] = [];
904
try {
905
undoCancelEdits = this._strategy.cancel();
906
} catch (err) {
907
this._dialogService.error(localize('err.discard', "Failed to discard changes.", toErrorMessage(err)));
908
this._log('FAILED to discard changes');
909
this._log(err);
910
}
911
912
this._stashedSession.clear();
913
if (shouldStash) {
914
this._stashedSession.value = this._inlineChatSessionService.stashSession(this._session, this._editor, undoCancelEdits);
915
} else {
916
this._inlineChatSessionService.releaseSession(this._session);
917
}
918
}
919
920
921
this._strategy?.dispose();
922
this._strategy = undefined;
923
this._session = undefined;
924
}
925
926
// ----
927
928
private _showWidget(headless: boolean = false, initialRender: boolean = false, position?: Position) {
929
assertType(this._editor.hasModel());
930
this._ctxVisible.set(true);
931
932
let widgetPosition: Position;
933
if (position) {
934
// explicit position wins
935
widgetPosition = position;
936
} else if (this._ui.rawValue?.position) {
937
// already showing - special case of line 1
938
if (this._ui.rawValue?.position.lineNumber === 1) {
939
widgetPosition = this._ui.rawValue?.position.delta(-1);
940
} else {
941
widgetPosition = this._ui.rawValue?.position;
942
}
943
} else {
944
// default to ABOVE the selection
945
widgetPosition = this._editor.getSelection().getStartPosition().delta(-1);
946
}
947
948
if (this._session && !position && (this._session.hasChangedText || this._session.chatModel.hasRequests)) {
949
widgetPosition = this._session.wholeRange.trackedInitialRange.getStartPosition().delta(-1);
950
}
951
952
if (initialRender && (this._editor.getOption(EditorOption.stickyScroll)).enabled) {
953
this._editor.revealLine(widgetPosition.lineNumber); // do NOT substract `this._editor.getOption(EditorOption.stickyScroll).maxLineCount` because the editor already does that
954
}
955
956
if (!headless) {
957
if (this._ui.rawValue?.position) {
958
this._ui.value.updatePositionAndHeight(widgetPosition);
959
} else {
960
this._ui.value.show(widgetPosition);
961
}
962
}
963
964
return widgetPosition;
965
}
966
967
private _resetWidget() {
968
969
this._sessionStore.clear();
970
this._ctxVisible.reset();
971
972
this._ui.rawValue?.hide();
973
974
// Return focus to the editor only if the current focus is within the editor widget
975
if (this._editor.hasWidgetFocus()) {
976
this._editor.focus();
977
}
978
}
979
980
private _updateCtxResponseType(): void {
981
982
if (!this._session) {
983
this._ctxResponseType.set(InlineChatResponseType.None);
984
return;
985
}
986
987
const hasLocalEdit = (response: IResponse): boolean => {
988
return response.value.some(part => part.kind === 'textEditGroup' && isEqual(part.uri, this._session?.textModelN.uri));
989
};
990
991
let responseType = InlineChatResponseType.None;
992
for (const request of this._session.chatModel.getRequests()) {
993
if (!request.response) {
994
continue;
995
}
996
responseType = InlineChatResponseType.Messages;
997
if (hasLocalEdit(request.response.response)) {
998
responseType = InlineChatResponseType.MessagesAndEdits;
999
break; // no need to check further
1000
}
1001
}
1002
this._ctxResponseType.set(responseType);
1003
this._ctxResponse.set(responseType !== InlineChatResponseType.None);
1004
}
1005
1006
private _createChatTextEditGroupState(): IChatTextEditGroupState {
1007
assertType(this._session);
1008
1009
const sha1 = new DefaultModelSHA1Computer();
1010
const textModel0Sha1 = sha1.canComputeSHA1(this._session.textModel0)
1011
? sha1.computeSHA1(this._session.textModel0)
1012
: generateUuid();
1013
1014
return {
1015
sha1: textModel0Sha1,
1016
applied: 0
1017
};
1018
}
1019
1020
private async _makeChanges(edits: TextEdit[], opts: ProgressingEditsOptions | undefined, undoStopBefore: boolean) {
1021
assertType(this._session);
1022
assertType(this._strategy);
1023
1024
const moreMinimalEdits = await this._editorWorkerService.computeMoreMinimalEdits(this._session.textModelN.uri, edits);
1025
this._log('edits from PROVIDER and after making them MORE MINIMAL', this._session.agent.extensionId, edits, moreMinimalEdits);
1026
1027
if (moreMinimalEdits?.length === 0) {
1028
// nothing left to do
1029
return;
1030
}
1031
1032
const actualEdits = !opts && moreMinimalEdits ? moreMinimalEdits : edits;
1033
const editOperations = actualEdits.map(TextEdit.asEditOperation);
1034
1035
const editsObserver: IEditObserver = {
1036
start: () => this._session!.hunkData.ignoreTextModelNChanges = true,
1037
stop: () => this._session!.hunkData.ignoreTextModelNChanges = false,
1038
};
1039
1040
const metadata = this._getMetadata();
1041
if (opts) {
1042
await this._strategy.makeProgressiveChanges(editOperations, editsObserver, opts, undoStopBefore, metadata);
1043
} else {
1044
await this._strategy.makeChanges(editOperations, editsObserver, undoStopBefore, metadata);
1045
}
1046
}
1047
1048
private _getMetadata(): IInlineChatMetadata {
1049
const lastRequest = this._session?.chatModel.lastRequest;
1050
return {
1051
extensionId: VersionedExtensionId.tryCreate(this._session?.agent.extensionId.value, this._session?.agent.extensionVersion),
1052
modelId: lastRequest?.modelId,
1053
requestId: lastRequest?.id,
1054
};
1055
}
1056
1057
private _updatePlaceholder(): void {
1058
this._ui.value.widget.placeholder = this._session?.agent.description ?? localize('askOrEditInContext', 'Ask or edit in context');
1059
}
1060
1061
private _updateInput(text: string, selectAll = true): void {
1062
1063
this._ui.value.widget.chatWidget.setInput(text);
1064
if (selectAll) {
1065
const newSelection = new Selection(1, 1, Number.MAX_SAFE_INTEGER, 1);
1066
this._ui.value.widget.chatWidget.inputEditor.setSelection(newSelection);
1067
}
1068
}
1069
1070
// ---- controller API
1071
1072
arrowOut(up: boolean): void {
1073
if (this._ui.value.position && this._editor.hasModel()) {
1074
const { column } = this._editor.getPosition();
1075
const { lineNumber } = this._ui.value.position;
1076
const newLine = up ? lineNumber : lineNumber + 1;
1077
this._editor.setPosition({ lineNumber: newLine, column });
1078
this._editor.focus();
1079
}
1080
}
1081
1082
focus(): void {
1083
this._ui.value.widget.focus();
1084
}
1085
1086
async viewInChat() {
1087
if (!this._strategy || !this._session) {
1088
return;
1089
}
1090
1091
let someApplied = false;
1092
let lastEdit: IChatTextEditGroup | undefined;
1093
1094
const uri = this._editor.getModel()?.uri;
1095
const requests = this._session.chatModel.getRequests();
1096
for (const request of requests) {
1097
if (!request.response) {
1098
continue;
1099
}
1100
for (const part of request.response.response.value) {
1101
if (part.kind === 'textEditGroup' && isEqual(part.uri, uri)) {
1102
// fully or partially applied edits
1103
someApplied = someApplied || Boolean(part.state?.applied);
1104
lastEdit = part;
1105
part.edits = [];
1106
part.state = undefined;
1107
}
1108
}
1109
}
1110
1111
const doEdits = this._strategy.cancel();
1112
1113
if (someApplied) {
1114
assertType(lastEdit);
1115
lastEdit.edits = [doEdits];
1116
}
1117
1118
await this._instaService.invokeFunction(moveToPanelChat, this._session?.chatModel);
1119
1120
this.cancelSession();
1121
}
1122
1123
acceptSession(): void {
1124
const response = this._session?.chatModel.getRequests().at(-1)?.response;
1125
if (response) {
1126
this._chatService.notifyUserAction({
1127
sessionId: response.session.sessionId,
1128
requestId: response.requestId,
1129
agentId: response.agent?.id,
1130
command: response.slashCommand?.name,
1131
result: response.result,
1132
action: {
1133
kind: 'inlineChat',
1134
action: 'accepted'
1135
}
1136
});
1137
}
1138
this._messages.fire(Message.ACCEPT_SESSION);
1139
}
1140
1141
acceptHunk(hunkInfo?: HunkInformation) {
1142
return this._strategy?.performHunkAction(hunkInfo, HunkAction.Accept);
1143
}
1144
1145
discardHunk(hunkInfo?: HunkInformation) {
1146
return this._strategy?.performHunkAction(hunkInfo, HunkAction.Discard);
1147
}
1148
1149
toggleDiff(hunkInfo?: HunkInformation) {
1150
return this._strategy?.performHunkAction(hunkInfo, HunkAction.ToggleDiff);
1151
}
1152
1153
moveHunk(next: boolean) {
1154
this.focus();
1155
this._strategy?.performHunkAction(undefined, next ? HunkAction.MoveNext : HunkAction.MovePrev);
1156
}
1157
1158
async cancelSession() {
1159
const response = this._session?.chatModel.lastRequest?.response;
1160
if (response) {
1161
this._chatService.notifyUserAction({
1162
sessionId: response.session.sessionId,
1163
requestId: response.requestId,
1164
agentId: response.agent?.id,
1165
command: response.slashCommand?.name,
1166
result: response.result,
1167
action: {
1168
kind: 'inlineChat',
1169
action: 'discarded'
1170
}
1171
});
1172
}
1173
1174
this._messages.fire(Message.CANCEL_SESSION);
1175
}
1176
1177
reportIssue() {
1178
const response = this._session?.chatModel.lastRequest?.response;
1179
if (response) {
1180
this._chatService.notifyUserAction({
1181
sessionId: response.session.sessionId,
1182
requestId: response.requestId,
1183
agentId: response.agent?.id,
1184
command: response.slashCommand?.name,
1185
result: response.result,
1186
action: { kind: 'bug' }
1187
});
1188
}
1189
}
1190
1191
unstashLastSession(): Session | undefined {
1192
const result = this._stashedSession.value?.unstash();
1193
return result;
1194
}
1195
1196
joinCurrentRun(): Promise<void> | undefined {
1197
return this._currentRun;
1198
}
1199
1200
get isActive() {
1201
return Boolean(this._currentRun);
1202
}
1203
1204
async createImageAttachment(attachment: URI): Promise<IChatRequestVariableEntry | undefined> {
1205
if (attachment.scheme === Schemas.file) {
1206
if (await this._fileService.canHandleResource(attachment)) {
1207
return await this._chatAttachmentResolveService.resolveImageEditorAttachContext(attachment);
1208
}
1209
} else if (attachment.scheme === Schemas.http || attachment.scheme === Schemas.https) {
1210
const extractedImages = await this._webContentExtractorService.readImage(attachment, CancellationToken.None);
1211
if (extractedImages) {
1212
return await this._chatAttachmentResolveService.resolveImageEditorAttachContext(attachment, extractedImages);
1213
}
1214
}
1215
1216
return undefined;
1217
}
1218
}
1219
1220
export class InlineChatController2 implements IEditorContribution {
1221
1222
static readonly ID = 'editor.contrib.inlineChatController2';
1223
1224
static get(editor: ICodeEditor): InlineChatController2 | undefined {
1225
return editor.getContribution<InlineChatController2>(InlineChatController2.ID) ?? undefined;
1226
}
1227
1228
private readonly _store = new DisposableStore();
1229
private readonly _showWidgetOverrideObs = observableValue(this, false);
1230
private readonly _isActiveController = observableValue(this, false);
1231
private readonly _zone: Lazy<InlineChatZoneWidget>;
1232
1233
private readonly _currentSession: IObservable<IInlineChatSession2 | undefined>;
1234
1235
get widget(): EditorBasedInlineChatWidget {
1236
return this._zone.value.widget;
1237
}
1238
1239
get isActive() {
1240
return Boolean(this._currentSession.get());
1241
}
1242
1243
constructor(
1244
private readonly _editor: ICodeEditor,
1245
@IInstantiationService private readonly _instaService: IInstantiationService,
1246
@INotebookEditorService private readonly _notebookEditorService: INotebookEditorService,
1247
@IInlineChatSessionService private readonly _inlineChatSessions: IInlineChatSessionService,
1248
@ICodeEditorService codeEditorService: ICodeEditorService,
1249
@IContextKeyService contextKeyService: IContextKeyService,
1250
@ISharedWebContentExtractorService private readonly _webContentExtractorService: ISharedWebContentExtractorService,
1251
@IFileService private readonly _fileService: IFileService,
1252
@IChatAttachmentResolveService private readonly _chatAttachmentResolveService: IChatAttachmentResolveService,
1253
@IEditorService private readonly _editorService: IEditorService,
1254
@IInlineChatSessionService inlineChatService: IInlineChatSessionService,
1255
@IConfigurationService configurationService: IConfigurationService,
1256
) {
1257
1258
const ctxInlineChatVisible = CTX_INLINE_CHAT_VISIBLE.bindTo(contextKeyService);
1259
1260
this._zone = new Lazy<InlineChatZoneWidget>(() => {
1261
1262
1263
const location: IChatWidgetLocationOptions = {
1264
location: ChatAgentLocation.Editor,
1265
resolveData: () => {
1266
assertType(this._editor.hasModel());
1267
1268
return {
1269
type: ChatAgentLocation.Editor,
1270
selection: this._editor.getSelection(),
1271
document: this._editor.getModel().uri,
1272
wholeRange: this._editor.getSelection(),
1273
};
1274
}
1275
};
1276
1277
// inline chat in notebooks
1278
// check if this editor is part of a notebook editor
1279
// if so, update the location and use the notebook specific widget
1280
let notebookEditor: INotebookEditor | undefined;
1281
for (const editor of this._notebookEditorService.listNotebookEditors()) {
1282
for (const [, codeEditor] of editor.codeEditors) {
1283
if (codeEditor === this._editor) {
1284
location.location = ChatAgentLocation.Notebook;
1285
notebookEditor = editor;
1286
// set location2 so that the notebook agent intent is used
1287
if (configurationService.getValue(InlineChatConfigKeys.notebookAgent)) {
1288
location.resolveData = () => {
1289
assertType(this._editor.hasModel());
1290
1291
return {
1292
type: ChatAgentLocation.Notebook,
1293
sessionInputUri: this._editor.getModel().uri,
1294
};
1295
};
1296
}
1297
1298
break;
1299
}
1300
}
1301
}
1302
1303
const result = this._instaService.createInstance(InlineChatZoneWidget,
1304
location,
1305
{
1306
enableWorkingSet: 'implicit',
1307
rendererOptions: {
1308
renderTextEditsAsSummary: _uri => true
1309
}
1310
},
1311
{ editor: this._editor, notebookEditor },
1312
);
1313
1314
result.domNode.classList.add('inline-chat-2');
1315
1316
return result;
1317
});
1318
1319
1320
const editorObs = observableCodeEditor(_editor);
1321
1322
const sessionsSignal = observableSignalFromEvent(this, _inlineChatSessions.onDidChangeSessions);
1323
1324
this._currentSession = derived(r => {
1325
sessionsSignal.read(r);
1326
const model = editorObs.model.read(r);
1327
const value = model && _inlineChatSessions.getSession2(model.uri);
1328
return value ?? undefined;
1329
});
1330
1331
1332
this._store.add(autorun(r => {
1333
const session = this._currentSession.read(r);
1334
if (!session) {
1335
this._isActiveController.set(false, undefined);
1336
return;
1337
}
1338
let foundOne = false;
1339
for (const editor of codeEditorService.listCodeEditors()) {
1340
if (Boolean(InlineChatController2.get(editor)?._isActiveController.get())) {
1341
foundOne = true;
1342
break;
1343
}
1344
}
1345
if (!foundOne && editorObs.isFocused.read(r)) {
1346
this._isActiveController.set(true, undefined);
1347
}
1348
}));
1349
1350
const visibleSessionObs = observableValue<IInlineChatSession2 | undefined>(this, undefined);
1351
1352
this._store.add(autorunWithStore((r, store) => {
1353
1354
const model = editorObs.model.read(r);
1355
const session = this._currentSession.read(r);
1356
const isActive = this._isActiveController.read(r);
1357
1358
if (!session || !isActive || !model) {
1359
visibleSessionObs.set(undefined, undefined);
1360
return;
1361
}
1362
1363
const { chatModel } = session;
1364
const showShowUntil = this._showWidgetOverrideObs.read(r);
1365
const hasNoRequests = chatModel.getRequests().length === 0;
1366
const hideOnRequest = inlineChatService.hideOnRequest.read(r);
1367
1368
const responseListener = store.add(new MutableDisposable());
1369
1370
if (hideOnRequest) {
1371
// hide the request once the request has been added, reveal it again when no edit was made
1372
// or when an error happened
1373
store.add(chatModel.onDidChange(e => {
1374
if (e.kind === 'addRequest') {
1375
transaction(tx => {
1376
this._showWidgetOverrideObs.set(false, tx);
1377
visibleSessionObs.set(undefined, tx);
1378
});
1379
const { response } = e.request;
1380
if (!response) {
1381
return;
1382
}
1383
responseListener.value = response.onDidChange(async e => {
1384
1385
if (!response.isComplete) {
1386
return;
1387
}
1388
1389
const shouldShow = response.isCanceled // cancelled
1390
|| response.result?.errorDetails // errors
1391
|| !response.response.value.find(part => part.kind === 'textEditGroup'
1392
&& part.edits.length > 0
1393
&& isEqual(part.uri, model.uri)); // NO edits for file
1394
1395
if (shouldShow) {
1396
visibleSessionObs.set(session, undefined);
1397
}
1398
});
1399
}
1400
}));
1401
}
1402
1403
if (showShowUntil || hasNoRequests || !hideOnRequest) {
1404
visibleSessionObs.set(session, undefined);
1405
} else {
1406
visibleSessionObs.set(undefined, undefined);
1407
}
1408
}));
1409
1410
this._store.add(autorun(r => {
1411
1412
const session = visibleSessionObs.read(r);
1413
1414
if (!session) {
1415
this._zone.rawValue?.hide();
1416
_editor.focus();
1417
ctxInlineChatVisible.reset();
1418
} else {
1419
ctxInlineChatVisible.set(true);
1420
this._zone.value.widget.setChatModel(session.chatModel);
1421
if (!this._zone.value.position) {
1422
this._zone.value.show(session.initialPosition);
1423
}
1424
this._zone.value.reveal(this._zone.value.position!);
1425
this._zone.value.widget.focus();
1426
this._zone.value.widget.updateToolbar(true);
1427
const entry = session.editingSession.getEntry(session.uri);
1428
1429
entry?.autoAcceptController.get()?.cancel();
1430
1431
const requestCount = observableFromEvent(this, session.chatModel.onDidChange, () => session.chatModel.getRequests().length).read(r);
1432
this._zone.value.widget.updateToolbar(requestCount > 0);
1433
}
1434
}));
1435
1436
this._store.add(autorun(r => {
1437
1438
const session = visibleSessionObs.read(r);
1439
const entry = session?.editingSession.readEntry(session.uri, r);
1440
const pane = this._editorService.visibleEditorPanes.find(candidate => candidate.getControl() === this._editor || isNotebookWithCellEditor(candidate, this._editor));
1441
if (pane && entry) {
1442
entry?.getEditorIntegration(pane);
1443
}
1444
}));
1445
}
1446
1447
dispose(): void {
1448
this._store.dispose();
1449
}
1450
1451
toggleWidgetUntilNextRequest() {
1452
const value = this._showWidgetOverrideObs.get();
1453
this._showWidgetOverrideObs.set(!value, undefined);
1454
}
1455
1456
getWidgetPosition(): Position | undefined {
1457
return this._zone.rawValue?.position;
1458
}
1459
1460
focus() {
1461
this._zone.rawValue?.widget.focus();
1462
}
1463
1464
markActiveController() {
1465
this._isActiveController.set(true, undefined);
1466
}
1467
1468
async run(arg?: InlineChatRunOptions): Promise<boolean> {
1469
assertType(this._editor.hasModel());
1470
1471
this.markActiveController();
1472
1473
const uri = this._editor.getModel().uri;
1474
const session = this._inlineChatSessions.getSession2(uri)
1475
?? await this._inlineChatSessions.createSession2(this._editor, uri, CancellationToken.None);
1476
1477
if (arg && InlineChatRunOptions.isInlineChatRunOptions(arg)) {
1478
if (arg.initialRange) {
1479
this._editor.revealRange(arg.initialRange);
1480
}
1481
if (arg.initialSelection) {
1482
this._editor.setSelection(arg.initialSelection);
1483
}
1484
if (arg.attachments) {
1485
await Promise.all(arg.attachments.map(async attachment => {
1486
await this._zone.value.widget.chatWidget.attachmentModel.addFile(attachment);
1487
}));
1488
delete arg.attachments;
1489
}
1490
if (arg.message) {
1491
this._zone.value.widget.chatWidget.setInput(arg.message);
1492
if (arg.autoSend) {
1493
await this._zone.value.widget.chatWidget.acceptInput();
1494
}
1495
}
1496
}
1497
1498
await Event.toPromise(session.editingSession.onDidDispose);
1499
1500
const rejected = session.editingSession.getEntry(uri)?.state.get() === ModifiedFileEntryState.Rejected;
1501
return !rejected;
1502
}
1503
1504
acceptSession() {
1505
const value = this._currentSession.get();
1506
value?.editingSession.accept();
1507
}
1508
1509
async createImageAttachment(attachment: URI): Promise<IChatRequestVariableEntry | undefined> {
1510
const value = this._currentSession.get();
1511
if (!value) {
1512
return undefined;
1513
}
1514
if (attachment.scheme === Schemas.file) {
1515
if (await this._fileService.canHandleResource(attachment)) {
1516
return await this._chatAttachmentResolveService.resolveImageEditorAttachContext(attachment);
1517
}
1518
} else if (attachment.scheme === Schemas.http || attachment.scheme === Schemas.https) {
1519
const extractedImages = await this._webContentExtractorService.readImage(attachment, CancellationToken.None);
1520
if (extractedImages) {
1521
return await this._chatAttachmentResolveService.resolveImageEditorAttachContext(attachment, extractedImages);
1522
}
1523
}
1524
return undefined;
1525
}
1526
}
1527
1528
export async function reviewEdits(accessor: ServicesAccessor, editor: ICodeEditor, stream: AsyncIterable<TextEdit[]>, token: CancellationToken, applyCodeBlockSuggestionId: EditSuggestionId | undefined): Promise<boolean> {
1529
if (!editor.hasModel()) {
1530
return false;
1531
}
1532
1533
const chatService = accessor.get(IChatService);
1534
const uri = editor.getModel().uri;
1535
const chatModel = chatService.startSession(ChatAgentLocation.Editor, token, false);
1536
1537
chatModel.startEditingSession(true);
1538
1539
const editSession = await chatModel.editingSessionObs?.promise;
1540
1541
const store = new DisposableStore();
1542
store.add(chatModel);
1543
1544
// STREAM
1545
const chatRequest = chatModel?.addRequest({ text: '', parts: [] }, { variables: [] }, 0, {
1546
kind: undefined,
1547
modeId: 'applyCodeBlock',
1548
instructions: undefined,
1549
isBuiltin: true,
1550
applyCodeBlockSuggestionId,
1551
});
1552
assertType(chatRequest.response);
1553
chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: [], done: false });
1554
for await (const chunk of stream) {
1555
1556
if (token.isCancellationRequested) {
1557
chatRequest.response.cancel();
1558
break;
1559
}
1560
1561
chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: chunk, done: false });
1562
}
1563
chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: [], done: true });
1564
1565
if (!token.isCancellationRequested) {
1566
chatModel.completeResponse(chatRequest);
1567
}
1568
1569
const isSettled = derived(r => {
1570
const entry = editSession?.readEntry(uri, r);
1571
if (!entry) {
1572
return false;
1573
}
1574
const state = entry.state.read(r);
1575
return state === ModifiedFileEntryState.Accepted || state === ModifiedFileEntryState.Rejected;
1576
});
1577
const whenDecided = waitForState(isSettled, Boolean);
1578
await raceCancellation(whenDecided, token);
1579
store.dispose();
1580
return true;
1581
}
1582
1583
export async function reviewNotebookEdits(accessor: ServicesAccessor, uri: URI, stream: AsyncIterable<[URI, TextEdit[]] | ICellEditOperation[]>, token: CancellationToken): Promise<boolean> {
1584
1585
const chatService = accessor.get(IChatService);
1586
const notebookService = accessor.get(INotebookService);
1587
const isNotebook = notebookService.hasSupportedNotebooks(uri);
1588
const chatModel = chatService.startSession(ChatAgentLocation.Editor, token, false);
1589
1590
chatModel.startEditingSession(true);
1591
1592
const editSession = await chatModel.editingSessionObs?.promise;
1593
1594
const store = new DisposableStore();
1595
store.add(chatModel);
1596
1597
// STREAM
1598
const chatRequest = chatModel?.addRequest({ text: '', parts: [] }, { variables: [] }, 0);
1599
assertType(chatRequest.response);
1600
if (isNotebook) {
1601
chatRequest.response.updateContent({ kind: 'notebookEdit', uri, edits: [], done: false });
1602
} else {
1603
chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: [], done: false });
1604
}
1605
for await (const chunk of stream) {
1606
1607
if (token.isCancellationRequested) {
1608
chatRequest.response.cancel();
1609
break;
1610
}
1611
if (chunk.every(isCellEditOperation)) {
1612
chatRequest.response.updateContent({ kind: 'notebookEdit', uri, edits: chunk, done: false });
1613
} else {
1614
chatRequest.response.updateContent({ kind: 'textEdit', uri: chunk[0], edits: chunk[1], done: false });
1615
}
1616
}
1617
if (isNotebook) {
1618
chatRequest.response.updateContent({ kind: 'notebookEdit', uri, edits: [], done: true });
1619
} else {
1620
chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: [], done: true });
1621
}
1622
1623
if (!token.isCancellationRequested) {
1624
chatRequest.response.complete();
1625
}
1626
1627
const isSettled = derived(r => {
1628
const entry = editSession?.readEntry(uri, r);
1629
if (!entry) {
1630
return false;
1631
}
1632
const state = entry.state.read(r);
1633
return state === ModifiedFileEntryState.Accepted || state === ModifiedFileEntryState.Rejected;
1634
});
1635
1636
const whenDecided = waitForState(isSettled, Boolean);
1637
1638
await raceCancellation(whenDecided, token);
1639
1640
store.dispose();
1641
1642
return true;
1643
}
1644
1645
function isCellEditOperation(edit: URI | TextEdit[] | ICellEditOperation): edit is ICellEditOperation {
1646
if (URI.isUri(edit)) {
1647
return false;
1648
}
1649
if (Array.isArray(edit)) {
1650
return false;
1651
}
1652
return true;
1653
}
1654
1655
async function moveToPanelChat(accessor: ServicesAccessor, model: ChatModel | undefined) {
1656
1657
const viewsService = accessor.get(IViewsService);
1658
const chatService = accessor.get(IChatService);
1659
1660
const widget = await showChatView(viewsService);
1661
1662
if (widget && widget.viewModel && model) {
1663
for (const request of model.getRequests().slice()) {
1664
await chatService.adoptRequest(widget.viewModel.model.sessionId, request);
1665
}
1666
widget.focusLastMessage();
1667
}
1668
}
1669
1670