Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/chatInputPart.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 dom from '../../../../base/browser/dom.js';
7
import { addDisposableListener } from '../../../../base/browser/dom.js';
8
import { DEFAULT_FONT_FAMILY } from '../../../../base/browser/fonts.js';
9
import { IHistoryNavigationWidget } from '../../../../base/browser/history.js';
10
import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js';
11
import { ActionViewItem, IActionViewItemOptions } from '../../../../base/browser/ui/actionbar/actionViewItems.js';
12
import * as aria from '../../../../base/browser/ui/aria/aria.js';
13
import { Button, ButtonWithIcon } from '../../../../base/browser/ui/button/button.js';
14
import { createInstantHoverDelegate, getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js';
15
import { renderLabelWithIcons } from '../../../../base/browser/ui/iconLabel/iconLabels.js';
16
import { IAction } from '../../../../base/common/actions.js';
17
import { DeferredPromise } from '../../../../base/common/async.js';
18
import { CancellationToken } from '../../../../base/common/cancellation.js';
19
import { Codicon } from '../../../../base/common/codicons.js';
20
import { Emitter, Event } from '../../../../base/common/event.js';
21
import { HistoryNavigator2 } from '../../../../base/common/history.js';
22
import { KeyCode } from '../../../../base/common/keyCodes.js';
23
import { Disposable, DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
24
import { ResourceSet } from '../../../../base/common/map.js';
25
import { Schemas } from '../../../../base/common/network.js';
26
import { autorun, IObservable, observableValue } from '../../../../base/common/observable.js';
27
import { isMacintosh } from '../../../../base/common/platform.js';
28
import { isEqual } from '../../../../base/common/resources.js';
29
import { ScrollbarVisibility } from '../../../../base/common/scrollable.js';
30
import { assertType } from '../../../../base/common/types.js';
31
import { URI } from '../../../../base/common/uri.js';
32
import { IEditorConstructionOptions } from '../../../../editor/browser/config/editorConfiguration.js';
33
import { EditorExtensionsRegistry } from '../../../../editor/browser/editorExtensions.js';
34
import { CodeEditorWidget } from '../../../../editor/browser/widget/codeEditor/codeEditorWidget.js';
35
import { EditorOptions, IEditorOptions } from '../../../../editor/common/config/editorOptions.js';
36
import { IDimension } from '../../../../editor/common/core/2d/dimension.js';
37
import { IPosition } from '../../../../editor/common/core/position.js';
38
import { Range } from '../../../../editor/common/core/range.js';
39
import { isLocation } from '../../../../editor/common/languages.js';
40
import { ITextModel } from '../../../../editor/common/model.js';
41
import { IModelService } from '../../../../editor/common/services/model.js';
42
import { ITextModelService } from '../../../../editor/common/services/resolverService.js';
43
import { CopyPasteController } from '../../../../editor/contrib/dropOrPasteInto/browser/copyPasteController.js';
44
import { DropIntoEditorController } from '../../../../editor/contrib/dropOrPasteInto/browser/dropIntoEditorController.js';
45
import { ContentHoverController } from '../../../../editor/contrib/hover/browser/contentHoverController.js';
46
import { GlyphHoverController } from '../../../../editor/contrib/hover/browser/glyphHoverController.js';
47
import { LinkDetector } from '../../../../editor/contrib/links/browser/links.js';
48
import { SuggestController } from '../../../../editor/contrib/suggest/browser/suggestController.js';
49
import { localize } from '../../../../nls.js';
50
import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js';
51
import { MenuWorkbenchButtonBar } from '../../../../platform/actions/browser/buttonbar.js';
52
import { DropdownWithPrimaryActionViewItem, IDropdownWithPrimaryActionViewItemOptions } from '../../../../platform/actions/browser/dropdownWithPrimaryActionViewItem.js';
53
import { getFlatActionBarActions } from '../../../../platform/actions/browser/menuEntryActionViewItem.js';
54
import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js';
55
import { IMenuService, MenuId, MenuItemAction } from '../../../../platform/actions/common/actions.js';
56
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
57
import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
58
import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';
59
import { IFileService } from '../../../../platform/files/common/files.js';
60
import { registerAndCreateHistoryNavigationContext } from '../../../../platform/history/browser/contextScopedHistoryWidget.js';
61
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
62
import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js';
63
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
64
import { ILabelService } from '../../../../platform/label/common/label.js';
65
import { WorkbenchList } from '../../../../platform/list/browser/listService.js';
66
import { ILogService } from '../../../../platform/log/common/log.js';
67
import { INotificationService } from '../../../../platform/notification/common/notification.js';
68
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
69
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
70
import { ISharedWebContentExtractorService } from '../../../../platform/webContentExtractor/common/webContentExtractor.js';
71
import { ResourceLabels } from '../../../browser/labels.js';
72
import { IWorkbenchAssignmentService } from '../../../services/assignment/common/assignmentService.js';
73
import { ACTIVE_GROUP, IEditorService, SIDE_GROUP } from '../../../services/editor/common/editorService.js';
74
import { AccessibilityVerbositySettingId } from '../../accessibility/browser/accessibilityConfiguration.js';
75
import { AccessibilityCommandId } from '../../accessibility/common/accessibilityCommands.js';
76
import { getSimpleCodeEditorWidgetOptions, getSimpleEditorOptions, setupSimpleEditorSelectionStyling } from '../../codeEditor/browser/simpleEditorOptions.js';
77
import { IChatAgentService } from '../common/chatAgents.js';
78
import { ChatContextKeys } from '../common/chatContextKeys.js';
79
import { IChatEditingSession, ModifiedFileEntryState } from '../common/chatEditingService.js';
80
import { ChatEntitlement, IChatEntitlementService } from '../common/chatEntitlementService.js';
81
import { IChatRequestModeInfo } from '../common/chatModel.js';
82
import { ChatMode, IChatMode, IChatModeService } from '../common/chatModes.js';
83
import { IChatFollowup } from '../common/chatService.js';
84
import { ChatRequestVariableSet, IChatRequestVariableEntry, isElementVariableEntry, isImageVariableEntry, isNotebookOutputVariableEntry, isPasteVariableEntry, isPromptFileVariableEntry, isPromptTextVariableEntry, isSCMHistoryItemVariableEntry } from '../common/chatVariableEntries.js';
85
import { IChatResponseViewModel } from '../common/chatViewModel.js';
86
import { ChatInputHistoryMaxEntries, IChatHistoryEntry, IChatInputState, IChatWidgetHistoryService } from '../common/chatWidgetHistoryService.js';
87
import { ChatAgentLocation, ChatConfiguration, ChatModeKind, validateChatMode } from '../common/constants.js';
88
import { ILanguageModelChatMetadata, ILanguageModelChatMetadataAndIdentifier, ILanguageModelsService } from '../common/languageModels.js';
89
import { ILanguageModelToolsService } from '../common/languageModelToolsService.js';
90
import { PromptsType } from '../common/promptSyntax/promptTypes.js';
91
import { IPromptsService } from '../common/promptSyntax/service/promptsService.js';
92
import { CancelAction, ChatEditingSessionSubmitAction, ChatOpenModelPickerActionId, ChatSubmitAction, IChatExecuteActionContext, OpenModePickerAction } from './actions/chatExecuteActions.js';
93
import { ImplicitContextAttachmentWidget } from './attachments/implicitContextAttachment.js';
94
import { IChatWidget } from './chat.js';
95
import { ChatAttachmentModel } from './chatAttachmentModel.js';
96
import { DefaultChatAttachmentWidget, ElementChatAttachmentWidget, FileAttachmentWidget, ImageAttachmentWidget, NotebookCellOutputChatAttachmentWidget, PasteAttachmentWidget, PromptFileAttachmentWidget, PromptTextAttachmentWidget, SCMHistoryItemAttachmentWidget, ToolSetOrToolItemAttachmentWidget } from './chatAttachmentWidgets.js';
97
import { IDisposableReference } from './chatContentParts/chatCollections.js';
98
import { CollapsibleListPool, IChatCollapsibleListItem } from './chatContentParts/chatReferencesContentPart.js';
99
import { ChatDragAndDrop } from './chatDragAndDrop.js';
100
import { ChatEditingShowChangesAction, ViewPreviousEditsAction } from './chatEditing/chatEditingActions.js';
101
import { ChatFollowups } from './chatFollowups.js';
102
import { ChatSelectedTools } from './chatSelectedTools.js';
103
import { IChatViewState } from './chatWidget.js';
104
import { ChatImplicitContext } from './contrib/chatImplicitContext.js';
105
import { ChatRelatedFiles } from './contrib/chatInputRelatedFilesContrib.js';
106
import { resizeImage } from './imageUtils.js';
107
import { IModelPickerDelegate, ModelPickerActionItem } from './modelPicker/modelPickerActionItem.js';
108
import { IModePickerDelegate, ModePickerActionItem } from './modelPicker/modePickerActionItem.js';
109
110
const $ = dom.$;
111
112
const INPUT_EDITOR_MAX_HEIGHT = 250;
113
114
export interface IChatInputStyles {
115
overlayBackground: string;
116
listForeground: string;
117
listBackground: string;
118
}
119
120
interface IChatInputPartOptions {
121
renderFollowups: boolean;
122
renderStyle?: 'compact';
123
menus: {
124
executeToolbar: MenuId;
125
telemetrySource: string;
126
inputSideToolbar?: MenuId;
127
};
128
editorOverflowWidgetsDomNode?: HTMLElement;
129
renderWorkingSet?: boolean;
130
enableImplicitContext?: boolean;
131
supportsChangingModes?: boolean;
132
dndContainer?: HTMLElement;
133
widgetViewKindTag: string;
134
}
135
136
export interface IWorkingSetEntry {
137
uri: URI;
138
}
139
140
const GlobalLastChatModeKey = 'chat.lastChatMode';
141
142
export class ChatInputPart extends Disposable implements IHistoryNavigationWidget {
143
private static _counter = 0;
144
145
private _workingSetCollapsed = true;
146
private _lastEditingSessionId: string | undefined;
147
148
private _onDidLoadInputState: Emitter<IChatInputState | undefined>;
149
readonly onDidLoadInputState: Event<IChatInputState | undefined>;
150
151
private _onDidChangeHeight: Emitter<void>;
152
readonly onDidChangeHeight: Event<void>;
153
154
private _onDidFocus: Emitter<void>;
155
readonly onDidFocus: Event<void>;
156
157
private _onDidBlur: Emitter<void>;
158
readonly onDidBlur: Event<void>;
159
160
private _onDidChangeContext: Emitter<{ removed?: IChatRequestVariableEntry[]; added?: IChatRequestVariableEntry[] }>;
161
readonly onDidChangeContext: Event<{ removed?: IChatRequestVariableEntry[]; added?: IChatRequestVariableEntry[] }>;
162
163
private _onDidAcceptFollowup: Emitter<{ followup: IChatFollowup; response: IChatResponseViewModel | undefined }>;
164
readonly onDidAcceptFollowup: Event<{ followup: IChatFollowup; response: IChatResponseViewModel | undefined }>;
165
166
private _onDidClickOverlay: Emitter<void>;
167
readonly onDidClickOverlay: Event<void>;
168
169
private readonly _attachmentModel: ChatAttachmentModel;
170
public get attachmentModel(): ChatAttachmentModel {
171
return this._attachmentModel;
172
}
173
174
readonly selectedToolsModel: ChatSelectedTools;
175
176
public getAttachedAndImplicitContext(sessionId: string): ChatRequestVariableSet {
177
178
const contextArr = new ChatRequestVariableSet();
179
180
contextArr.add(...this.attachmentModel.attachments);
181
182
if ((this.implicitContext?.enabled && this.implicitContext?.value) || (isLocation(this.implicitContext?.value) && this.configurationService.getValue<boolean>('chat.implicitContext.suggestedContext'))) {
183
const implicitChatVariables = this.implicitContext.toBaseEntries();
184
contextArr.add(...implicitChatVariables);
185
}
186
return contextArr;
187
}
188
189
/**
190
* Check if the chat input part has any prompt file attachments.
191
*/
192
get hasPromptFileAttachments(): boolean {
193
return this._attachmentModel.attachments.some(entry => {
194
return isPromptFileVariableEntry(entry) && entry.isRoot && this.promptsService.getPromptFileType(entry.value) === PromptsType.prompt;
195
});
196
}
197
198
private _indexOfLastAttachedContextDeletedWithKeyboard: number;
199
private _indexOfLastOpenedContext: number;
200
201
private _implicitContext: ChatImplicitContext | undefined;
202
public get implicitContext(): ChatImplicitContext | undefined {
203
return this._implicitContext;
204
}
205
206
private _relatedFiles: ChatRelatedFiles | undefined;
207
public get relatedFiles(): ChatRelatedFiles | undefined {
208
return this._relatedFiles;
209
}
210
211
private _hasFileAttachmentContextKey: IContextKey<boolean>;
212
213
private readonly _onDidChangeVisibility: Emitter<boolean>;
214
private readonly _contextResourceLabels: ResourceLabels;
215
216
private readonly inputEditorMaxHeight: number;
217
private inputEditorHeight: number;
218
private container!: HTMLElement;
219
220
private inputSideToolbarContainer?: HTMLElement;
221
222
private followupsContainer!: HTMLElement;
223
private readonly followupsDisposables: DisposableStore;
224
225
private attachmentsContainer!: HTMLElement;
226
227
private chatInputOverlay!: HTMLElement;
228
private readonly overlayClickListener: MutableDisposable<IDisposable>;
229
230
private attachedContextContainer!: HTMLElement;
231
private readonly attachedContextDisposables: MutableDisposable<DisposableStore>;
232
233
private relatedFilesContainer!: HTMLElement;
234
235
private chatEditingSessionWidgetContainer!: HTMLElement;
236
237
private _inputPartHeight: number;
238
get inputPartHeight() {
239
return this._inputPartHeight;
240
}
241
242
private _followupsHeight: number;
243
get followupsHeight() {
244
return this._followupsHeight;
245
}
246
247
private _editSessionWidgetHeight: number;
248
get editSessionWidgetHeight() {
249
return this._editSessionWidgetHeight;
250
}
251
252
get attachmentsHeight() {
253
return this.attachmentsContainer.offsetHeight + (this.attachmentsContainer.checkVisibility() ? 6 : 0);
254
}
255
256
private _inputEditor!: CodeEditorWidget;
257
private _inputEditorElement!: HTMLElement;
258
259
private executeToolbar!: MenuWorkbenchToolBar;
260
private inputActionsToolbar!: MenuWorkbenchToolBar;
261
262
private addFilesToolbar: MenuWorkbenchToolBar | undefined;
263
264
get inputEditor() {
265
return this._inputEditor;
266
}
267
268
readonly dnd: ChatDragAndDrop;
269
270
private history: HistoryNavigator2<IChatHistoryEntry>;
271
private historyNavigationBackwardsEnablement!: IContextKey<boolean>;
272
private historyNavigationForewardsEnablement!: IContextKey<boolean>;
273
private inputModel: ITextModel | undefined;
274
private inputEditorHasText: IContextKey<boolean>;
275
private chatCursorAtTop: IContextKey<boolean>;
276
private inputEditorHasFocus: IContextKey<boolean>;
277
private currentlyEditingInputKey!: IContextKey<boolean>;
278
/**
279
* Context key is set when prompt instructions are attached.
280
*/
281
private promptFileAttached: IContextKey<boolean>;
282
private chatModeKindKey: IContextKey<ChatModeKind>;
283
284
private modelWidget: ModelPickerActionItem | undefined;
285
private modeWidget: ModePickerActionItem | undefined;
286
private readonly _waitForPersistedLanguageModel: MutableDisposable<IDisposable>;
287
private _onDidChangeCurrentLanguageModel: Emitter<ILanguageModelChatMetadataAndIdentifier>;
288
289
private _currentLanguageModel: ILanguageModelChatMetadataAndIdentifier | undefined;
290
291
get currentLanguageModel() {
292
return this._currentLanguageModel?.identifier;
293
}
294
295
get selectedLanguageModel(): ILanguageModelChatMetadataAndIdentifier | undefined {
296
return this._currentLanguageModel;
297
}
298
299
private _onDidChangeCurrentChatMode: Emitter<void>;
300
readonly onDidChangeCurrentChatMode: Event<void>;
301
302
private readonly _currentModeObservable = observableValue<IChatMode>('currentMode', ChatMode.Ask);
303
public get currentModeKind(): ChatModeKind {
304
const mode = this._currentModeObservable.get();
305
return mode.kind === ChatModeKind.Agent && !this.agentService.hasToolsAgent ?
306
ChatModeKind.Edit :
307
mode.kind;
308
}
309
310
public get currentModeObs(): IObservable<IChatMode> {
311
return this._currentModeObservable;
312
}
313
314
public get currentModeInfo(): IChatRequestModeInfo {
315
const mode = this._currentModeObservable.get();
316
const modeId: 'ask' | 'agent' | 'edit' | 'custom' | undefined = mode.isBuiltin ? this.currentModeKind : 'custom';
317
318
return {
319
kind: this.currentModeKind,
320
isBuiltin: mode.isBuiltin,
321
instructions: {
322
content: mode.body?.get(),
323
toolReferences: mode.variableReferences ? this.toolService.toToolReferences(mode.variableReferences.get()) : undefined
324
},
325
modeId: modeId,
326
applyCodeBlockSuggestionId: undefined,
327
};
328
}
329
330
private cachedDimensions: dom.Dimension | undefined;
331
private cachedExecuteToolbarWidth: number | undefined;
332
private cachedInputToolbarWidth: number | undefined;
333
334
readonly inputUri: URI;
335
336
private _workingSetLinesAddedSpan?: HTMLElement;
337
private _workingSetLinesRemovedSpan?: HTMLElement;
338
339
private readonly _chatEditsActionsDisposables: DisposableStore;
340
private readonly _chatEditsDisposables: DisposableStore;
341
private _chatEditsListPool: CollapsibleListPool;
342
private _chatEditList: IDisposableReference<WorkbenchList<IChatCollapsibleListItem>> | undefined;
343
get selectedElements(): URI[] {
344
const edits = [];
345
const editsList = this._chatEditList?.object;
346
const selectedElements = editsList?.getSelectedElements() ?? [];
347
for (const element of selectedElements) {
348
if (element.kind === 'reference' && URI.isUri(element.reference)) {
349
edits.push(element.reference);
350
}
351
}
352
return edits;
353
}
354
355
private _attemptedWorkingSetEntriesCount: number;
356
/**
357
* The number of working set entries that the user actually wanted to attach.
358
* This is less than or equal to {@link ChatInputPart.chatEditWorkingSetFiles}.
359
*/
360
public get attemptedWorkingSetEntriesCount() {
361
return this._attemptedWorkingSetEntriesCount;
362
}
363
364
private readonly getInputState: () => IChatInputState;
365
366
/**
367
* Number consumers holding the 'generating' lock.
368
*/
369
private _generating?: { rc: number; defer: DeferredPromise<void> };
370
371
constructor(
372
// private readonly editorOptions: ChatEditorOptions, // TODO this should be used
373
private readonly location: ChatAgentLocation,
374
private readonly options: IChatInputPartOptions,
375
styles: IChatInputStyles,
376
getContribsInputState: () => any,
377
private readonly inline: boolean,
378
@IChatWidgetHistoryService private readonly historyService: IChatWidgetHistoryService,
379
@IModelService private readonly modelService: IModelService,
380
@IInstantiationService private readonly instantiationService: IInstantiationService,
381
@IContextKeyService private readonly contextKeyService: IContextKeyService,
382
@IConfigurationService private readonly configurationService: IConfigurationService,
383
@IKeybindingService private readonly keybindingService: IKeybindingService,
384
@IAccessibilityService private readonly accessibilityService: IAccessibilityService,
385
@ILanguageModelsService private readonly languageModelsService: ILanguageModelsService,
386
@ILogService private readonly logService: ILogService,
387
@IFileService private readonly fileService: IFileService,
388
@IEditorService private readonly editorService: IEditorService,
389
@IThemeService private readonly themeService: IThemeService,
390
@ITextModelService private readonly textModelResolverService: ITextModelService,
391
@IStorageService private readonly storageService: IStorageService,
392
@ILabelService private readonly labelService: ILabelService,
393
@IChatAgentService private readonly agentService: IChatAgentService,
394
@ISharedWebContentExtractorService private readonly sharedWebExtracterService: ISharedWebContentExtractorService,
395
@IWorkbenchAssignmentService private readonly experimentService: IWorkbenchAssignmentService,
396
@IChatEntitlementService private readonly entitlementService: IChatEntitlementService,
397
@IChatModeService private readonly chatModeService: IChatModeService,
398
@IPromptsService private readonly promptsService: IPromptsService,
399
@ILanguageModelToolsService private readonly toolService: ILanguageModelToolsService,
400
) {
401
super();
402
this._onDidLoadInputState = this._register(new Emitter<any>());
403
this.onDidLoadInputState = this._onDidLoadInputState.event;
404
this._onDidChangeHeight = this._register(new Emitter<void>());
405
this.onDidChangeHeight = this._onDidChangeHeight.event;
406
this._onDidFocus = this._register(new Emitter<void>());
407
this.onDidFocus = this._onDidFocus.event;
408
this._onDidBlur = this._register(new Emitter<void>());
409
this.onDidBlur = this._onDidBlur.event;
410
this._onDidClickOverlay = this._register(new Emitter<void>());
411
this.onDidClickOverlay = this._onDidClickOverlay.event;
412
this._onDidChangeContext = this._register(new Emitter<{ removed?: IChatRequestVariableEntry[]; added?: IChatRequestVariableEntry[] }>());
413
this.onDidChangeContext = this._onDidChangeContext.event;
414
this._onDidAcceptFollowup = this._register(new Emitter<{ followup: IChatFollowup; response: IChatResponseViewModel | undefined }>());
415
this.onDidAcceptFollowup = this._onDidAcceptFollowup.event;
416
this._indexOfLastAttachedContextDeletedWithKeyboard = -1;
417
this._indexOfLastOpenedContext = -1;
418
this._onDidChangeVisibility = this._register(new Emitter<boolean>());
419
this._contextResourceLabels = this._register(this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this._onDidChangeVisibility.event }));
420
this.inputEditorHeight = 0;
421
this.followupsDisposables = this._register(new DisposableStore());
422
this.attachedContextDisposables = this._register(new MutableDisposable<DisposableStore>());
423
this.overlayClickListener = this._register(new MutableDisposable<IDisposable>());
424
this._inputPartHeight = 0;
425
this._followupsHeight = 0;
426
this._editSessionWidgetHeight = 0;
427
this._waitForPersistedLanguageModel = this._register(new MutableDisposable<IDisposable>());
428
this._onDidChangeCurrentLanguageModel = this._register(new Emitter<ILanguageModelChatMetadataAndIdentifier>());
429
this._onDidChangeCurrentChatMode = this._register(new Emitter<void>());
430
this.onDidChangeCurrentChatMode = this._onDidChangeCurrentChatMode.event;
431
this._currentModeObservable.set(ChatMode.Ask, undefined);
432
this.inputUri = URI.parse(`${Schemas.vscodeChatInput}:input-${ChatInputPart._counter++}`);
433
this._chatEditsActionsDisposables = this._register(new DisposableStore());
434
this._chatEditsDisposables = this._register(new DisposableStore());
435
this._attemptedWorkingSetEntriesCount = 0;
436
437
this._register(this.editorService.onDidActiveEditorChange(() => {
438
this._indexOfLastOpenedContext = -1;
439
}));
440
441
this._attachmentModel = this._register(this.instantiationService.createInstance(ChatAttachmentModel));
442
this.selectedToolsModel = this._register(this.instantiationService.createInstance(ChatSelectedTools, this.currentModeObs));
443
this.dnd = this._register(this.instantiationService.createInstance(ChatDragAndDrop, this._attachmentModel, styles));
444
445
this.getInputState = (): IChatInputState => {
446
return {
447
...getContribsInputState(),
448
chatContextAttachments: this._attachmentModel.attachments,
449
chatMode: this._currentModeObservable.get().id,
450
};
451
};
452
this.inputEditorMaxHeight = this.options.renderStyle === 'compact' ? INPUT_EDITOR_MAX_HEIGHT / 3 : INPUT_EDITOR_MAX_HEIGHT;
453
454
this.inputEditorHasText = ChatContextKeys.inputHasText.bindTo(contextKeyService);
455
this.chatCursorAtTop = ChatContextKeys.inputCursorAtTop.bindTo(contextKeyService);
456
this.inputEditorHasFocus = ChatContextKeys.inputHasFocus.bindTo(contextKeyService);
457
this.promptFileAttached = ChatContextKeys.hasPromptFile.bindTo(contextKeyService);
458
this.chatModeKindKey = ChatContextKeys.chatModeKind.bindTo(contextKeyService);
459
const chatToolCount = ChatContextKeys.chatToolCount.bindTo(contextKeyService);
460
461
this._register(autorun(reader => {
462
let count = 0;
463
const userSelectedTools = this.selectedToolsModel.userSelectedTools.read(reader);
464
for (const key in userSelectedTools) {
465
if (userSelectedTools[key] === true) {
466
count++;
467
}
468
}
469
470
chatToolCount.set(count);
471
}));
472
473
this.history = this.loadHistory();
474
this._register(this.historyService.onDidClearHistory(() => this.history = new HistoryNavigator2<IChatHistoryEntry>([{ text: '', state: this.getInputState() }], ChatInputHistoryMaxEntries, historyKeyFn)));
475
476
this._register(this.configurationService.onDidChangeConfiguration(e => {
477
const newOptions: IEditorOptions = {};
478
if (e.affectsConfiguration(AccessibilityVerbositySettingId.Chat)) {
479
newOptions.ariaLabel = this._getAriaLabel();
480
}
481
if (e.affectsConfiguration('editor.wordSegmenterLocales')) {
482
newOptions.wordSegmenterLocales = this.configurationService.getValue<string | string[]>('editor.wordSegmenterLocales');
483
}
484
485
this.inputEditor.updateOptions(newOptions);
486
}));
487
488
this._chatEditsListPool = this._register(this.instantiationService.createInstance(CollapsibleListPool, this._onDidChangeVisibility.event, MenuId.ChatEditingWidgetModifiedFilesToolbar, { verticalScrollMode: ScrollbarVisibility.Visible }));
489
490
this._hasFileAttachmentContextKey = ChatContextKeys.hasFileAttachments.bindTo(contextKeyService);
491
492
this.initSelectedModel();
493
494
this._register(this.onDidChangeCurrentChatMode(() => {
495
this.accessibilityService.alert(this._currentModeObservable.get().label);
496
if (this._inputEditor) {
497
this._inputEditor.updateOptions({ ariaLabel: this._getAriaLabel() });
498
}
499
500
if (this.implicitContext && this.configurationService.getValue<boolean>('chat.implicitContext.suggestedContext')) {
501
this.implicitContext.enabled = this._currentModeObservable.get() !== ChatMode.Agent;
502
}
503
}));
504
this._register(this._onDidChangeCurrentLanguageModel.event(() => {
505
if (this._currentLanguageModel?.metadata.name) {
506
this.accessibilityService.alert(this._currentLanguageModel.metadata.name);
507
}
508
}));
509
this._register(this.chatModeService.onDidChangeChatModes(() => this.validateCurrentChatMode()));
510
this._register(autorun(r => {
511
const mode = this._currentModeObservable.read(r);
512
const model = mode.model?.read(r);
513
if (model) {
514
this.switchModelByQualifiedName(model);
515
}
516
}));
517
}
518
519
private getSelectedModelStorageKey(): string {
520
return `chat.currentLanguageModel.${this.location}`;
521
}
522
523
private getSelectedModelIsDefaultStorageKey(): string {
524
return `chat.currentLanguageModel.${this.location}.isDefault`;
525
}
526
527
private initSelectedModel() {
528
let persistedSelection = this.storageService.get(this.getSelectedModelStorageKey(), StorageScope.APPLICATION);
529
if (persistedSelection && persistedSelection.startsWith('github.copilot-chat/')) {
530
// Convert the persisted selection to make it backwards comptabile with the old LM API. TODO @lramos15 - Remove this after a bit
531
persistedSelection = persistedSelection.replace('github.copilot-chat/', 'copilot/');
532
this.storageService.store(this.getSelectedModelStorageKey(), persistedSelection, StorageScope.APPLICATION, StorageTarget.USER);
533
}
534
const persistedAsDefault = this.storageService.getBoolean(this.getSelectedModelIsDefaultStorageKey(), StorageScope.APPLICATION, persistedSelection === 'copilot/gpt-4.1');
535
536
if (persistedSelection) {
537
const model = this.languageModelsService.lookupLanguageModel(persistedSelection);
538
if (model) {
539
// Only restore the model if it wasn't the default at the time of storing or it is now the default
540
if (!persistedAsDefault || model.isDefault) {
541
this.setCurrentLanguageModel({ metadata: model, identifier: persistedSelection });
542
this.checkModelSupported();
543
}
544
} else {
545
this._waitForPersistedLanguageModel.value = this.languageModelsService.onDidChangeLanguageModels(e => {
546
const persistedModel = this.languageModelsService.lookupLanguageModel(persistedSelection);
547
if (persistedModel) {
548
this._waitForPersistedLanguageModel.clear();
549
550
// Only restore the model if it wasn't the default at the time of storing or it is now the default
551
if (!persistedAsDefault || persistedModel.isDefault) {
552
if (persistedModel.isUserSelectable) {
553
this.setCurrentLanguageModel({ metadata: persistedModel, identifier: persistedSelection });
554
this.checkModelSupported();
555
}
556
}
557
}
558
});
559
}
560
}
561
562
this._register(this._onDidChangeCurrentChatMode.event(() => {
563
this.checkModelSupported();
564
}));
565
this._register(this.configurationService.onDidChangeConfiguration(e => {
566
if (e.affectsConfiguration(ChatConfiguration.Edits2Enabled)) {
567
this.checkModelSupported();
568
}
569
}));
570
}
571
572
public setEditing(enabled: boolean) {
573
this.currentlyEditingInputKey?.set(enabled);
574
}
575
576
public switchModel(modelMetadata: Pick<ILanguageModelChatMetadata, 'vendor' | 'id' | 'family'>) {
577
const models = this.getModels();
578
const model = models.find(m => m.metadata.vendor === modelMetadata.vendor && m.metadata.id === modelMetadata.id && m.metadata.family === modelMetadata.family);
579
if (model) {
580
this.setCurrentLanguageModel(model);
581
}
582
}
583
584
public switchModelByQualifiedName(qualifiedModelName: string): boolean {
585
const models = this.getModels();
586
const model = models.find(m => ILanguageModelChatMetadata.matchesQualifiedName(qualifiedModelName, m.metadata));
587
if (model) {
588
this.setCurrentLanguageModel(model);
589
return true;
590
}
591
return false;
592
}
593
594
public switchToNextModel(): void {
595
const models = this.getModels();
596
if (models.length > 0) {
597
const currentIndex = models.findIndex(model => model.identifier === this._currentLanguageModel?.identifier);
598
const nextIndex = (currentIndex + 1) % models.length;
599
this.setCurrentLanguageModel(models[nextIndex]);
600
}
601
}
602
603
public openModelPicker(): void {
604
this.modelWidget?.show();
605
}
606
607
public openModePicker(): void {
608
this.modeWidget?.show();
609
}
610
611
public setCurrentLanguageModel(model: ILanguageModelChatMetadataAndIdentifier) {
612
this._currentLanguageModel = model;
613
614
if (this.cachedDimensions) {
615
// For quick chat and editor chat, relayout because the input may need to shrink to accomodate the model name
616
this.layout(this.cachedDimensions.height, this.cachedDimensions.width);
617
}
618
619
this.storageService.store(this.getSelectedModelStorageKey(), model.identifier, StorageScope.APPLICATION, StorageTarget.USER);
620
this.storageService.store(this.getSelectedModelIsDefaultStorageKey(), !!model.metadata.isDefault, StorageScope.APPLICATION, StorageTarget.USER);
621
622
this._onDidChangeCurrentLanguageModel.fire(model);
623
}
624
625
private checkModelSupported(): void {
626
if (this._currentLanguageModel && !this.modelSupportedForDefaultAgent(this._currentLanguageModel)) {
627
this.setCurrentLanguageModelToDefault();
628
}
629
}
630
631
/**
632
* By ID- prefer this method
633
*/
634
setChatMode(mode: ChatModeKind | string, storeSelection = true): void {
635
if (!this.options.supportsChangingModes) {
636
return;
637
}
638
639
const mode2 = this.chatModeService.findModeById(mode) ??
640
this.chatModeService.findModeById(ChatModeKind.Agent) ??
641
ChatMode.Ask;
642
this.setChatMode2(mode2, storeSelection);
643
}
644
645
private setChatMode2(mode: IChatMode, storeSelection = true): void {
646
if (!this.options.supportsChangingModes) {
647
return;
648
}
649
650
this._currentModeObservable.set(mode, undefined);
651
this.chatModeKindKey.set(mode.kind);
652
this._onDidChangeCurrentChatMode.fire();
653
654
if (storeSelection) {
655
this.storageService.store(GlobalLastChatModeKey, mode.kind, StorageScope.APPLICATION, StorageTarget.USER);
656
}
657
}
658
659
private modelSupportedForDefaultAgent(model: ILanguageModelChatMetadataAndIdentifier): boolean {
660
// Probably this logic could live in configuration on the agent, or somewhere else, if it gets more complex
661
if (this.currentModeKind === ChatModeKind.Agent || (this.currentModeKind === ChatModeKind.Edit && this.configurationService.getValue(ChatConfiguration.Edits2Enabled))) {
662
return ILanguageModelChatMetadata.suitableForAgentMode(model.metadata);
663
}
664
665
return true;
666
}
667
668
private getModels(): ILanguageModelChatMetadataAndIdentifier[] {
669
const models = this.languageModelsService.getLanguageModelIds()
670
.map(modelId => ({ identifier: modelId, metadata: this.languageModelsService.lookupLanguageModel(modelId)! }))
671
.filter(entry => entry.metadata?.isUserSelectable && this.modelSupportedForDefaultAgent(entry));
672
models.sort((a, b) => a.metadata.name.localeCompare(b.metadata.name));
673
return models;
674
}
675
676
private setCurrentLanguageModelToDefault() {
677
const defaultLanguageModelId = this.languageModelsService.getLanguageModelIds().find(id => this.languageModelsService.lookupLanguageModel(id)?.isDefault);
678
const hasUserSelectableLanguageModels = this.languageModelsService.getLanguageModelIds().find(id => {
679
const model = this.languageModelsService.lookupLanguageModel(id);
680
return model?.isUserSelectable && !model.isDefault;
681
});
682
const defaultModel = hasUserSelectableLanguageModels && defaultLanguageModelId ?
683
{ metadata: this.languageModelsService.lookupLanguageModel(defaultLanguageModelId)!, identifier: defaultLanguageModelId } :
684
undefined;
685
if (defaultModel) {
686
this.setCurrentLanguageModel(defaultModel);
687
}
688
}
689
690
private loadHistory(): HistoryNavigator2<IChatHistoryEntry> {
691
const history = this.historyService.getHistory(this.location);
692
if (history.length === 0) {
693
history.push({ text: '', state: this.getInputState() });
694
}
695
696
return new HistoryNavigator2(history, 50, historyKeyFn);
697
}
698
699
private _getAriaLabel(): string {
700
const verbose = this.configurationService.getValue<boolean>(AccessibilityVerbositySettingId.Chat);
701
let kbLabel;
702
if (verbose) {
703
kbLabel = this.keybindingService.lookupKeybinding(AccessibilityCommandId.OpenAccessibilityHelp)?.getLabel();
704
}
705
let modeLabel = '';
706
switch (this.currentModeKind) {
707
case ChatModeKind.Agent:
708
modeLabel = localize('chatInput.mode.agent', "(Agent Mode), edit files in your workspace.");
709
break;
710
case ChatModeKind.Edit:
711
modeLabel = localize('chatInput.mode.edit', "(Edit Mode), edit files in your workspace.");
712
break;
713
case ChatModeKind.Ask:
714
default:
715
modeLabel = localize('chatInput.mode.ask', "(Ask Mode), ask questions or type / for topics.");
716
break;
717
}
718
if (verbose) {
719
return kbLabel
720
? localize('actions.chat.accessibiltyHelp', "Chat Input {0} Press Enter to send out the request. Use {1} for Chat Accessibility Help.", modeLabel, kbLabel)
721
: localize('chatInput.accessibilityHelpNoKb', "Chat Input {0} Press Enter to send out the request. Use the Chat Accessibility Help command for more information.", modeLabel);
722
} else {
723
return localize('chatInput.accessibilityHelp', "Chat Input {0}.", modeLabel);
724
}
725
}
726
727
private validateCurrentChatMode() {
728
const currentMode = this._currentModeObservable.get();
729
const validMode = this.chatModeService.findModeById(currentMode.id);
730
if (!validMode) {
731
this.setChatMode(ChatModeKind.Agent);
732
return;
733
}
734
}
735
736
initForNewChatModel(state: IChatViewState, modelIsEmpty: boolean): void {
737
this.history = this.loadHistory();
738
this.history.add({
739
text: state.inputValue ?? this.history.current().text,
740
state: state.inputState ?? this.getInputState()
741
});
742
const attachments = state.inputState?.chatContextAttachments ?? [];
743
this._attachmentModel.clearAndSetContext(...attachments);
744
745
this.selectedToolsModel.resetSessionEnablementState();
746
747
if (state.inputValue) {
748
this.setValue(state.inputValue, false);
749
}
750
751
if (state.inputState?.chatMode) {
752
if (typeof state.inputState.chatMode === 'string') {
753
this.setChatMode(state.inputState.chatMode);
754
} else {
755
// This path is deprecated, but handle old state
756
this.setChatMode(state.inputState.chatMode.id);
757
}
758
} else {
759
const persistedMode = this.storageService.get(GlobalLastChatModeKey, StorageScope.APPLICATION);
760
if (persistedMode) {
761
this.setChatMode(persistedMode);
762
}
763
}
764
765
// TODO@roblourens This is for an experiment which will be obsolete in a month or two and can then be removed.
766
if (modelIsEmpty) {
767
const storageKey = this.getDefaultModeExperimentStorageKey();
768
const hasSetDefaultMode = this.storageService.getBoolean(storageKey, StorageScope.WORKSPACE, false);
769
if (!hasSetDefaultMode) {
770
const defaultModeKey = this.entitlementService.entitlement === ChatEntitlement.Free ? 'chat.defaultModeFree' : 'chat.defaultMode';
771
const defaultLanguageModelKey = this.entitlementService.entitlement === ChatEntitlement.Free ? 'chat.defaultLanguageModelFree' : 'chat.defaultLanguageModel';
772
Promise.all([
773
this.experimentService.getTreatment(defaultModeKey),
774
this.experimentService.getTreatment(defaultLanguageModelKey),
775
]).then(([defaultModeTreatment, defaultLanguageModelTreatment]) => {
776
if (typeof defaultModeTreatment === 'string') {
777
this.storageService.store(storageKey, true, StorageScope.WORKSPACE, StorageTarget.MACHINE);
778
const defaultMode = validateChatMode(defaultModeTreatment);
779
if (defaultMode) {
780
this.logService.trace(`Applying default mode from experiment: ${defaultMode}`);
781
this.setChatMode(defaultMode, false);
782
this.checkModelSupported();
783
}
784
}
785
786
if (typeof defaultLanguageModelTreatment === 'string' && this._currentModeObservable.get().kind === ChatModeKind.Agent) {
787
this.storageService.store(storageKey, true, StorageScope.WORKSPACE, StorageTarget.MACHINE);
788
this.logService.trace(`Applying default language model from experiment: ${defaultLanguageModelTreatment}`);
789
this.setExpModelOrWait(defaultLanguageModelTreatment);
790
}
791
});
792
}
793
}
794
}
795
796
private setExpModelOrWait(modelId: string) {
797
const model = this.languageModelsService.lookupLanguageModel(modelId);
798
if (model) {
799
this.setCurrentLanguageModel({ metadata: model, identifier: modelId });
800
this.checkModelSupported();
801
this._waitForPersistedLanguageModel.clear();
802
} else {
803
this._waitForPersistedLanguageModel.value = this.languageModelsService.onDidChangeLanguageModels(() => {
804
const model = this.languageModelsService.lookupLanguageModel(modelId);
805
if (model) {
806
this._waitForPersistedLanguageModel.clear();
807
808
if (model.isUserSelectable) {
809
this.setCurrentLanguageModel({ metadata: model, identifier: modelId });
810
this.checkModelSupported();
811
}
812
}
813
});
814
}
815
}
816
817
private getDefaultModeExperimentStorageKey(): string {
818
const tag = this.options.widgetViewKindTag;
819
return `chat.${tag}.hasSetDefaultModeByExperiment`;
820
}
821
822
logInputHistory(): void {
823
const historyStr = [...this.history].map(entry => JSON.stringify(entry)).join('\n');
824
this.logService.info(`[${this.location}] Chat input history:`, historyStr);
825
}
826
827
setVisible(visible: boolean): void {
828
this._onDidChangeVisibility.fire(visible);
829
}
830
831
/** If consumers are busy generating the chat input, returns the promise resolved when they finish */
832
get generating() {
833
return this._generating?.defer.p;
834
}
835
836
/** Disables the input submissions buttons until the disposable is disposed. */
837
startGenerating(): IDisposable {
838
this.logService.trace('ChatWidget#startGenerating');
839
if (this._generating) {
840
this._generating.rc++;
841
} else {
842
this._generating = { rc: 1, defer: new DeferredPromise<void>() };
843
}
844
845
return toDisposable(() => {
846
this.logService.trace('ChatWidget#doneGenerating');
847
if (this._generating && !--this._generating.rc) {
848
this._generating.defer.complete();
849
this._generating = undefined;
850
}
851
});
852
}
853
854
get element(): HTMLElement {
855
return this.container;
856
}
857
858
async showPreviousValue(): Promise<void> {
859
const inputState = this.getInputState();
860
if (this.history.isAtEnd()) {
861
this.saveCurrentValue(inputState);
862
} else {
863
const currentEntry = this.getFilteredEntry(this._inputEditor.getValue(), inputState);
864
if (!this.history.has(currentEntry)) {
865
this.saveCurrentValue(inputState);
866
this.history.resetCursor();
867
}
868
}
869
870
this.navigateHistory(true);
871
}
872
873
async showNextValue(): Promise<void> {
874
const inputState = this.getInputState();
875
if (this.history.isAtEnd()) {
876
return;
877
} else {
878
const currentEntry = this.getFilteredEntry(this._inputEditor.getValue(), inputState);
879
if (!this.history.has(currentEntry)) {
880
this.saveCurrentValue(inputState);
881
this.history.resetCursor();
882
}
883
}
884
885
this.navigateHistory(false);
886
}
887
888
private async navigateHistory(previous: boolean): Promise<void> {
889
const historyEntry = previous ?
890
this.history.previous() : this.history.next();
891
892
let historyAttachments = historyEntry.state?.chatContextAttachments ?? [];
893
894
// Check for images in history to restore the value.
895
if (historyAttachments.length > 0) {
896
historyAttachments = (await Promise.all(historyAttachments.map(async (attachment) => {
897
if (isImageVariableEntry(attachment) && attachment.references?.length && URI.isUri(attachment.references[0].reference)) {
898
const currReference = attachment.references[0].reference;
899
try {
900
const imageBinary = currReference.toString(true).startsWith('http') ? await this.sharedWebExtracterService.readImage(currReference, CancellationToken.None) : (await this.fileService.readFile(currReference)).value;
901
if (!imageBinary) {
902
return undefined;
903
}
904
const newAttachment = { ...attachment };
905
newAttachment.value = (isImageVariableEntry(attachment) && attachment.isPasted) ? imageBinary.buffer : await resizeImage(imageBinary.buffer); // if pasted image, we do not need to resize.
906
return newAttachment;
907
} catch (err) {
908
this.logService.error('Failed to fetch and reference.', err);
909
return undefined;
910
}
911
}
912
return attachment;
913
}))).filter(attachment => attachment !== undefined);
914
}
915
916
this._attachmentModel.clearAndSetContext(...historyAttachments);
917
918
aria.status(historyEntry.text);
919
this.setValue(historyEntry.text, true);
920
921
this._onDidLoadInputState.fire(historyEntry.state);
922
923
const model = this._inputEditor.getModel();
924
if (!model) {
925
return;
926
}
927
928
if (previous) {
929
const endOfFirstViewLine = this._inputEditor._getViewModel()?.getLineLength(1) ?? 1;
930
const endOfFirstModelLine = model.getLineLength(1);
931
if (endOfFirstViewLine === endOfFirstModelLine) {
932
// Not wrapped - set cursor to the end of the first line
933
this._inputEditor.setPosition({ lineNumber: 1, column: endOfFirstViewLine + 1 });
934
} else {
935
// Wrapped - set cursor one char short of the end of the first view line.
936
// If it's after the next character, the cursor shows on the second line.
937
this._inputEditor.setPosition({ lineNumber: 1, column: endOfFirstViewLine });
938
}
939
} else {
940
this._inputEditor.setPosition(getLastPosition(model));
941
}
942
}
943
944
setValue(value: string, transient: boolean): void {
945
this.inputEditor.setValue(value);
946
// always leave cursor at the end
947
this.inputEditor.setPosition({ lineNumber: 1, column: value.length + 1 });
948
949
if (!transient) {
950
this.saveCurrentValue(this.getInputState());
951
}
952
}
953
954
private saveCurrentValue(inputState: IChatInputState): void {
955
const newEntry = this.getFilteredEntry(this._inputEditor.getValue(), inputState);
956
this.history.replaceLast(newEntry);
957
}
958
959
focus() {
960
this._inputEditor.focus();
961
}
962
963
hasFocus(): boolean {
964
return this._inputEditor.hasWidgetFocus();
965
}
966
967
/**
968
* Reset the input and update history.
969
* @param userQuery If provided, this will be added to the history. Followups and programmatic queries should not be passed.
970
*/
971
async acceptInput(isUserQuery?: boolean): Promise<void> {
972
if (isUserQuery) {
973
const userQuery = this._inputEditor.getValue();
974
const inputState = this.getInputState();
975
const entry = this.getFilteredEntry(userQuery, inputState);
976
this.history.replaceLast(entry);
977
this.history.add({ text: '' });
978
}
979
980
// Clear attached context, fire event to clear input state, and clear the input editor
981
this.attachmentModel.clear();
982
this._onDidLoadInputState.fire({});
983
if (this.accessibilityService.isScreenReaderOptimized() && isMacintosh) {
984
this._acceptInputForVoiceover();
985
} else {
986
this._inputEditor.focus();
987
this._inputEditor.setValue('');
988
}
989
}
990
991
validateAgentMode(): void {
992
if (!this.agentService.hasToolsAgent && this._currentModeObservable.get().kind === ChatModeKind.Agent) {
993
this.setChatMode(ChatModeKind.Edit);
994
}
995
}
996
997
// A function that filters out specifically the `value` property of the attachment.
998
private getFilteredEntry(query: string, inputState: IChatInputState): IChatHistoryEntry {
999
const attachmentsWithoutImageValues = inputState.chatContextAttachments?.map(attachment => {
1000
if (isImageVariableEntry(attachment) && attachment.references?.length && attachment.value) {
1001
const newAttachment = { ...attachment };
1002
newAttachment.value = undefined;
1003
return newAttachment;
1004
}
1005
return attachment;
1006
});
1007
1008
inputState.chatContextAttachments = attachmentsWithoutImageValues;
1009
const newEntry = {
1010
text: query,
1011
state: inputState,
1012
};
1013
1014
return newEntry;
1015
}
1016
1017
private _acceptInputForVoiceover(): void {
1018
const domNode = this._inputEditor.getDomNode();
1019
if (!domNode) {
1020
return;
1021
}
1022
// Remove the input editor from the DOM temporarily to prevent VoiceOver
1023
// from reading the cleared text (the request) to the user.
1024
domNode.remove();
1025
this._inputEditor.setValue('');
1026
this._inputEditorElement.appendChild(domNode);
1027
this._inputEditor.focus();
1028
}
1029
1030
private _handleAttachedContextChange() {
1031
this._hasFileAttachmentContextKey.set(Boolean(this._attachmentModel.attachments.find(a => a.kind === 'file')));
1032
this.renderAttachedContext();
1033
}
1034
1035
render(container: HTMLElement, initialValue: string, widget: IChatWidget) {
1036
let elements;
1037
if (this.options.renderStyle === 'compact') {
1038
elements = dom.h('.interactive-input-part', [
1039
dom.h('.interactive-input-and-edit-session', [
1040
dom.h('.chat-editing-session@chatEditingSessionWidgetContainer'),
1041
dom.h('.interactive-input-and-side-toolbar@inputAndSideToolbar', [
1042
dom.h('.chat-input-container@inputContainer', [
1043
dom.h('.chat-editor-container@editorContainer'),
1044
dom.h('.chat-input-toolbars@inputToolbars'),
1045
]),
1046
]),
1047
dom.h('.chat-attachments-container@attachmentsContainer', [
1048
dom.h('.chat-attachment-toolbar@attachmentToolbar'),
1049
dom.h('.chat-attached-context@attachedContextContainer'),
1050
dom.h('.chat-related-files@relatedFilesContainer'),
1051
]),
1052
dom.h('.interactive-input-followups@followupsContainer'),
1053
])
1054
]);
1055
} else {
1056
elements = dom.h('.interactive-input-part', [
1057
dom.h('.interactive-input-followups@followupsContainer'),
1058
dom.h('.chat-editing-session@chatEditingSessionWidgetContainer'),
1059
dom.h('.interactive-input-and-side-toolbar@inputAndSideToolbar', [
1060
dom.h('.chat-input-container@inputContainer', [
1061
dom.h('.chat-attachments-container@attachmentsContainer', [
1062
dom.h('.chat-attachment-toolbar@attachmentToolbar'),
1063
dom.h('.chat-related-files@relatedFilesContainer'),
1064
dom.h('.chat-attached-context@attachedContextContainer'),
1065
]),
1066
dom.h('.chat-editor-container@editorContainer'),
1067
dom.h('.chat-input-toolbars@inputToolbars'),
1068
]),
1069
]),
1070
]);
1071
}
1072
this.container = elements.root;
1073
this.chatInputOverlay = dom.$('.chat-input-overlay');
1074
container.append(this.chatInputOverlay);
1075
container.append(this.container);
1076
this.container.classList.toggle('compact', this.options.renderStyle === 'compact');
1077
this.followupsContainer = elements.followupsContainer;
1078
const inputAndSideToolbar = elements.inputAndSideToolbar; // The chat input and toolbar to the right
1079
const inputContainer = elements.inputContainer; // The chat editor, attachments, and toolbars
1080
const editorContainer = elements.editorContainer;
1081
this.attachmentsContainer = elements.attachmentsContainer;
1082
this.attachedContextContainer = elements.attachedContextContainer;
1083
this.relatedFilesContainer = elements.relatedFilesContainer;
1084
const toolbarsContainer = elements.inputToolbars;
1085
const attachmentToolbarContainer = elements.attachmentToolbar;
1086
this.chatEditingSessionWidgetContainer = elements.chatEditingSessionWidgetContainer;
1087
if (this.options.enableImplicitContext) {
1088
this._implicitContext = this._register(
1089
this.instantiationService.createInstance(ChatImplicitContext),
1090
);
1091
1092
this._register(this._implicitContext.onDidChangeValue(() => {
1093
this._indexOfLastAttachedContextDeletedWithKeyboard = -1;
1094
this._handleAttachedContextChange();
1095
}));
1096
}
1097
1098
this.renderAttachedContext();
1099
this._register(this._attachmentModel.onDidChange((e) => {
1100
if (e.added.length > 0) {
1101
this._indexOfLastAttachedContextDeletedWithKeyboard = -1;
1102
}
1103
this._handleAttachedContextChange();
1104
}));
1105
this.renderChatEditingSessionState(null);
1106
1107
if (this.options.renderWorkingSet) {
1108
this._relatedFiles = this._register(new ChatRelatedFiles());
1109
this._register(this._relatedFiles.onDidChange(() => this.renderChatRelatedFiles()));
1110
}
1111
this.renderChatRelatedFiles();
1112
1113
this.dnd.addOverlay(this.options.dndContainer ?? container, this.options.dndContainer ?? container);
1114
1115
const inputScopedContextKeyService = this._register(this.contextKeyService.createScoped(inputContainer));
1116
ChatContextKeys.inChatInput.bindTo(inputScopedContextKeyService).set(true);
1117
this.currentlyEditingInputKey = ChatContextKeys.currentlyEditingInput.bindTo(inputScopedContextKeyService);
1118
const scopedInstantiationService = this._register(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, inputScopedContextKeyService])));
1119
1120
const { historyNavigationBackwardsEnablement, historyNavigationForwardsEnablement } = this._register(registerAndCreateHistoryNavigationContext(inputScopedContextKeyService, this));
1121
this.historyNavigationBackwardsEnablement = historyNavigationBackwardsEnablement;
1122
this.historyNavigationForewardsEnablement = historyNavigationForwardsEnablement;
1123
1124
const options: IEditorConstructionOptions = getSimpleEditorOptions(this.configurationService);
1125
options.overflowWidgetsDomNode = this.options.editorOverflowWidgetsDomNode;
1126
options.pasteAs = EditorOptions.pasteAs.defaultValue;
1127
options.readOnly = false;
1128
options.ariaLabel = this._getAriaLabel();
1129
options.fontFamily = DEFAULT_FONT_FAMILY;
1130
options.fontSize = 13;
1131
options.lineHeight = 20;
1132
options.padding = this.options.renderStyle === 'compact' ? { top: 2, bottom: 2 } : { top: 8, bottom: 8 };
1133
options.cursorWidth = 1;
1134
options.wrappingStrategy = 'advanced';
1135
options.bracketPairColorization = { enabled: false };
1136
options.suggest = {
1137
showIcons: true,
1138
showSnippets: false,
1139
showWords: true,
1140
showStatusBar: false,
1141
insertMode: 'insert',
1142
};
1143
options.scrollbar = { ...(options.scrollbar ?? {}), vertical: 'hidden' };
1144
options.stickyScroll = { enabled: false };
1145
1146
this._inputEditorElement = dom.append(editorContainer!, $(chatInputEditorContainerSelector));
1147
const editorOptions = getSimpleCodeEditorWidgetOptions();
1148
editorOptions.contributions?.push(...EditorExtensionsRegistry.getSomeEditorContributions([ContentHoverController.ID, GlyphHoverController.ID, DropIntoEditorController.ID, CopyPasteController.ID, LinkDetector.ID]));
1149
this._inputEditor = this._register(scopedInstantiationService.createInstance(CodeEditorWidget, this._inputEditorElement, options, editorOptions));
1150
1151
SuggestController.get(this._inputEditor)?.forceRenderingAbove();
1152
options.overflowWidgetsDomNode?.classList.add('hideSuggestTextIcons');
1153
this._inputEditorElement.classList.add('hideSuggestTextIcons');
1154
1155
this._register(this._inputEditor.onDidChangeModelContent(() => {
1156
const currentHeight = Math.min(this._inputEditor.getContentHeight(), this.inputEditorMaxHeight);
1157
if (currentHeight !== this.inputEditorHeight) {
1158
this.inputEditorHeight = currentHeight;
1159
this._onDidChangeHeight.fire();
1160
}
1161
1162
const model = this._inputEditor.getModel();
1163
const inputHasText = !!model && model.getValue().trim().length > 0;
1164
this.inputEditorHasText.set(inputHasText);
1165
}));
1166
this._register(this._inputEditor.onDidContentSizeChange(e => {
1167
if (e.contentHeightChanged) {
1168
this.inputEditorHeight = !this.inline ? e.contentHeight : this.inputEditorHeight;
1169
this._onDidChangeHeight.fire();
1170
}
1171
}));
1172
this._register(this._inputEditor.onDidFocusEditorText(() => {
1173
this.inputEditorHasFocus.set(true);
1174
this._onDidFocus.fire();
1175
inputContainer.classList.toggle('focused', true);
1176
}));
1177
this._register(this._inputEditor.onDidBlurEditorText(() => {
1178
this.inputEditorHasFocus.set(false);
1179
inputContainer.classList.toggle('focused', false);
1180
1181
this._onDidBlur.fire();
1182
}));
1183
this._register(this._inputEditor.onDidBlurEditorWidget(() => {
1184
CopyPasteController.get(this._inputEditor)?.clearWidgets();
1185
DropIntoEditorController.get(this._inputEditor)?.clearWidgets();
1186
}));
1187
1188
const hoverDelegate = this._register(createInstantHoverDelegate());
1189
1190
this._register(dom.addStandardDisposableListener(toolbarsContainer, dom.EventType.CLICK, e => this.inputEditor.focus()));
1191
this._register(dom.addStandardDisposableListener(this.attachmentsContainer, dom.EventType.CLICK, e => this.inputEditor.focus()));
1192
this.inputActionsToolbar = this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, toolbarsContainer, MenuId.ChatInput, {
1193
telemetrySource: this.options.menus.telemetrySource,
1194
menuOptions: { shouldForwardArgs: true },
1195
hiddenItemStrategy: HiddenItemStrategy.Ignore,
1196
hoverDelegate,
1197
actionViewItemProvider: (action, options) => {
1198
if (action.id === ChatOpenModelPickerActionId && action instanceof MenuItemAction) {
1199
if (!this._currentLanguageModel) {
1200
this.setCurrentLanguageModelToDefault();
1201
}
1202
1203
const itemDelegate: IModelPickerDelegate = {
1204
getCurrentModel: () => this._currentLanguageModel,
1205
onDidChangeModel: this._onDidChangeCurrentLanguageModel.event,
1206
setModel: (model: ILanguageModelChatMetadataAndIdentifier) => {
1207
// The user changed the language model, so we don't wait for the persisted option to be registered
1208
this._waitForPersistedLanguageModel.clear();
1209
this.setCurrentLanguageModel(model);
1210
this.renderAttachedContext();
1211
},
1212
getModels: () => this.getModels()
1213
};
1214
return this.modelWidget = this.instantiationService.createInstance(ModelPickerActionItem, action, this._currentLanguageModel, itemDelegate);
1215
} else if (action.id === OpenModePickerAction.ID && action instanceof MenuItemAction) {
1216
const delegate: IModePickerDelegate = {
1217
currentMode: this._currentModeObservable
1218
};
1219
return this.modeWidget = this.instantiationService.createInstance(ModePickerActionItem, action, delegate);
1220
}
1221
1222
return undefined;
1223
}
1224
}));
1225
this.inputActionsToolbar.getElement().classList.add('chat-input-toolbar');
1226
this.inputActionsToolbar.context = { widget } satisfies IChatExecuteActionContext;
1227
this._register(this.inputActionsToolbar.onDidChangeMenuItems(() => {
1228
if (this.cachedDimensions && typeof this.cachedInputToolbarWidth === 'number' && this.cachedInputToolbarWidth !== this.inputActionsToolbar.getItemsWidth()) {
1229
this.layout(this.cachedDimensions.height, this.cachedDimensions.width);
1230
}
1231
}));
1232
this.executeToolbar = this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, toolbarsContainer, this.options.menus.executeToolbar, {
1233
telemetrySource: this.options.menus.telemetrySource,
1234
menuOptions: {
1235
shouldForwardArgs: true
1236
},
1237
hoverDelegate,
1238
hiddenItemStrategy: HiddenItemStrategy.Ignore, // keep it lean when hiding items and avoid a "..." overflow menu
1239
actionViewItemProvider: (action, options) => {
1240
if (this.location === ChatAgentLocation.Panel || this.location === ChatAgentLocation.Editor) {
1241
if ((action.id === ChatSubmitAction.ID || action.id === CancelAction.ID || action.id === ChatEditingSessionSubmitAction.ID) && action instanceof MenuItemAction) {
1242
const dropdownAction = this.instantiationService.createInstance(MenuItemAction, { id: 'chat.moreExecuteActions', title: localize('notebook.moreExecuteActionsLabel', "More..."), icon: Codicon.chevronDown }, undefined, undefined, undefined, undefined);
1243
return this.instantiationService.createInstance(ChatSubmitDropdownActionItem, action, dropdownAction, { ...options, menuAsChild: false });
1244
}
1245
}
1246
1247
return undefined;
1248
}
1249
}));
1250
this.executeToolbar.getElement().classList.add('chat-execute-toolbar');
1251
this.executeToolbar.context = { widget } satisfies IChatExecuteActionContext;
1252
this._register(this.executeToolbar.onDidChangeMenuItems(() => {
1253
if (this.cachedDimensions && typeof this.cachedExecuteToolbarWidth === 'number' && this.cachedExecuteToolbarWidth !== this.executeToolbar.getItemsWidth()) {
1254
this.layout(this.cachedDimensions.height, this.cachedDimensions.width);
1255
}
1256
}));
1257
if (this.options.menus.inputSideToolbar) {
1258
const toolbarSide = this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, inputAndSideToolbar, this.options.menus.inputSideToolbar, {
1259
telemetrySource: this.options.menus.telemetrySource,
1260
menuOptions: {
1261
shouldForwardArgs: true
1262
},
1263
hoverDelegate
1264
}));
1265
this.inputSideToolbarContainer = toolbarSide.getElement();
1266
toolbarSide.getElement().classList.add('chat-side-toolbar');
1267
toolbarSide.context = { widget } satisfies IChatExecuteActionContext;
1268
}
1269
1270
let inputModel = this.modelService.getModel(this.inputUri);
1271
if (!inputModel) {
1272
inputModel = this.modelService.createModel('', null, this.inputUri, true);
1273
}
1274
1275
this.textModelResolverService.createModelReference(this.inputUri).then(ref => {
1276
// make sure to hold a reference so that the model doesn't get disposed by the text model service
1277
if (this._store.isDisposed) {
1278
ref.dispose();
1279
return;
1280
}
1281
this._register(ref);
1282
});
1283
1284
this.inputModel = inputModel;
1285
this.inputModel.updateOptions({ bracketColorizationOptions: { enabled: false, independentColorPoolPerBracketType: false } });
1286
this._inputEditor.setModel(this.inputModel);
1287
if (initialValue) {
1288
this.inputModel.setValue(initialValue);
1289
const lineNumber = this.inputModel.getLineCount();
1290
this._inputEditor.setPosition({ lineNumber, column: this.inputModel.getLineMaxColumn(lineNumber) });
1291
}
1292
1293
const onDidChangeCursorPosition = () => {
1294
const model = this._inputEditor.getModel();
1295
if (!model) {
1296
return;
1297
}
1298
1299
const position = this._inputEditor.getPosition();
1300
if (!position) {
1301
return;
1302
}
1303
1304
const atTop = position.lineNumber === 1 && position.column - 1 <= (this._inputEditor._getViewModel()?.getLineLength(1) ?? 0);
1305
this.chatCursorAtTop.set(atTop);
1306
1307
this.historyNavigationBackwardsEnablement.set(atTop);
1308
this.historyNavigationForewardsEnablement.set(position.equals(getLastPosition(model)));
1309
};
1310
this._register(this._inputEditor.onDidChangeCursorPosition(e => onDidChangeCursorPosition()));
1311
onDidChangeCursorPosition();
1312
1313
this._register(this.themeService.onDidFileIconThemeChange(() => {
1314
this.renderAttachedContext();
1315
}));
1316
1317
this.addFilesToolbar = this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, attachmentToolbarContainer, MenuId.ChatInputAttachmentToolbar, {
1318
telemetrySource: this.options.menus.telemetrySource,
1319
label: true,
1320
menuOptions: { shouldForwardArgs: true, renderShortTitle: true },
1321
hiddenItemStrategy: HiddenItemStrategy.Ignore,
1322
hoverDelegate,
1323
actionViewItemProvider: (action, options) => {
1324
if (action.id === 'workbench.action.chat.attachContext') {
1325
const viewItem = this.instantiationService.createInstance(AddFilesButton, undefined, action, options);
1326
return viewItem;
1327
}
1328
return undefined;
1329
}
1330
}));
1331
this.addFilesToolbar.context = { widget, placeholder: localize('chatAttachFiles', 'Search for files and context to add to your request') };
1332
this._register(this.addFilesToolbar.onDidChangeMenuItems(() => {
1333
if (this.cachedDimensions) {
1334
this._onDidChangeHeight.fire();
1335
}
1336
}));
1337
}
1338
1339
public toggleChatInputOverlay(editing: boolean): void {
1340
this.chatInputOverlay.classList.toggle('disabled', editing);
1341
if (editing) {
1342
this.overlayClickListener.value = dom.addStandardDisposableListener(this.chatInputOverlay, dom.EventType.CLICK, e => {
1343
e.preventDefault();
1344
e.stopPropagation();
1345
this._onDidClickOverlay.fire();
1346
});
1347
} else {
1348
this.overlayClickListener.clear();
1349
}
1350
}
1351
1352
public renderAttachedContext() {
1353
const container = this.attachedContextContainer;
1354
// Note- can't measure attachedContextContainer, because it has `display: contents`, so measure the parent to check for height changes
1355
const oldHeight = this.attachmentsContainer.offsetHeight;
1356
const store = new DisposableStore();
1357
this.attachedContextDisposables.value = store;
1358
1359
dom.clearNode(container);
1360
const hoverDelegate = store.add(createInstantHoverDelegate());
1361
1362
store.add(dom.addStandardDisposableListener(this.attachmentsContainer, dom.EventType.KEY_DOWN, (e: StandardKeyboardEvent) => {
1363
this.handleAttachmentNavigation(e);
1364
}));
1365
1366
const attachments = [...this.attachmentModel.attachments.entries()];
1367
const hasAttachments = Boolean(attachments.length) || Boolean(this.implicitContext?.value);
1368
dom.setVisibility(Boolean(hasAttachments || (this.addFilesToolbar && !this.addFilesToolbar.isEmpty())), this.attachmentsContainer);
1369
dom.setVisibility(hasAttachments, this.attachedContextContainer);
1370
if (!attachments.length) {
1371
this._indexOfLastAttachedContextDeletedWithKeyboard = -1;
1372
this._indexOfLastOpenedContext = -1;
1373
}
1374
1375
const isSuggestedEnabled = this.configurationService.getValue<boolean>('chat.implicitContext.suggestedContext');
1376
1377
if (this.implicitContext?.value && !isSuggestedEnabled) {
1378
const implicitPart = store.add(this.instantiationService.createInstance(ImplicitContextAttachmentWidget, this.implicitContext, this._contextResourceLabels, this.attachmentModel));
1379
container.appendChild(implicitPart.domNode);
1380
}
1381
1382
this.promptFileAttached.set(this.hasPromptFileAttachments);
1383
1384
for (const [index, attachment] of attachments) {
1385
const resource = URI.isUri(attachment.value) ? attachment.value : attachment.value && typeof attachment.value === 'object' && 'uri' in attachment.value && URI.isUri(attachment.value.uri) ? attachment.value.uri : undefined;
1386
const range = attachment.value && typeof attachment.value === 'object' && 'range' in attachment.value && Range.isIRange(attachment.value.range) ? attachment.value.range : undefined;
1387
const shouldFocusClearButton = index === Math.min(this._indexOfLastAttachedContextDeletedWithKeyboard, this.attachmentModel.size - 1) && this._indexOfLastAttachedContextDeletedWithKeyboard > -1;
1388
1389
let attachmentWidget;
1390
const options = { shouldFocusClearButton, supportsDeletion: true };
1391
if (attachment.kind === 'tool' || attachment.kind === 'toolset') {
1392
attachmentWidget = this.instantiationService.createInstance(ToolSetOrToolItemAttachmentWidget, attachment, this._currentLanguageModel, options, container, this._contextResourceLabels, hoverDelegate);
1393
} else if (resource && isNotebookOutputVariableEntry(attachment)) {
1394
attachmentWidget = this.instantiationService.createInstance(NotebookCellOutputChatAttachmentWidget, resource, attachment, this._currentLanguageModel, options, container, this._contextResourceLabels, hoverDelegate);
1395
} else if (isPromptFileVariableEntry(attachment)) {
1396
attachmentWidget = this.instantiationService.createInstance(PromptFileAttachmentWidget, attachment, this._currentLanguageModel, options, container, this._contextResourceLabels, hoverDelegate);
1397
} else if (isPromptTextVariableEntry(attachment)) {
1398
attachmentWidget = this.instantiationService.createInstance(PromptTextAttachmentWidget, attachment, undefined, options, container, this._contextResourceLabels, hoverDelegate);
1399
} else if (resource && (attachment.kind === 'file' || attachment.kind === 'directory')) {
1400
attachmentWidget = this.instantiationService.createInstance(FileAttachmentWidget, resource, range, attachment, undefined, this._currentLanguageModel, options, container, this._contextResourceLabels, hoverDelegate);
1401
} else if (isImageVariableEntry(attachment)) {
1402
attachmentWidget = this.instantiationService.createInstance(ImageAttachmentWidget, resource, attachment, this._currentLanguageModel, options, container, this._contextResourceLabels, hoverDelegate);
1403
} else if (isElementVariableEntry(attachment)) {
1404
attachmentWidget = this.instantiationService.createInstance(ElementChatAttachmentWidget, attachment, this._currentLanguageModel, options, container, this._contextResourceLabels, hoverDelegate);
1405
} else if (isPasteVariableEntry(attachment)) {
1406
attachmentWidget = this.instantiationService.createInstance(PasteAttachmentWidget, attachment, this._currentLanguageModel, options, container, this._contextResourceLabels, hoverDelegate);
1407
} else if (isSCMHistoryItemVariableEntry(attachment)) {
1408
attachmentWidget = this.instantiationService.createInstance(SCMHistoryItemAttachmentWidget, attachment, this._currentLanguageModel, options, container, this._contextResourceLabels, hoverDelegate);
1409
} else {
1410
attachmentWidget = this.instantiationService.createInstance(DefaultChatAttachmentWidget, resource, range, attachment, undefined, this._currentLanguageModel, options, container, this._contextResourceLabels, hoverDelegate);
1411
}
1412
1413
if (shouldFocusClearButton) {
1414
attachmentWidget.element.focus();
1415
}
1416
1417
if (index === Math.min(this._indexOfLastOpenedContext, this.attachmentModel.size - 1)) {
1418
attachmentWidget.element.focus();
1419
}
1420
1421
store.add(attachmentWidget);
1422
store.add(attachmentWidget.onDidDelete(e => {
1423
this.handleAttachmentDeletion(e, index, attachment);
1424
}));
1425
1426
store.add(attachmentWidget.onDidOpen(e => {
1427
this.handleAttachmentOpen(index, attachment);
1428
}));
1429
}
1430
1431
const implicitUri = this.implicitContext?.value;
1432
const isUri = URI.isUri(implicitUri);
1433
1434
if (isSuggestedEnabled && implicitUri && (isUri || isLocation(implicitUri))) {
1435
const targetUri = isUri ? implicitUri : implicitUri.uri;
1436
const currentlyAttached = attachments.some(([, attachment]) => URI.isUri(attachment.value) && isEqual(attachment.value, targetUri));
1437
1438
const shouldShowImplicit = isUri ? !currentlyAttached : implicitUri.range;
1439
if (shouldShowImplicit) {
1440
const implicitPart = store.add(this.instantiationService.createInstance(ImplicitContextAttachmentWidget, this.implicitContext, this._contextResourceLabels, this._attachmentModel));
1441
container.appendChild(implicitPart.domNode);
1442
}
1443
}
1444
1445
if (oldHeight !== this.attachmentsContainer.offsetHeight) {
1446
this._onDidChangeHeight.fire();
1447
}
1448
1449
this._indexOfLastOpenedContext = -1;
1450
}
1451
1452
private handleAttachmentDeletion(e: KeyboardEvent | unknown, index: number, attachment: IChatRequestVariableEntry) {
1453
// Set focus to the next attached context item if deletion was triggered by a keystroke (vs a mouse click)
1454
if (dom.isKeyboardEvent(e)) {
1455
this._indexOfLastAttachedContextDeletedWithKeyboard = index;
1456
}
1457
1458
this._attachmentModel.delete(attachment.id);
1459
1460
1461
if (this.configurationService.getValue<boolean>('chat.implicitContext.enableImplicitContext')) {
1462
// if currently opened file is deleted, do not show implicit context
1463
const implicitValue = URI.isUri(this.implicitContext?.value) && URI.isUri(attachment.value) && isEqual(this.implicitContext.value, attachment.value);
1464
1465
if (this.implicitContext?.isFile && implicitValue) {
1466
this.implicitContext.enabled = false;
1467
}
1468
}
1469
1470
if (this._attachmentModel.size === 0) {
1471
this.focus();
1472
}
1473
1474
this._onDidChangeContext.fire({ removed: [attachment] });
1475
this.renderAttachedContext();
1476
}
1477
1478
private handleAttachmentOpen(index: number, attachment: IChatRequestVariableEntry): void {
1479
this._indexOfLastOpenedContext = index;
1480
this._indexOfLastAttachedContextDeletedWithKeyboard = -1;
1481
1482
if (this._attachmentModel.size === 0) {
1483
this.focus();
1484
}
1485
}
1486
1487
private handleAttachmentNavigation(e: StandardKeyboardEvent): void {
1488
if (!e.equals(KeyCode.LeftArrow) && !e.equals(KeyCode.RightArrow)) {
1489
return;
1490
}
1491
1492
const toolbar = this.addFilesToolbar?.getElement().querySelector('.action-label');
1493
if (!toolbar) {
1494
return;
1495
}
1496
1497
const attachments = Array.from(this.attachedContextContainer.querySelectorAll('.chat-attached-context-attachment'));
1498
if (!attachments.length) {
1499
return;
1500
}
1501
1502
attachments.unshift(toolbar);
1503
1504
const activeElement = dom.getWindow(this.attachmentsContainer).document.activeElement;
1505
const currentIndex = attachments.findIndex(attachment => attachment === activeElement);
1506
let newIndex = currentIndex;
1507
1508
if (e.equals(KeyCode.LeftArrow)) {
1509
newIndex = currentIndex > 0 ? currentIndex - 1 : attachments.length - 1;
1510
} else if (e.equals(KeyCode.RightArrow)) {
1511
newIndex = currentIndex < attachments.length - 1 ? currentIndex + 1 : 0;
1512
}
1513
1514
if (newIndex !== -1) {
1515
const nextElement = attachments[newIndex] as HTMLElement;
1516
nextElement.focus();
1517
e.preventDefault();
1518
e.stopPropagation();
1519
}
1520
}
1521
1522
async renderChatEditingSessionState(chatEditingSession: IChatEditingSession | null) {
1523
dom.setVisibility(Boolean(chatEditingSession), this.chatEditingSessionWidgetContainer);
1524
1525
if (chatEditingSession) {
1526
if (chatEditingSession.chatSessionId !== this._lastEditingSessionId) {
1527
this._workingSetCollapsed = true;
1528
}
1529
this._lastEditingSessionId = chatEditingSession.chatSessionId;
1530
}
1531
1532
const seenEntries = new ResourceSet();
1533
const entries: IChatCollapsibleListItem[] = [];
1534
if (chatEditingSession) {
1535
for (const entry of chatEditingSession.entries.get()) {
1536
if (entry.state.get() !== ModifiedFileEntryState.Modified) {
1537
continue;
1538
}
1539
1540
if (!seenEntries.has(entry.modifiedURI)) {
1541
seenEntries.add(entry.modifiedURI);
1542
const linesAdded = entry.linesAdded?.get();
1543
const linesRemoved = entry.linesRemoved?.get();
1544
entries.push({
1545
reference: entry.modifiedURI,
1546
state: entry.state.get(),
1547
kind: 'reference',
1548
options: {
1549
status: undefined,
1550
diffMeta: { added: linesAdded ?? 0, removed: linesRemoved ?? 0 }
1551
}
1552
});
1553
}
1554
}
1555
}
1556
1557
if (!chatEditingSession || !this.options.renderWorkingSet || !entries.length) {
1558
dom.clearNode(this.chatEditingSessionWidgetContainer);
1559
this._chatEditsDisposables.clear();
1560
this._chatEditList = undefined;
1561
return;
1562
}
1563
1564
// Summary of number of files changed
1565
const innerContainer = this.chatEditingSessionWidgetContainer.querySelector('.chat-editing-session-container.show-file-icons') as HTMLElement ?? dom.append(this.chatEditingSessionWidgetContainer, $('.chat-editing-session-container.show-file-icons'));
1566
1567
entries.sort((a, b) => {
1568
if (a.kind === 'reference' && b.kind === 'reference') {
1569
if (a.state === b.state || a.state === undefined || b.state === undefined) {
1570
return a.reference.toString().localeCompare(b.reference.toString());
1571
}
1572
return a.state - b.state;
1573
}
1574
return 0;
1575
});
1576
1577
const overviewRegion = innerContainer.querySelector('.chat-editing-session-overview') as HTMLElement ?? dom.append(innerContainer, $('.chat-editing-session-overview'));
1578
const overviewTitle = overviewRegion.querySelector('.working-set-title') as HTMLElement ?? dom.append(overviewRegion, $('.working-set-title'));
1579
1580
// Clear out the previous actions (if any)
1581
this._chatEditsActionsDisposables.clear();
1582
1583
// Chat editing session actions
1584
const actionsContainer = overviewRegion.querySelector('.chat-editing-session-actions') as HTMLElement ?? dom.append(overviewRegion, $('.chat-editing-session-actions'));
1585
1586
this._chatEditsActionsDisposables.add(this.instantiationService.createInstance(MenuWorkbenchButtonBar, actionsContainer, MenuId.ChatEditingWidgetToolbar, {
1587
telemetrySource: this.options.menus.telemetrySource,
1588
menuOptions: {
1589
arg: { sessionId: chatEditingSession.chatSessionId },
1590
},
1591
buttonConfigProvider: (action) => {
1592
if (action.id === ChatEditingShowChangesAction.ID || action.id === ViewPreviousEditsAction.Id) {
1593
return { showIcon: true, showLabel: false, isSecondary: true };
1594
}
1595
return undefined;
1596
}
1597
}));
1598
1599
if (!chatEditingSession) {
1600
return;
1601
}
1602
1603
// Working set
1604
const workingSetContainer = innerContainer.querySelector('.chat-editing-session-list') as HTMLElement ?? dom.append(innerContainer, $('.chat-editing-session-list'));
1605
1606
const button = this._chatEditsActionsDisposables.add(new ButtonWithIcon(overviewTitle, {
1607
supportIcons: true,
1608
secondary: true,
1609
ariaLabel: localize('chatEditingSession.toggleWorkingSet', 'Toggle changed files.'),
1610
}));
1611
1612
let added = 0;
1613
let removed = 0;
1614
if (chatEditingSession) {
1615
for (const entry of chatEditingSession.entries.get()) {
1616
if (entry.linesAdded && entry.linesRemoved) {
1617
added += entry.linesAdded.get();
1618
removed += entry.linesRemoved.get();
1619
}
1620
}
1621
}
1622
1623
const baseLabel = entries.length === 1 ? localize('chatEditingSession.oneFile.1', '1 file changed') : localize('chatEditingSession.manyFiles.1', '{0} files changed', entries.length);
1624
button.label = baseLabel;
1625
1626
if (!this._workingSetLinesAddedSpan) {
1627
this._workingSetLinesAddedSpan = dom.$('.working-set-lines-added');
1628
}
1629
if (!this._workingSetLinesRemovedSpan) {
1630
this._workingSetLinesRemovedSpan = dom.$('.working-set-lines-removed');
1631
}
1632
1633
const countsContainer = dom.$('.working-set-line-counts');
1634
button.element.appendChild(countsContainer);
1635
countsContainer.appendChild(this._workingSetLinesAddedSpan);
1636
countsContainer.appendChild(this._workingSetLinesRemovedSpan);
1637
1638
this._workingSetLinesAddedSpan.textContent = `+${added}`;
1639
this._workingSetLinesRemovedSpan.textContent = `-${removed}`;
1640
button.element.setAttribute('aria-label', localize('chatEditingSession.ariaLabelWithCounts', '{0}, {1} lines added, {2} lines removed', baseLabel, added, removed));
1641
1642
const applyCollapseState = () => {
1643
button.icon = this._workingSetCollapsed ? Codicon.chevronRight : Codicon.chevronDown;
1644
workingSetContainer.classList.toggle('collapsed', this._workingSetCollapsed);
1645
this._onDidChangeHeight.fire();
1646
};
1647
1648
const toggleWorkingSet = () => {
1649
this._workingSetCollapsed = !this._workingSetCollapsed;
1650
applyCollapseState();
1651
};
1652
1653
this._chatEditsActionsDisposables.add(button.onDidClick(() => { toggleWorkingSet(); }));
1654
this._chatEditsActionsDisposables.add(addDisposableListener(overviewRegion, 'click', e => {
1655
if (e.defaultPrevented) {
1656
return;
1657
}
1658
const target = e.target as HTMLElement;
1659
if (target.closest('.monaco-button')) {
1660
return;
1661
}
1662
toggleWorkingSet();
1663
}));
1664
1665
applyCollapseState();
1666
1667
if (!this._chatEditList) {
1668
this._chatEditList = this._chatEditsListPool.get();
1669
const list = this._chatEditList.object;
1670
this._chatEditsDisposables.add(this._chatEditList);
1671
this._chatEditsDisposables.add(list.onDidFocus(() => {
1672
this._onDidFocus.fire();
1673
}));
1674
this._chatEditsDisposables.add(list.onDidOpen(async (e) => {
1675
if (e.element?.kind === 'reference' && URI.isUri(e.element.reference)) {
1676
const modifiedFileUri = e.element.reference;
1677
1678
const entry = chatEditingSession.getEntry(modifiedFileUri);
1679
1680
const pane = await this.editorService.openEditor({
1681
resource: modifiedFileUri,
1682
options: e.editorOptions
1683
}, e.sideBySide ? SIDE_GROUP : ACTIVE_GROUP);
1684
1685
if (pane) {
1686
entry?.getEditorIntegration(pane).reveal(true, e.editorOptions.preserveFocus);
1687
}
1688
}
1689
}));
1690
this._chatEditsDisposables.add(addDisposableListener(list.getHTMLElement(), 'click', e => {
1691
if (!this.hasFocus()) {
1692
this._onDidFocus.fire();
1693
}
1694
}, true));
1695
dom.append(workingSetContainer, list.getHTMLElement());
1696
dom.append(innerContainer, workingSetContainer);
1697
}
1698
1699
const maxItemsShown = 6;
1700
const itemsShown = Math.min(entries.length, maxItemsShown);
1701
const height = itemsShown * 22;
1702
const list = this._chatEditList.object;
1703
list.layout(height);
1704
list.getHTMLElement().style.height = `${height}px`;
1705
list.splice(0, list.length, entries);
1706
this._onDidChangeHeight.fire();
1707
}
1708
1709
async renderChatRelatedFiles() {
1710
const anchor = this.relatedFilesContainer;
1711
dom.clearNode(anchor);
1712
const shouldRender = this.configurationService.getValue('chat.renderRelatedFiles');
1713
dom.setVisibility(Boolean(this.relatedFiles?.value.length && shouldRender), anchor);
1714
if (!shouldRender || !this.relatedFiles?.value.length) {
1715
return;
1716
}
1717
1718
const hoverDelegate = getDefaultHoverDelegate('element');
1719
for (const { uri, description } of this.relatedFiles.value) {
1720
const uriLabel = this._chatEditsActionsDisposables.add(new Button(anchor, {
1721
supportIcons: true,
1722
secondary: true,
1723
hoverDelegate
1724
}));
1725
uriLabel.label = this.labelService.getUriBasenameLabel(uri);
1726
uriLabel.element.classList.add('monaco-icon-label');
1727
uriLabel.element.title = localize('suggeste.title', "{0} - {1}", this.labelService.getUriLabel(uri, { relative: true }), description ?? '');
1728
1729
this._chatEditsActionsDisposables.add(uriLabel.onDidClick(async () => {
1730
group.remove(); // REMOVE asap
1731
await this._attachmentModel.addFile(uri);
1732
this.relatedFiles?.remove(uri);
1733
}));
1734
1735
const addButton = this._chatEditsActionsDisposables.add(new Button(anchor, {
1736
supportIcons: false,
1737
secondary: true,
1738
hoverDelegate,
1739
ariaLabel: localize('chatEditingSession.addSuggestion', 'Add suggestion {0}', this.labelService.getUriLabel(uri, { relative: true })),
1740
}));
1741
addButton.icon = Codicon.add;
1742
addButton.setTitle(localize('chatEditingSession.addSuggested', 'Add suggestion'));
1743
this._chatEditsActionsDisposables.add(addButton.onDidClick(async () => {
1744
group.remove(); // REMOVE asap
1745
await this._attachmentModel.addFile(uri);
1746
this.relatedFiles?.remove(uri);
1747
}));
1748
1749
const sep = document.createElement('div');
1750
sep.classList.add('separator');
1751
1752
const group = document.createElement('span');
1753
group.classList.add('monaco-button-dropdown', 'sidebyside-button');
1754
group.appendChild(addButton.element);
1755
group.appendChild(sep);
1756
group.appendChild(uriLabel.element);
1757
dom.append(anchor, group);
1758
1759
this._chatEditsActionsDisposables.add(toDisposable(() => {
1760
group.remove();
1761
}));
1762
}
1763
this._onDidChangeHeight.fire();
1764
}
1765
1766
async renderFollowups(items: IChatFollowup[] | undefined, response: IChatResponseViewModel | undefined): Promise<void> {
1767
if (!this.options.renderFollowups) {
1768
return;
1769
}
1770
this.followupsDisposables.clear();
1771
dom.clearNode(this.followupsContainer);
1772
1773
if (items && items.length > 0) {
1774
this.followupsDisposables.add(this.instantiationService.createInstance<typeof ChatFollowups<IChatFollowup>, ChatFollowups<IChatFollowup>>(ChatFollowups, this.followupsContainer, items, this.location, undefined, followup => this._onDidAcceptFollowup.fire({ followup, response })));
1775
}
1776
this._onDidChangeHeight.fire();
1777
}
1778
1779
get contentHeight(): number {
1780
const data = this.getLayoutData();
1781
return data.followupsHeight + data.inputPartEditorHeight + data.inputPartVerticalPadding + data.inputEditorBorder + data.attachmentsHeight + data.toolbarsHeight + data.chatEditingStateHeight;
1782
}
1783
1784
layout(height: number, width: number) {
1785
this.cachedDimensions = new dom.Dimension(width, height);
1786
1787
return this._layout(height, width);
1788
}
1789
1790
private previousInputEditorDimension: IDimension | undefined;
1791
private _layout(height: number, width: number, allowRecurse = true): void {
1792
const data = this.getLayoutData();
1793
const inputEditorHeight = Math.min(data.inputPartEditorHeight, height - data.followupsHeight - data.attachmentsHeight - data.inputPartVerticalPadding - data.toolbarsHeight);
1794
1795
const followupsWidth = width - data.inputPartHorizontalPadding;
1796
this.followupsContainer.style.width = `${followupsWidth}px`;
1797
1798
this._inputPartHeight = data.inputPartVerticalPadding + data.followupsHeight + inputEditorHeight + data.inputEditorBorder + data.attachmentsHeight + data.toolbarsHeight + data.chatEditingStateHeight;
1799
this._followupsHeight = data.followupsHeight;
1800
this._editSessionWidgetHeight = data.chatEditingStateHeight;
1801
1802
const initialEditorScrollWidth = this._inputEditor.getScrollWidth();
1803
const newEditorWidth = width - data.inputPartHorizontalPadding - data.editorBorder - data.inputPartHorizontalPaddingInside - data.toolbarsWidth - data.sideToolbarWidth;
1804
const newDimension = { width: newEditorWidth, height: inputEditorHeight };
1805
if (!this.previousInputEditorDimension || (this.previousInputEditorDimension.width !== newDimension.width || this.previousInputEditorDimension.height !== newDimension.height)) {
1806
// This layout call has side-effects that are hard to understand. eg if we are calling this inside a onDidChangeContent handler, this can trigger the next onDidChangeContent handler
1807
// to be invoked, and we have a lot of these on this editor. Only doing a layout this when the editor size has actually changed makes it much easier to follow.
1808
this._inputEditor.layout(newDimension);
1809
this.previousInputEditorDimension = newDimension;
1810
}
1811
1812
if (allowRecurse && initialEditorScrollWidth < 10) {
1813
// This is probably the initial layout. Now that the editor is layed out with its correct width, it should report the correct contentHeight
1814
return this._layout(height, width, false);
1815
}
1816
}
1817
1818
private getLayoutData() {
1819
const executeToolbarWidth = this.cachedExecuteToolbarWidth = this.executeToolbar.getItemsWidth();
1820
const inputToolbarWidth = this.cachedInputToolbarWidth = this.inputActionsToolbar.getItemsWidth();
1821
const executeToolbarPadding = (this.executeToolbar.getItemsLength() - 1) * 4;
1822
const inputToolbarPadding = this.inputActionsToolbar.getItemsLength() ? (this.inputActionsToolbar.getItemsLength() - 1) * 4 : 0;
1823
return {
1824
inputEditorBorder: 2,
1825
followupsHeight: this.followupsContainer.offsetHeight,
1826
inputPartEditorHeight: Math.min(this._inputEditor.getContentHeight(), this.inputEditorMaxHeight),
1827
inputPartHorizontalPadding: this.options.renderStyle === 'compact' ? 16 : 32,
1828
inputPartVerticalPadding: this.options.renderStyle === 'compact' ? 12 : 28,
1829
attachmentsHeight: this.attachmentsHeight,
1830
editorBorder: 2,
1831
inputPartHorizontalPaddingInside: 12,
1832
toolbarsWidth: this.options.renderStyle === 'compact' ? executeToolbarWidth + executeToolbarPadding + inputToolbarWidth + inputToolbarPadding : 0,
1833
toolbarsHeight: this.options.renderStyle === 'compact' ? 0 : 22,
1834
chatEditingStateHeight: this.chatEditingSessionWidgetContainer.offsetHeight,
1835
sideToolbarWidth: this.inputSideToolbarContainer ? dom.getTotalWidth(this.inputSideToolbarContainer) + 4 /*gap*/ : 0,
1836
};
1837
}
1838
1839
getViewState(): IChatInputState {
1840
return this.getInputState();
1841
}
1842
1843
saveState(): void {
1844
if (this.history.isAtEnd()) {
1845
this.saveCurrentValue(this.getInputState());
1846
}
1847
1848
const inputHistory = [...this.history];
1849
this.historyService.saveHistory(this.location, inputHistory);
1850
}
1851
}
1852
1853
const historyKeyFn = (entry: IChatHistoryEntry) => JSON.stringify({ ...entry, state: { ...entry.state, chatMode: undefined } });
1854
1855
function getLastPosition(model: ITextModel): IPosition {
1856
return { lineNumber: model.getLineCount(), column: model.getLineLength(model.getLineCount()) + 1 };
1857
}
1858
1859
// This does seems like a lot just to customize an item with dropdown. This whole class exists just because we need an
1860
// onDidChange listener on the submenu, which is apparently not needed in other cases.
1861
class ChatSubmitDropdownActionItem extends DropdownWithPrimaryActionViewItem {
1862
constructor(
1863
action: MenuItemAction,
1864
dropdownAction: IAction,
1865
options: IDropdownWithPrimaryActionViewItemOptions,
1866
@IMenuService menuService: IMenuService,
1867
@IContextMenuService contextMenuService: IContextMenuService,
1868
@IContextKeyService contextKeyService: IContextKeyService,
1869
@IKeybindingService keybindingService: IKeybindingService,
1870
@INotificationService notificationService: INotificationService,
1871
@IThemeService themeService: IThemeService,
1872
@IAccessibilityService accessibilityService: IAccessibilityService
1873
) {
1874
super(
1875
action,
1876
dropdownAction,
1877
[],
1878
'',
1879
{
1880
...options,
1881
getKeyBinding: (action: IAction) => keybindingService.lookupKeybinding(action.id, contextKeyService)
1882
},
1883
contextMenuService,
1884
keybindingService,
1885
notificationService,
1886
contextKeyService,
1887
themeService,
1888
accessibilityService);
1889
const menu = menuService.createMenu(MenuId.ChatExecuteSecondary, contextKeyService);
1890
const setActions = () => {
1891
const secondary = getFlatActionBarActions(menu.getActions({ shouldForwardArgs: true }));
1892
this.update(dropdownAction, secondary);
1893
};
1894
setActions();
1895
this._register(menu.onDidChange(() => setActions()));
1896
}
1897
}
1898
1899
const chatInputEditorContainerSelector = '.interactive-input-editor';
1900
setupSimpleEditorSelectionStyling(chatInputEditorContainerSelector);
1901
1902
class AddFilesButton extends ActionViewItem {
1903
1904
constructor(context: unknown, action: IAction, options: IActionViewItemOptions) {
1905
super(context, action, {
1906
...options,
1907
icon: false,
1908
label: true,
1909
keybindingNotRenderedWithLabel: true,
1910
});
1911
}
1912
1913
override render(container: HTMLElement): void {
1914
container.classList.add('chat-attachment-button');
1915
super.render(container);
1916
}
1917
1918
protected override updateLabel(): void {
1919
assertType(this.label);
1920
const message = `$(attach) ${this.action.label}`;
1921
dom.reset(this.label, ...renderLabelWithIcons(message));
1922
}
1923
}
1924
1925