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
5236 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 { renderAsPlaintext } from '../../../../base/browser/markdownRenderer.js';
7
import { alert } from '../../../../base/browser/ui/aria/aria.js';
8
import { raceCancellation } from '../../../../base/common/async.js';
9
import { CancellationToken } from '../../../../base/common/cancellation.js';
10
import { onUnexpectedError } from '../../../../base/common/errors.js';
11
import { Event } from '../../../../base/common/event.js';
12
import { Lazy } from '../../../../base/common/lazy.js';
13
import { DisposableStore } from '../../../../base/common/lifecycle.js';
14
import { Schemas } from '../../../../base/common/network.js';
15
import { autorun, derived, IObservable, observableFromEvent, observableSignalFromEvent, observableValue, waitForState } from '../../../../base/common/observable.js';
16
import { isEqual } from '../../../../base/common/resources.js';
17
import { assertType } from '../../../../base/common/types.js';
18
import { URI } from '../../../../base/common/uri.js';
19
import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js';
20
import { observableCodeEditor } from '../../../../editor/browser/observableCodeEditor.js';
21
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
22
import { IPosition, Position } from '../../../../editor/common/core/position.js';
23
import { IRange, Range } from '../../../../editor/common/core/range.js';
24
import { ISelection, Selection } from '../../../../editor/common/core/selection.js';
25
import { IEditorContribution } from '../../../../editor/common/editorCommon.js';
26
import { TextEdit } from '../../../../editor/common/languages.js';
27
import { ITextModel } from '../../../../editor/common/model.js';
28
import { IMarkerDecorationsService } from '../../../../editor/common/services/markerDecorations.js';
29
import { EditSuggestionId } from '../../../../editor/common/textModelEditSource.js';
30
import { localize } from '../../../../nls.js';
31
import { MenuId } from '../../../../platform/actions/common/actions.js';
32
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
33
import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
34
import { IFileService } from '../../../../platform/files/common/files.js';
35
import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
36
import { ILogService } from '../../../../platform/log/common/log.js';
37
import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js';
38
import { ISharedWebContentExtractorService } from '../../../../platform/webContentExtractor/common/webContentExtractor.js';
39
import { IEditorService, SIDE_GROUP } from '../../../services/editor/common/editorService.js';
40
import { IChatAttachmentResolveService } from '../../chat/browser/attachments/chatAttachmentResolveService.js';
41
import { IChatWidgetLocationOptions } from '../../chat/browser/widget/chatWidget.js';
42
import { ModifiedFileEntryState } from '../../chat/common/editing/chatEditingService.js';
43
import { ChatModel } from '../../chat/common/model/chatModel.js';
44
import { ChatMode } from '../../chat/common/chatModes.js';
45
import { IChatService } from '../../chat/common/chatService/chatService.js';
46
import { IChatRequestVariableEntry, IDiagnosticVariableEntryFilterData } from '../../chat/common/attachments/chatVariableEntries.js';
47
import { isResponseVM } from '../../chat/common/model/chatViewModel.js';
48
import { ChatAgentLocation } from '../../chat/common/constants.js';
49
import { ILanguageModelChatMetadata, ILanguageModelChatSelector, ILanguageModelsService, isILanguageModelChatSelector } from '../../chat/common/languageModels.js';
50
import { isNotebookContainingCellEditor as isNotebookWithCellEditor } from '../../notebook/browser/notebookEditor.js';
51
import { INotebookEditorService } from '../../notebook/browser/services/notebookEditorService.js';
52
import { CellUri, ICellEditOperation } from '../../notebook/common/notebookCommon.js';
53
import { INotebookService } from '../../notebook/common/notebookService.js';
54
import { CTX_INLINE_CHAT_VISIBLE, InlineChatConfigKeys } from '../common/inlineChat.js';
55
import { InlineChatAffordance } from './inlineChatAffordance.js';
56
import { InlineChatInputWidget, InlineChatSessionOverlayWidget } from './inlineChatOverlayWidget.js';
57
import { IInlineChatSession2, IInlineChatSessionService } from './inlineChatSessionService.js';
58
import { EditorBasedInlineChatWidget } from './inlineChatWidget.js';
59
import { InlineChatZoneWidget } from './inlineChatZoneWidget.js';
60
61
62
export abstract class InlineChatRunOptions {
63
64
initialSelection?: ISelection;
65
initialRange?: IRange;
66
message?: string;
67
attachments?: URI[];
68
autoSend?: boolean;
69
position?: IPosition;
70
modelSelector?: ILanguageModelChatSelector;
71
resolveOnResponse?: boolean;
72
73
static isInlineChatRunOptions(options: unknown): options is InlineChatRunOptions {
74
75
if (typeof options !== 'object' || options === null) {
76
return false;
77
}
78
79
const { initialSelection, initialRange, message, autoSend, position, attachments, modelSelector, resolveOnResponse } = <InlineChatRunOptions>options;
80
if (
81
typeof message !== 'undefined' && typeof message !== 'string'
82
|| typeof autoSend !== 'undefined' && typeof autoSend !== 'boolean'
83
|| typeof initialRange !== 'undefined' && !Range.isIRange(initialRange)
84
|| typeof initialSelection !== 'undefined' && !Selection.isISelection(initialSelection)
85
|| typeof position !== 'undefined' && !Position.isIPosition(position)
86
|| typeof attachments !== 'undefined' && (!Array.isArray(attachments) || !attachments.every(item => item instanceof URI))
87
|| typeof modelSelector !== 'undefined' && !isILanguageModelChatSelector(modelSelector)
88
|| typeof resolveOnResponse !== 'undefined' && typeof resolveOnResponse !== 'boolean'
89
) {
90
return false;
91
}
92
93
return true;
94
}
95
}
96
97
// TODO@jrieken THIS should be shared with the code in MainThreadEditors
98
function getEditorId(editor: ICodeEditor, model: ITextModel): string {
99
return `${editor.getId()},${model.id}`;
100
}
101
102
export class InlineChatController implements IEditorContribution {
103
104
static readonly ID = 'editor.contrib.inlineChatController';
105
106
static get(editor: ICodeEditor): InlineChatController | undefined {
107
return editor.getContribution<InlineChatController>(InlineChatController.ID) ?? undefined;
108
}
109
110
/**
111
* Stores the user's explicitly chosen model (qualified name) from a previous inline chat request in the same session.
112
* When set, this takes priority over the inlineChat.defaultModel setting.
113
*/
114
private static _userSelectedModel: string | undefined;
115
116
private readonly _store = new DisposableStore();
117
private readonly _isActiveController = observableValue(this, false);
118
private readonly _renderMode: IObservable<'zone' | 'hover'>;
119
private readonly _zone: Lazy<InlineChatZoneWidget>;
120
private readonly _gutterIndicator: InlineChatAffordance;
121
122
private readonly _currentSession: IObservable<IInlineChatSession2 | undefined>;
123
124
get widget(): EditorBasedInlineChatWidget {
125
return this._zone.value.widget;
126
}
127
128
get isActive() {
129
return Boolean(this._currentSession.get());
130
}
131
132
constructor(
133
private readonly _editor: ICodeEditor,
134
@IInstantiationService private readonly _instaService: IInstantiationService,
135
@INotebookEditorService private readonly _notebookEditorService: INotebookEditorService,
136
@IInlineChatSessionService private readonly _inlineChatSessionService: IInlineChatSessionService,
137
@ICodeEditorService codeEditorService: ICodeEditorService,
138
@IContextKeyService contextKeyService: IContextKeyService,
139
@IConfigurationService private readonly _configurationService: IConfigurationService,
140
@ISharedWebContentExtractorService private readonly _webContentExtractorService: ISharedWebContentExtractorService,
141
@IFileService private readonly _fileService: IFileService,
142
@IChatAttachmentResolveService private readonly _chatAttachmentResolveService: IChatAttachmentResolveService,
143
@IEditorService private readonly _editorService: IEditorService,
144
@IMarkerDecorationsService private readonly _markerDecorationsService: IMarkerDecorationsService,
145
@ILanguageModelsService private readonly _languageModelService: ILanguageModelsService,
146
@ILogService private readonly _logService: ILogService,
147
) {
148
const editorObs = observableCodeEditor(_editor);
149
150
151
const ctxInlineChatVisible = CTX_INLINE_CHAT_VISIBLE.bindTo(contextKeyService);
152
const notebookAgentConfig = observableConfigValue(InlineChatConfigKeys.notebookAgent, false, this._configurationService);
153
this._renderMode = observableConfigValue(InlineChatConfigKeys.RenderMode, 'zone', this._configurationService);
154
155
const overlayWidget = this._store.add(this._instaService.createInstance(InlineChatInputWidget, editorObs));
156
const sessionOverlayWidget = this._store.add(this._instaService.createInstance(InlineChatSessionOverlayWidget, editorObs));
157
this._gutterIndicator = this._store.add(this._instaService.createInstance(InlineChatAffordance, this._editor, overlayWidget));
158
159
this._zone = new Lazy<InlineChatZoneWidget>(() => {
160
161
assertType(this._editor.hasModel(), '[Illegal State] widget should only be created when the editor has a model');
162
163
const location: IChatWidgetLocationOptions = {
164
location: ChatAgentLocation.EditorInline,
165
resolveData: () => {
166
assertType(this._editor.hasModel());
167
const wholeRange = this._editor.getSelection();
168
const document = this._editor.getModel().uri;
169
170
return {
171
type: ChatAgentLocation.EditorInline,
172
id: getEditorId(this._editor, this._editor.getModel()),
173
selection: this._editor.getSelection(),
174
document,
175
wholeRange
176
};
177
}
178
};
179
180
// inline chat in notebooks
181
// check if this editor is part of a notebook editor
182
// if so, update the location and use the notebook specific widget
183
const notebookEditor = this._notebookEditorService.getNotebookForPossibleCell(this._editor);
184
if (!!notebookEditor) {
185
location.location = ChatAgentLocation.Notebook;
186
if (notebookAgentConfig.get()) {
187
location.resolveData = () => {
188
assertType(this._editor.hasModel());
189
190
return {
191
type: ChatAgentLocation.Notebook,
192
sessionInputUri: this._editor.getModel().uri,
193
};
194
};
195
}
196
}
197
198
const result = this._instaService.createInstance(InlineChatZoneWidget,
199
location,
200
{
201
enableWorkingSet: 'implicit',
202
enableImplicitContext: false,
203
renderInputOnTop: false,
204
renderInputToolbarBelowInput: true,
205
filter: item => {
206
if (!isResponseVM(item)) {
207
return false;
208
}
209
return !!item.model.isPendingConfirmation.get();
210
},
211
menus: {
212
telemetrySource: 'inlineChatWidget',
213
executeToolbar: MenuId.ChatEditorInlineExecute,
214
inputSideToolbar: MenuId.ChatEditorInlineInputSide
215
},
216
defaultMode: ChatMode.Ask
217
},
218
{ editor: this._editor, notebookEditor },
219
() => Promise.resolve(),
220
);
221
222
this._store.add(result);
223
224
result.domNode.classList.add('inline-chat-2');
225
226
return result;
227
});
228
229
230
231
const sessionsSignal = observableSignalFromEvent(this, _inlineChatSessionService.onDidChangeSessions);
232
233
this._currentSession = derived(r => {
234
sessionsSignal.read(r);
235
const model = editorObs.model.read(r);
236
const session = model && _inlineChatSessionService.getSessionByTextModel(model.uri);
237
return session ?? undefined;
238
});
239
240
241
let lastSession: IInlineChatSession2 | undefined = undefined;
242
243
this._store.add(autorun(r => {
244
const session = this._currentSession.read(r);
245
if (!session) {
246
this._isActiveController.set(false, undefined);
247
248
if (lastSession && !lastSession.chatModel.hasRequests) {
249
const state = lastSession.chatModel.inputModel.state.read(undefined);
250
if (!state || (!state.inputText && state.attachments.length === 0)) {
251
lastSession.dispose();
252
lastSession = undefined;
253
}
254
}
255
return;
256
}
257
258
lastSession = session;
259
260
let foundOne = false;
261
for (const editor of codeEditorService.listCodeEditors()) {
262
if (Boolean(InlineChatController.get(editor)?._isActiveController.read(undefined))) {
263
foundOne = true;
264
break;
265
}
266
}
267
if (!foundOne && editorObs.isFocused.read(r)) {
268
this._isActiveController.set(true, undefined);
269
}
270
}));
271
272
const visibleSessionObs = observableValue<IInlineChatSession2 | undefined>(this, undefined);
273
274
this._store.add(autorun(r => {
275
276
const model = editorObs.model.read(r);
277
const session = this._currentSession.read(r);
278
const isActive = this._isActiveController.read(r);
279
280
if (!session || !isActive || !model) {
281
visibleSessionObs.set(undefined, undefined);
282
} else {
283
visibleSessionObs.set(session, undefined);
284
}
285
}));
286
287
const defaultPlaceholderObs = visibleSessionObs.map((session, r) => {
288
return session?.initialSelection.isEmpty()
289
? localize('placeholder', "Generate code")
290
: localize('placeholderWithSelection', "Modify selected code");
291
});
292
293
294
this._store.add(autorun(r => {
295
296
// HIDE/SHOW
297
const session = visibleSessionObs.read(r);
298
const renderMode = this._renderMode.read(r);
299
if (!session) {
300
this._zone.rawValue?.hide();
301
this._zone.rawValue?.widget.chatWidget.setModel(undefined);
302
_editor.focus();
303
ctxInlineChatVisible.reset();
304
} else if (renderMode === 'hover') {
305
// hover mode: set model but don't show zone, keep focus in editor
306
this._zone.value.widget.chatWidget.setModel(session.chatModel);
307
this._zone.rawValue?.hide();
308
ctxInlineChatVisible.set(true);
309
} else {
310
ctxInlineChatVisible.set(true);
311
this._zone.value.widget.chatWidget.setModel(session.chatModel);
312
if (!this._zone.value.position) {
313
this._zone.value.widget.chatWidget.setInputPlaceholder(defaultPlaceholderObs.read(r));
314
this._zone.value.widget.chatWidget.input.renderAttachedContext(); // TODO - fights layout bug
315
this._zone.value.show(session.initialPosition);
316
}
317
this._zone.value.reveal(this._zone.value.position!);
318
this._zone.value.widget.focus();
319
}
320
}));
321
322
// Show progress overlay widget in hover mode when a request is in progress or edits are not yet settled
323
this._store.add(autorun(r => {
324
const session = visibleSessionObs.read(r);
325
const renderMode = this._renderMode.read(r);
326
if (!session || renderMode !== 'hover') {
327
sessionOverlayWidget.hide();
328
return;
329
}
330
const lastRequest = session.chatModel.lastRequestObs.read(r);
331
const isInProgress = lastRequest?.response?.isInProgress.read(r);
332
const entry = session.editingSession.readEntry(session.uri, r);
333
// When there's no entry (no changes made) and the response is complete, the widget should be hidden.
334
// When there's an entry in Modified state, it needs to be settled (accepted/rejected).
335
const isNotSettled = entry ? entry.state.read(r) === ModifiedFileEntryState.Modified : false;
336
if (isInProgress || isNotSettled) {
337
sessionOverlayWidget.show(session);
338
} else {
339
sessionOverlayWidget.hide();
340
}
341
}));
342
343
this._store.add(autorun(r => {
344
const session = visibleSessionObs.read(r);
345
if (session) {
346
const entries = session.editingSession.entries.read(r);
347
const sessionCellUri = CellUri.parse(session.uri);
348
const otherEntries = entries.filter(entry => {
349
if (isEqual(entry.modifiedURI, session.uri)) {
350
return false;
351
}
352
// Don't count notebooks that include the session's cell
353
if (!!sessionCellUri && isEqual(sessionCellUri.notebook, entry.modifiedURI)) {
354
return false;
355
}
356
return true;
357
});
358
for (const entry of otherEntries) {
359
// OPEN other modified files in side group. This is a workaround, temp-solution until we have no more backend
360
// that modifies other files
361
this._editorService.openEditor({ resource: entry.modifiedURI }, SIDE_GROUP).catch(onUnexpectedError);
362
}
363
}
364
}));
365
366
const lastResponseObs = visibleSessionObs.map((session, r) => {
367
if (!session) {
368
return;
369
}
370
const lastRequest = observableFromEvent(this, session.chatModel.onDidChange, () => session.chatModel.getRequests().at(-1)).read(r);
371
return lastRequest?.response;
372
});
373
374
const lastResponseProgressObs = lastResponseObs.map((response, r) => {
375
if (!response) {
376
return;
377
}
378
return observableFromEvent(this, response.onDidChange, () => response.response.value.findLast(part => part.kind === 'progressMessage')).read(r);
379
});
380
381
382
this._store.add(autorun(r => {
383
const response = lastResponseObs.read(r);
384
385
this._zone.rawValue?.widget.updateInfo('');
386
387
if (!response?.isInProgress.read(r)) {
388
389
if (response?.result?.errorDetails) {
390
// ERROR case
391
this._zone.rawValue?.widget.updateInfo(`$(error) ${response.result.errorDetails.message}`);
392
alert(response.result.errorDetails.message);
393
}
394
395
// no response or not in progress
396
this._zone.rawValue?.widget.domNode.classList.toggle('request-in-progress', false);
397
this._zone.rawValue?.widget.chatWidget.setInputPlaceholder(defaultPlaceholderObs.read(r));
398
399
} else {
400
this._zone.rawValue?.widget.domNode.classList.toggle('request-in-progress', true);
401
let placeholder = response.request?.message.text;
402
const lastProgress = lastResponseProgressObs.read(r);
403
if (lastProgress) {
404
placeholder = renderAsPlaintext(lastProgress.content);
405
}
406
this._zone.rawValue?.widget.chatWidget.setInputPlaceholder(placeholder || localize('loading', "Working..."));
407
}
408
409
}));
410
411
this._store.add(autorun(r => {
412
const session = visibleSessionObs.read(r);
413
if (!session) {
414
return;
415
}
416
417
const entry = session.editingSession.readEntry(session.uri, r);
418
if (entry?.state.read(r) === ModifiedFileEntryState.Modified) {
419
entry?.enableReviewModeUntilSettled();
420
}
421
}));
422
423
424
this._store.add(autorun(r => {
425
426
const session = visibleSessionObs.read(r);
427
const entry = session?.editingSession.readEntry(session.uri, r);
428
429
// make sure there is an editor integration
430
const pane = this._editorService.visibleEditorPanes.find(candidate => candidate.getControl() === this._editor || isNotebookWithCellEditor(candidate, this._editor));
431
if (pane && entry) {
432
entry?.getEditorIntegration(pane);
433
}
434
435
// make sure the ZONE isn't inbetween a diff and move above if so
436
if (entry?.diffInfo && this._zone.value.position) {
437
const { position } = this._zone.value;
438
const diff = entry.diffInfo.read(r);
439
440
for (const change of diff.changes) {
441
if (change.modified.contains(position.lineNumber)) {
442
this._zone.value.updatePositionAndHeight(new Position(change.modified.startLineNumber - 1, 1));
443
break;
444
}
445
}
446
}
447
}));
448
}
449
450
dispose(): void {
451
this._store.dispose();
452
}
453
454
getWidgetPosition(): Position | undefined {
455
return this._zone.rawValue?.position;
456
}
457
458
focus() {
459
this._zone.rawValue?.widget.focus();
460
}
461
462
async run(arg?: InlineChatRunOptions): Promise<boolean> {
463
assertType(this._editor.hasModel());
464
const uri = this._editor.getModel().uri;
465
466
const existingSession = this._inlineChatSessionService.getSessionByTextModel(uri);
467
if (existingSession) {
468
await existingSession.editingSession.accept();
469
existingSession.dispose();
470
}
471
472
// use hover overlay to ask for input
473
if (!arg?.message && this._configurationService.getValue<string>(InlineChatConfigKeys.RenderMode) === 'hover') {
474
// show menu and RETURN because the menu is re-entrant
475
await this._gutterIndicator.showMenuAtSelection();
476
return true;
477
}
478
479
this._isActiveController.set(true, undefined);
480
481
const session = this._inlineChatSessionService.createSession(this._editor);
482
483
484
// Store for tracking model changes during this session
485
const sessionStore = new DisposableStore();
486
487
try {
488
await this._applyModelDefaults(session, sessionStore);
489
490
// ADD diagnostics
491
const entries: IChatRequestVariableEntry[] = [];
492
for (const [range, marker] of this._markerDecorationsService.getLiveMarkers(uri)) {
493
if (range.intersectRanges(this._editor.getSelection())) {
494
const filter = IDiagnosticVariableEntryFilterData.fromMarker(marker);
495
entries.push(IDiagnosticVariableEntryFilterData.toEntry(filter));
496
}
497
}
498
if (entries.length > 0) {
499
this._zone.value.widget.chatWidget.attachmentModel.addContext(...entries);
500
this._zone.value.widget.chatWidget.input.setValue(entries.length > 1
501
? localize('fixN', "Fix the attached problems")
502
: localize('fix1', "Fix the attached problem"),
503
true
504
);
505
this._zone.value.widget.chatWidget.inputEditor.setSelection(new Selection(1, 1, Number.MAX_SAFE_INTEGER, 1));
506
}
507
508
// Check args
509
if (arg && InlineChatRunOptions.isInlineChatRunOptions(arg)) {
510
if (arg.initialRange) {
511
this._editor.revealRange(arg.initialRange);
512
}
513
if (arg.initialSelection) {
514
this._editor.setSelection(arg.initialSelection);
515
}
516
if (arg.attachments) {
517
await Promise.all(arg.attachments.map(async attachment => {
518
await this._zone.value.widget.chatWidget.attachmentModel.addFile(attachment);
519
}));
520
delete arg.attachments;
521
}
522
if (arg.modelSelector) {
523
const id = (await this._languageModelService.selectLanguageModels(arg.modelSelector)).sort().at(0);
524
if (!id) {
525
throw new Error(`No language models found matching selector: ${JSON.stringify(arg.modelSelector)}.`);
526
}
527
const model = this._languageModelService.lookupLanguageModel(id);
528
if (!model) {
529
throw new Error(`Language model not loaded: ${id}.`);
530
}
531
this._zone.value.widget.chatWidget.input.setCurrentLanguageModel({ metadata: model, identifier: id });
532
}
533
if (arg.message) {
534
this._zone.value.widget.chatWidget.setInput(arg.message);
535
if (arg.autoSend) {
536
await this._zone.value.widget.chatWidget.acceptInput();
537
}
538
}
539
}
540
541
if (!arg?.resolveOnResponse) {
542
// DEFAULT: wait for the session to be accepted or rejected
543
await Event.toPromise(session.editingSession.onDidDispose);
544
const rejected = session.editingSession.getEntry(uri)?.state.get() === ModifiedFileEntryState.Rejected;
545
return !rejected;
546
547
} else {
548
// resolveOnResponse: ONLY wait for the file to be modified
549
const modifiedObs = derived(r => {
550
const entry = session.editingSession.readEntry(uri, r);
551
return entry?.state.read(r) === ModifiedFileEntryState.Modified && !entry?.isCurrentlyBeingModifiedBy.read(r);
552
});
553
await waitForState(modifiedObs, state => state === true);
554
return true;
555
}
556
} finally {
557
sessionStore.dispose();
558
}
559
}
560
561
async acceptSession() {
562
const session = this._currentSession.get();
563
if (!session) {
564
return;
565
}
566
await session.editingSession.accept();
567
session.dispose();
568
}
569
570
async rejectSession() {
571
const session = this._currentSession.get();
572
if (!session) {
573
return;
574
}
575
await session.editingSession.reject();
576
session.dispose();
577
}
578
579
private async _selectVendorDefaultModel(session: IInlineChatSession2): Promise<void> {
580
const model = this._zone.value.widget.chatWidget.input.selectedLanguageModel.get();
581
if (model && !model.metadata.isDefaultForLocation[session.chatModel.initialLocation]) {
582
const ids = await this._languageModelService.selectLanguageModels({ vendor: model.metadata.vendor });
583
for (const identifier of ids) {
584
const candidate = this._languageModelService.lookupLanguageModel(identifier);
585
if (candidate?.isDefaultForLocation[session.chatModel.initialLocation]) {
586
this._zone.value.widget.chatWidget.input.setCurrentLanguageModel({ metadata: candidate, identifier });
587
break;
588
}
589
}
590
}
591
}
592
593
/**
594
* Applies model defaults based on settings and tracks user model changes.
595
* Prioritization: user session choice > inlineChat.defaultModel setting > vendor default
596
*/
597
private async _applyModelDefaults(session: IInlineChatSession2, sessionStore: DisposableStore): Promise<void> {
598
const userSelectedModel = InlineChatController._userSelectedModel;
599
const defaultModelSetting = this._configurationService.getValue<string>(InlineChatConfigKeys.DefaultModel);
600
601
let modelApplied = false;
602
603
// 1. Try user's explicitly chosen model from a previous inline chat in the same session
604
if (userSelectedModel) {
605
modelApplied = this._zone.value.widget.chatWidget.input.switchModelByQualifiedName([userSelectedModel]);
606
if (!modelApplied) {
607
// User's previously selected model is no longer available, clear it
608
InlineChatController._userSelectedModel = undefined;
609
}
610
}
611
612
// 2. Try inlineChat.defaultModel setting
613
if (!modelApplied && defaultModelSetting) {
614
modelApplied = this._zone.value.widget.chatWidget.input.switchModelByQualifiedName([defaultModelSetting]);
615
if (!modelApplied) {
616
this._logService.warn(`inlineChat.defaultModel setting value '${defaultModelSetting}' did not match any available model. Falling back to vendor default.`);
617
}
618
}
619
620
// 3. Fall back to vendor default
621
if (!modelApplied) {
622
await this._selectVendorDefaultModel(session);
623
}
624
625
// Track model changes - store user's explicit choice in the given sessions.
626
// NOTE: This currently detects any model change, not just user-initiated ones.
627
let initialModelId: string | undefined;
628
sessionStore.add(autorun(r => {
629
const newModel = this._zone.value.widget.chatWidget.input.selectedLanguageModel.read(r);
630
if (!newModel) {
631
return;
632
}
633
if (!initialModelId) {
634
initialModelId = newModel.identifier;
635
return;
636
}
637
if (initialModelId !== newModel.identifier) {
638
// User explicitly changed model, store their choice as qualified name
639
InlineChatController._userSelectedModel = ILanguageModelChatMetadata.asQualifiedName(newModel.metadata);
640
initialModelId = newModel.identifier;
641
}
642
}));
643
}
644
645
async createImageAttachment(attachment: URI): Promise<IChatRequestVariableEntry | undefined> {
646
const value = this._currentSession.get();
647
if (!value) {
648
return undefined;
649
}
650
if (attachment.scheme === Schemas.file) {
651
if (await this._fileService.canHandleResource(attachment)) {
652
return await this._chatAttachmentResolveService.resolveImageEditorAttachContext(attachment);
653
}
654
} else if (attachment.scheme === Schemas.http || attachment.scheme === Schemas.https) {
655
const extractedImages = await this._webContentExtractorService.readImage(attachment, CancellationToken.None);
656
if (extractedImages) {
657
return await this._chatAttachmentResolveService.resolveImageEditorAttachContext(attachment, extractedImages);
658
}
659
}
660
return undefined;
661
}
662
}
663
664
export async function reviewEdits(accessor: ServicesAccessor, editor: ICodeEditor, stream: AsyncIterable<TextEdit[]>, token: CancellationToken, applyCodeBlockSuggestionId: EditSuggestionId | undefined): Promise<boolean> {
665
if (!editor.hasModel()) {
666
return false;
667
}
668
669
const chatService = accessor.get(IChatService);
670
const uri = editor.getModel().uri;
671
const chatModelRef = chatService.startSession(ChatAgentLocation.EditorInline);
672
const chatModel = chatModelRef.object as ChatModel;
673
674
chatModel.startEditingSession(true);
675
676
const store = new DisposableStore();
677
store.add(chatModelRef);
678
679
// STREAM
680
const chatRequest = chatModel?.addRequest({ text: '', parts: [] }, { variables: [] }, 0, {
681
kind: undefined,
682
modeId: 'applyCodeBlock',
683
modeInstructions: undefined,
684
isBuiltin: true,
685
applyCodeBlockSuggestionId,
686
});
687
assertType(chatRequest.response);
688
chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: [], done: false });
689
for await (const chunk of stream) {
690
691
if (token.isCancellationRequested) {
692
chatRequest.response.cancel();
693
break;
694
}
695
696
chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: chunk, done: false });
697
}
698
chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: [], done: true });
699
700
if (!token.isCancellationRequested) {
701
chatRequest.response.complete();
702
}
703
704
const isSettled = derived(r => {
705
const entry = chatModel.editingSession?.readEntry(uri, r);
706
if (!entry) {
707
return false;
708
}
709
const state = entry.state.read(r);
710
return state === ModifiedFileEntryState.Accepted || state === ModifiedFileEntryState.Rejected;
711
});
712
const whenDecided = waitForState(isSettled, Boolean);
713
await raceCancellation(whenDecided, token);
714
store.dispose();
715
return true;
716
}
717
718
export async function reviewNotebookEdits(accessor: ServicesAccessor, uri: URI, stream: AsyncIterable<[URI, TextEdit[]] | ICellEditOperation[]>, token: CancellationToken): Promise<boolean> {
719
720
const chatService = accessor.get(IChatService);
721
const notebookService = accessor.get(INotebookService);
722
const isNotebook = notebookService.hasSupportedNotebooks(uri);
723
const chatModelRef = chatService.startSession(ChatAgentLocation.EditorInline);
724
const chatModel = chatModelRef.object as ChatModel;
725
726
chatModel.startEditingSession(true);
727
728
const store = new DisposableStore();
729
store.add(chatModelRef);
730
731
// STREAM
732
const chatRequest = chatModel?.addRequest({ text: '', parts: [] }, { variables: [] }, 0);
733
assertType(chatRequest.response);
734
if (isNotebook) {
735
chatRequest.response.updateContent({ kind: 'notebookEdit', uri, edits: [], done: false });
736
} else {
737
chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: [], done: false });
738
}
739
for await (const chunk of stream) {
740
741
if (token.isCancellationRequested) {
742
chatRequest.response.cancel();
743
break;
744
}
745
if (chunk.every(isCellEditOperation)) {
746
chatRequest.response.updateContent({ kind: 'notebookEdit', uri, edits: chunk, done: false });
747
} else {
748
chatRequest.response.updateContent({ kind: 'textEdit', uri: chunk[0], edits: chunk[1], done: false });
749
}
750
}
751
if (isNotebook) {
752
chatRequest.response.updateContent({ kind: 'notebookEdit', uri, edits: [], done: true });
753
} else {
754
chatRequest.response.updateContent({ kind: 'textEdit', uri, edits: [], done: true });
755
}
756
757
if (!token.isCancellationRequested) {
758
chatRequest.response.complete();
759
}
760
761
const isSettled = derived(r => {
762
const entry = chatModel.editingSession?.readEntry(uri, r);
763
if (!entry) {
764
return false;
765
}
766
const state = entry.state.read(r);
767
return state === ModifiedFileEntryState.Accepted || state === ModifiedFileEntryState.Rejected;
768
});
769
770
const whenDecided = waitForState(isSettled, Boolean);
771
772
await raceCancellation(whenDecided, token);
773
774
store.dispose();
775
776
return true;
777
}
778
779
function isCellEditOperation(edit: URI | TextEdit[] | ICellEditOperation): edit is ICellEditOperation {
780
if (URI.isUri(edit)) {
781
return false;
782
}
783
if (Array.isArray(edit)) {
784
return false;
785
}
786
return true;
787
}
788
789