Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/widget/chatWidget.ts
4780 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import './media/chat.css';
7
import './media/chatAgentHover.css';
8
import './media/chatViewWelcome.css';
9
import * as dom from '../../../../../base/browser/dom.js';
10
import { IMouseWheelEvent } from '../../../../../base/browser/mouseEvent.js';
11
import { Button } from '../../../../../base/browser/ui/button/button.js';
12
import { ITreeContextMenuEvent, ITreeElement } from '../../../../../base/browser/ui/tree/tree.js';
13
import { disposableTimeout, timeout } from '../../../../../base/common/async.js';
14
import { CancellationToken } from '../../../../../base/common/cancellation.js';
15
import { Codicon } from '../../../../../base/common/codicons.js';
16
import { toErrorMessage } from '../../../../../base/common/errorMessage.js';
17
import { Emitter, Event } from '../../../../../base/common/event.js';
18
import { FuzzyScore } from '../../../../../base/common/filters.js';
19
import { IMarkdownString, MarkdownString } from '../../../../../base/common/htmlContent.js';
20
import { Iterable } from '../../../../../base/common/iterator.js';
21
import { Disposable, DisposableStore, IDisposable, MutableDisposable, thenIfNotDisposed } from '../../../../../base/common/lifecycle.js';
22
import { ResourceSet } from '../../../../../base/common/map.js';
23
import { Schemas } from '../../../../../base/common/network.js';
24
import { filter } from '../../../../../base/common/objects.js';
25
import { autorun, observableFromEvent, observableValue } from '../../../../../base/common/observable.js';
26
import { basename, extUri, isEqual } from '../../../../../base/common/resources.js';
27
import { MicrotaskDelay } from '../../../../../base/common/symbols.js';
28
import { isDefined } from '../../../../../base/common/types.js';
29
import { URI } from '../../../../../base/common/uri.js';
30
import { ICodeEditor } from '../../../../../editor/browser/editorBrowser.js';
31
import { ICodeEditorService } from '../../../../../editor/browser/services/codeEditorService.js';
32
import { OffsetRange } from '../../../../../editor/common/core/ranges/offsetRange.js';
33
import { localize } from '../../../../../nls.js';
34
import { MenuId } from '../../../../../platform/actions/common/actions.js';
35
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
36
import { ContextKeyExpr, IContextKey, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';
37
import { IContextMenuService } from '../../../../../platform/contextview/browser/contextView.js';
38
import { ITextResourceEditorInput } from '../../../../../platform/editor/common/editor.js';
39
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
40
import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js';
41
import { WorkbenchObjectTree } from '../../../../../platform/list/browser/listService.js';
42
import { ILogService } from '../../../../../platform/log/common/log.js';
43
import { bindContextKey } from '../../../../../platform/observable/common/platformObservableUtils.js';
44
import product from '../../../../../platform/product/common/product.js';
45
import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';
46
import { buttonSecondaryBackground, buttonSecondaryForeground, buttonSecondaryHoverBackground } from '../../../../../platform/theme/common/colorRegistry.js';
47
import { asCssVariable } from '../../../../../platform/theme/common/colorUtils.js';
48
import { IThemeService } from '../../../../../platform/theme/common/themeService.js';
49
import { IWorkspaceContextService, WorkbenchState } from '../../../../../platform/workspace/common/workspace.js';
50
import { EditorResourceAccessor } from '../../../../common/editor.js';
51
import { IEditorService } from '../../../../services/editor/common/editorService.js';
52
import { IChatEntitlementService } from '../../../../services/chat/common/chatEntitlementService.js';
53
import { ILifecycleService } from '../../../../services/lifecycle/common/lifecycle.js';
54
import { katexContainerClassName } from '../../../markdown/common/markedKatexExtension.js';
55
import { checkModeOption } from '../../common/chat.js';
56
import { IChatAgentAttachmentCapabilities, IChatAgentCommand, IChatAgentData, IChatAgentService } from '../../common/participants/chatAgents.js';
57
import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';
58
import { applyingChatEditsFailedContextKey, decidedChatEditingResourceContextKey, hasAppliedChatEditsContextKey, hasUndecidedChatEditingResourceContextKey, IChatEditingService, IChatEditingSession, inChatEditingSessionContextKey, ModifiedFileEntryState } from '../../common/editing/chatEditingService.js';
59
import { IChatLayoutService } from '../../common/widget/chatLayoutService.js';
60
import { IChatModel, IChatModelInputState, IChatResponseModel } from '../../common/model/chatModel.js';
61
import { ChatMode, IChatModeService } from '../../common/chatModes.js';
62
import { chatAgentLeader, ChatRequestAgentPart, ChatRequestDynamicVariablePart, ChatRequestSlashPromptPart, ChatRequestToolPart, ChatRequestToolSetPart, chatSubcommandLeader, formatChatQuestion, IParsedChatRequest } from '../../common/requestParser/chatParserTypes.js';
63
import { ChatRequestParser } from '../../common/requestParser/chatRequestParser.js';
64
import { IChatLocationData, IChatSendRequestOptions, IChatService } from '../../common/chatService/chatService.js';
65
import { IChatSessionsService } from '../../common/chatSessionsService.js';
66
import { IChatSlashCommandService } from '../../common/participants/chatSlashCommands.js';
67
import { IChatTodoListService } from '../../common/tools/chatTodoListService.js';
68
import { ChatRequestVariableSet, IChatRequestVariableEntry, isPromptFileVariableEntry, isPromptTextVariableEntry, isWorkspaceVariableEntry, PromptFileVariableKind, toPromptFileVariableEntry } from '../../common/attachments/chatVariableEntries.js';
69
import { ChatViewModel, IChatRequestViewModel, IChatResponseViewModel, isRequestVM, isResponseVM } from '../../common/model/chatViewModel.js';
70
import { CodeBlockModelCollection } from '../../common/widget/codeBlockModelCollection.js';
71
import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../common/constants.js';
72
import { ILanguageModelToolsService, ToolSet } from '../../common/tools/languageModelToolsService.js';
73
import { ComputeAutomaticInstructions } from '../../common/promptSyntax/computeAutomaticInstructions.js';
74
import { PromptsConfig } from '../../common/promptSyntax/config/config.js';
75
import { IHandOff, PromptHeader, Target } from '../../common/promptSyntax/promptFileParser.js';
76
import { IPromptsService } from '../../common/promptSyntax/service/promptsService.js';
77
import { handleModeSwitch } from '../actions/chatActions.js';
78
import { ChatTreeItem, IChatAcceptInputOptions, IChatAccessibilityService, IChatCodeBlockInfo, IChatFileTreeInfo, IChatListItemRendererOptions, IChatWidget, IChatWidgetService, IChatWidgetViewContext, IChatWidgetViewModelChangeEvent, IChatWidgetViewOptions, isIChatResourceViewContext, isIChatViewViewContext } from '../chat.js';
79
import { ChatAccessibilityProvider } from '../accessibility/chatAccessibilityProvider.js';
80
import { ChatAttachmentModel } from '../attachments/chatAttachmentModel.js';
81
import { ChatSuggestNextWidget } from './chatContentParts/chatSuggestNextWidget.js';
82
import { ChatInputPart, IChatInputPartOptions, IChatInputStyles } from './input/chatInputPart.js';
83
import { ChatListDelegate, ChatListItemRenderer, IChatListItemTemplate, IChatRendererDelegate } from './chatListRenderer.js';
84
import { ChatEditorOptions } from './chatOptions.js';
85
import { ChatViewWelcomePart, IChatSuggestedPrompts, IChatViewWelcomeContent } from '../viewsWelcome/chatViewWelcomeController.js';
86
import { IAgentSessionsService } from '../agentSessions/agentSessionsService.js';
87
88
const $ = dom.$;
89
90
export interface IChatWidgetStyles extends IChatInputStyles {
91
readonly inputEditorBackground: string;
92
readonly resultEditorBackground: string;
93
}
94
95
export interface IChatWidgetContrib extends IDisposable {
96
97
readonly id: string;
98
99
/**
100
* A piece of state which is related to the input editor of the chat widget.
101
* Takes in the `contrib` object that will be saved in the {@link IChatModelInputState}.
102
*/
103
getInputState?(contrib: Record<string, unknown>): void;
104
105
/**
106
* Called with the result of getInputState when navigating input history.
107
*/
108
setInputState?(contrib: Readonly<Record<string, unknown>>): void;
109
}
110
111
interface IChatRequestInputOptions {
112
input: string;
113
attachedContext: ChatRequestVariableSet;
114
}
115
116
export interface IChatWidgetLocationOptions {
117
location: ChatAgentLocation;
118
119
resolveData?(): IChatLocationData | undefined;
120
}
121
122
export function isQuickChat(widget: IChatWidget): boolean {
123
return isIChatResourceViewContext(widget.viewContext) && Boolean(widget.viewContext.isQuickChat);
124
}
125
126
function isInlineChat(widget: IChatWidget): boolean {
127
return isIChatResourceViewContext(widget.viewContext) && Boolean(widget.viewContext.isInlineChat);
128
}
129
130
type ChatHandoffClickEvent = {
131
fromAgent: string;
132
toAgent: string;
133
hasPrompt: boolean;
134
autoSend: boolean;
135
};
136
137
type ChatHandoffClickClassification = {
138
owner: 'digitarald';
139
comment: 'Event fired when a user clicks on a handoff prompt in the chat suggest-next widget';
140
fromAgent: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The agent/mode the user was in before clicking the handoff' };
141
toAgent: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The agent/mode specified in the handoff' };
142
hasPrompt: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether the handoff includes a prompt' };
143
autoSend: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Whether the handoff automatically submits the request' };
144
};
145
146
type ChatHandoffWidgetShownEvent = {
147
agent: string;
148
handoffCount: number;
149
};
150
151
type ChatHandoffWidgetShownClassification = {
152
owner: 'digitarald';
153
comment: 'Event fired when the suggest-next widget is shown with handoff prompts';
154
agent: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The current agent/mode that has handoffs defined' };
155
handoffCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; isMeasurement: true; comment: 'Number of handoff options shown to the user' };
156
};
157
158
const supportsAllAttachments: Required<IChatAgentAttachmentCapabilities> = {
159
supportsFileAttachments: true,
160
supportsToolAttachments: true,
161
supportsMCPAttachments: true,
162
supportsImageAttachments: true,
163
supportsSearchResultAttachments: true,
164
supportsInstructionAttachments: true,
165
supportsSourceControlAttachments: true,
166
supportsProblemAttachments: true,
167
supportsSymbolAttachments: true,
168
supportsTerminalAttachments: true,
169
};
170
171
const DISCLAIMER = localize('chatDisclaimer', "AI responses may be inaccurate.");
172
173
export class ChatWidget extends Disposable implements IChatWidget {
174
175
// eslint-disable-next-line @typescript-eslint/no-explicit-any
176
static readonly CONTRIBS: { new(...args: [IChatWidget, ...any]): IChatWidgetContrib }[] = [];
177
178
private readonly _onDidSubmitAgent = this._register(new Emitter<{ agent: IChatAgentData; slashCommand?: IChatAgentCommand }>());
179
readonly onDidSubmitAgent = this._onDidSubmitAgent.event;
180
181
private _onDidChangeAgent = this._register(new Emitter<{ agent: IChatAgentData; slashCommand?: IChatAgentCommand }>());
182
readonly onDidChangeAgent = this._onDidChangeAgent.event;
183
184
private _onDidFocus = this._register(new Emitter<void>());
185
readonly onDidFocus = this._onDidFocus.event;
186
187
private _onDidChangeViewModel = this._register(new Emitter<IChatWidgetViewModelChangeEvent>());
188
readonly onDidChangeViewModel = this._onDidChangeViewModel.event;
189
190
private _onDidScroll = this._register(new Emitter<void>());
191
readonly onDidScroll = this._onDidScroll.event;
192
193
private _onDidAcceptInput = this._register(new Emitter<void>());
194
readonly onDidAcceptInput = this._onDidAcceptInput.event;
195
196
private _onDidHide = this._register(new Emitter<void>());
197
readonly onDidHide = this._onDidHide.event;
198
199
private _onDidShow = this._register(new Emitter<void>());
200
readonly onDidShow = this._onDidShow.event;
201
202
private _onDidChangeParsedInput = this._register(new Emitter<void>());
203
readonly onDidChangeParsedInput = this._onDidChangeParsedInput.event;
204
205
private readonly _onWillMaybeChangeHeight = new Emitter<void>();
206
readonly onWillMaybeChangeHeight: Event<void> = this._onWillMaybeChangeHeight.event;
207
208
private _onDidChangeHeight = this._register(new Emitter<number>());
209
readonly onDidChangeHeight = this._onDidChangeHeight.event;
210
211
private readonly _onDidChangeContentHeight = new Emitter<void>();
212
readonly onDidChangeContentHeight: Event<void> = this._onDidChangeContentHeight.event;
213
214
private _onDidChangeEmptyState = this._register(new Emitter<void>());
215
readonly onDidChangeEmptyState = this._onDidChangeEmptyState.event;
216
217
contribs: ReadonlyArray<IChatWidgetContrib> = [];
218
219
private listContainer!: HTMLElement;
220
private container!: HTMLElement;
221
222
get domNode() { return this.container; }
223
224
private tree!: WorkbenchObjectTree<ChatTreeItem, FuzzyScore>;
225
private renderer!: ChatListItemRenderer;
226
private readonly _codeBlockModelCollection: CodeBlockModelCollection;
227
private lastItem: ChatTreeItem | undefined;
228
229
private readonly visibilityTimeoutDisposable: MutableDisposable<IDisposable> = this._register(new MutableDisposable());
230
private readonly visibilityAnimationFrameDisposable: MutableDisposable<IDisposable> = this._register(new MutableDisposable());
231
private readonly scrollAnimationFrameDisposable: MutableDisposable<IDisposable> = this._register(new MutableDisposable());
232
233
private readonly inputPartDisposable: MutableDisposable<ChatInputPart> = this._register(new MutableDisposable());
234
private readonly inlineInputPartDisposable: MutableDisposable<ChatInputPart> = this._register(new MutableDisposable());
235
private inputContainer!: HTMLElement;
236
private focusedInputDOM!: HTMLElement;
237
private editorOptions!: ChatEditorOptions;
238
239
private recentlyRestoredCheckpoint: boolean = false;
240
241
private settingChangeCounter = 0;
242
243
private welcomeMessageContainer!: HTMLElement;
244
private readonly welcomePart: MutableDisposable<ChatViewWelcomePart> = this._register(new MutableDisposable());
245
246
private readonly chatSuggestNextWidget: ChatSuggestNextWidget;
247
248
private bodyDimension: dom.Dimension | undefined;
249
private visibleChangeCount = 0;
250
private requestInProgress: IContextKey<boolean>;
251
private agentInInput: IContextKey<boolean>;
252
private currentRequest: Promise<void> | undefined;
253
254
private _visible = false;
255
get visible() { return this._visible; }
256
257
private previousTreeScrollHeight: number = 0;
258
259
/**
260
* Whether the list is scroll-locked to the bottom. Initialize to true so that we can scroll to the bottom on first render.
261
* The initial render leads to a lot of `onDidChangeTreeContentHeight` as the renderer works out the real heights of rows.
262
*/
263
private scrollLock = true;
264
265
private _instructionFilesCheckPromise: Promise<boolean> | undefined;
266
private _instructionFilesExist: boolean | undefined;
267
268
private _isRenderingWelcome = false;
269
270
// Coding agent locking state
271
private _lockedAgent?: {
272
id: string;
273
name: string;
274
prefix: string;
275
displayName: string;
276
};
277
private readonly _lockedToCodingAgentContextKey: IContextKey<boolean>;
278
private readonly _agentSupportsAttachmentsContextKey: IContextKey<boolean>;
279
private readonly _sessionIsEmptyContextKey: IContextKey<boolean>;
280
private _attachmentCapabilities: IChatAgentAttachmentCapabilities = supportsAllAttachments;
281
282
// Cache for prompt file descriptions to avoid async calls during rendering
283
private readonly promptDescriptionsCache = new Map<string, string>();
284
private readonly promptUriCache = new Map<string, URI>();
285
private _isLoadingPromptDescriptions = false;
286
287
private _mostRecentlyFocusedItemIndex: number = -1;
288
289
private readonly viewModelDisposables = this._register(new DisposableStore());
290
private _viewModel: ChatViewModel | undefined;
291
292
private set viewModel(viewModel: ChatViewModel | undefined) {
293
if (this._viewModel === viewModel) {
294
return;
295
}
296
297
const previousSessionResource = this._viewModel?.sessionResource;
298
this.viewModelDisposables.clear();
299
300
this._viewModel = viewModel;
301
if (viewModel) {
302
this.viewModelDisposables.add(viewModel);
303
this.logService.debug('ChatWidget#setViewModel: have viewModel');
304
305
// If switching to a model with a request in progress, play progress sound
306
if (viewModel.model.requestInProgress.get()) {
307
this.chatAccessibilityService.acceptRequest(viewModel.sessionResource, true);
308
}
309
} else {
310
this.logService.debug('ChatWidget#setViewModel: no viewModel');
311
}
312
313
this.currentRequest = undefined;
314
this._onDidChangeViewModel.fire({ previousSessionResource, currentSessionResource: this._viewModel?.sessionResource });
315
}
316
317
get viewModel() {
318
return this._viewModel;
319
}
320
321
private readonly _editingSession = observableValue<IChatEditingSession | undefined>(this, undefined);
322
323
private parsedChatRequest: IParsedChatRequest | undefined;
324
get parsedInput() {
325
if (this.parsedChatRequest === undefined) {
326
if (!this.viewModel) {
327
return { text: '', parts: [] };
328
}
329
330
this.parsedChatRequest = this.instantiationService.createInstance(ChatRequestParser)
331
.parseChatRequest(this.viewModel.sessionResource, this.getInput(), this.location, {
332
selectedAgent: this._lastSelectedAgent,
333
mode: this.input.currentModeKind,
334
forcedAgent: this._lockedAgent?.id ? this.chatAgentService.getAgent(this._lockedAgent.id) : undefined
335
});
336
this._onDidChangeParsedInput.fire();
337
}
338
339
return this.parsedChatRequest;
340
}
341
342
get scopedContextKeyService(): IContextKeyService {
343
return this.contextKeyService;
344
}
345
346
private readonly _location: IChatWidgetLocationOptions;
347
get location() {
348
return this._location.location;
349
}
350
351
readonly viewContext: IChatWidgetViewContext;
352
353
get supportsChangingModes(): boolean {
354
return !!this.viewOptions.supportsChangingModes;
355
}
356
357
get locationData() {
358
return this._location.resolveData?.();
359
}
360
361
constructor(
362
location: ChatAgentLocation | IChatWidgetLocationOptions,
363
viewContext: IChatWidgetViewContext | undefined,
364
private readonly viewOptions: IChatWidgetViewOptions,
365
private readonly styles: IChatWidgetStyles,
366
@ICodeEditorService private readonly codeEditorService: ICodeEditorService,
367
@IEditorService private readonly editorService: IEditorService,
368
@IConfigurationService private readonly configurationService: IConfigurationService,
369
@IContextKeyService private readonly contextKeyService: IContextKeyService,
370
@IInstantiationService private readonly instantiationService: IInstantiationService,
371
@IChatService private readonly chatService: IChatService,
372
@IChatAgentService private readonly chatAgentService: IChatAgentService,
373
@IChatWidgetService private readonly chatWidgetService: IChatWidgetService,
374
@IContextMenuService private readonly contextMenuService: IContextMenuService,
375
@IChatAccessibilityService private readonly chatAccessibilityService: IChatAccessibilityService,
376
@ILogService private readonly logService: ILogService,
377
@IThemeService private readonly themeService: IThemeService,
378
@IChatSlashCommandService private readonly chatSlashCommandService: IChatSlashCommandService,
379
@IChatEditingService chatEditingService: IChatEditingService,
380
@ITelemetryService private readonly telemetryService: ITelemetryService,
381
@IPromptsService private readonly promptsService: IPromptsService,
382
@ILanguageModelToolsService private readonly toolsService: ILanguageModelToolsService,
383
@IChatModeService private readonly chatModeService: IChatModeService,
384
@IChatLayoutService private readonly chatLayoutService: IChatLayoutService,
385
@IChatEntitlementService private readonly chatEntitlementService: IChatEntitlementService,
386
@IChatSessionsService private readonly chatSessionsService: IChatSessionsService,
387
@IAgentSessionsService private readonly agentSessionsService: IAgentSessionsService,
388
@IChatTodoListService private readonly chatTodoListService: IChatTodoListService,
389
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
390
@ILifecycleService private readonly lifecycleService: ILifecycleService
391
) {
392
super();
393
394
this._lockedToCodingAgentContextKey = ChatContextKeys.lockedToCodingAgent.bindTo(this.contextKeyService);
395
this._agentSupportsAttachmentsContextKey = ChatContextKeys.agentSupportsAttachments.bindTo(this.contextKeyService);
396
this._sessionIsEmptyContextKey = ChatContextKeys.chatSessionIsEmpty.bindTo(this.contextKeyService);
397
398
this.viewContext = viewContext ?? {};
399
400
const viewModelObs = observableFromEvent(this, this.onDidChangeViewModel, () => this.viewModel);
401
402
if (typeof location === 'object') {
403
this._location = location;
404
} else {
405
this._location = { location };
406
}
407
408
ChatContextKeys.inChatSession.bindTo(contextKeyService).set(true);
409
ChatContextKeys.location.bindTo(contextKeyService).set(this._location.location);
410
ChatContextKeys.inQuickChat.bindTo(contextKeyService).set(isQuickChat(this));
411
this.agentInInput = ChatContextKeys.inputHasAgent.bindTo(contextKeyService);
412
this.requestInProgress = ChatContextKeys.requestInProgress.bindTo(contextKeyService);
413
414
this._register(this.chatEntitlementService.onDidChangeAnonymous(() => this.renderWelcomeViewContentIfNeeded()));
415
416
this._register(bindContextKey(decidedChatEditingResourceContextKey, contextKeyService, (reader) => {
417
const currentSession = this._editingSession.read(reader);
418
if (!currentSession) {
419
return;
420
}
421
const entries = currentSession.entries.read(reader);
422
const decidedEntries = entries.filter(entry => entry.state.read(reader) !== ModifiedFileEntryState.Modified);
423
return decidedEntries.map(entry => entry.entryId);
424
}));
425
this._register(bindContextKey(hasUndecidedChatEditingResourceContextKey, contextKeyService, (reader) => {
426
const currentSession = this._editingSession.read(reader);
427
const entries = currentSession?.entries.read(reader) ?? []; // using currentSession here
428
const decidedEntries = entries.filter(entry => entry.state.read(reader) === ModifiedFileEntryState.Modified);
429
return decidedEntries.length > 0;
430
}));
431
this._register(bindContextKey(hasAppliedChatEditsContextKey, contextKeyService, (reader) => {
432
const currentSession = this._editingSession.read(reader);
433
if (!currentSession) {
434
return false;
435
}
436
const entries = currentSession.entries.read(reader);
437
return entries.length > 0;
438
}));
439
this._register(bindContextKey(inChatEditingSessionContextKey, contextKeyService, (reader) => {
440
return this._editingSession.read(reader) !== null;
441
}));
442
this._register(bindContextKey(ChatContextKeys.chatEditingCanUndo, contextKeyService, (r) => {
443
return this._editingSession.read(r)?.canUndo.read(r) || false;
444
}));
445
this._register(bindContextKey(ChatContextKeys.chatEditingCanRedo, contextKeyService, (r) => {
446
return this._editingSession.read(r)?.canRedo.read(r) || false;
447
}));
448
this._register(bindContextKey(applyingChatEditsFailedContextKey, contextKeyService, (r) => {
449
const chatModel = viewModelObs.read(r)?.model;
450
const editingSession = this._editingSession.read(r);
451
if (!editingSession || !chatModel) {
452
return false;
453
}
454
const lastResponse = observableFromEvent(this, chatModel.onDidChange, () => chatModel.getRequests().at(-1)?.response).read(r);
455
return lastResponse?.result?.errorDetails && !lastResponse?.result?.errorDetails.responseIsIncomplete;
456
}));
457
458
this._codeBlockModelCollection = this._register(instantiationService.createInstance(CodeBlockModelCollection, undefined));
459
this.chatSuggestNextWidget = this._register(this.instantiationService.createInstance(ChatSuggestNextWidget));
460
461
this._register(this.configurationService.onDidChangeConfiguration((e) => {
462
if (e.affectsConfiguration('chat.renderRelatedFiles')) {
463
this.input.renderChatRelatedFiles();
464
}
465
466
if (e.affectsConfiguration(ChatConfiguration.EditRequests) || e.affectsConfiguration(ChatConfiguration.CheckpointsEnabled)) {
467
this.settingChangeCounter++;
468
this.onDidChangeItems();
469
}
470
}));
471
472
this._register(autorun(r => {
473
const viewModel = viewModelObs.read(r);
474
const sessions = chatEditingService.editingSessionsObs.read(r);
475
476
const session = sessions.find(candidate => isEqual(candidate.chatSessionResource, viewModel?.sessionResource));
477
this._editingSession.set(undefined, undefined);
478
this.renderChatEditingSessionState(); // this is necessary to make sure we dispose previous buttons, etc.
479
480
if (!session) {
481
// none or for a different chat widget
482
return;
483
}
484
485
const entries = session.entries.read(r);
486
for (const entry of entries) {
487
entry.state.read(r); // SIGNAL
488
}
489
490
this._editingSession.set(session, undefined);
491
492
r.store.add(session.onDidDispose(() => {
493
this._editingSession.set(undefined, undefined);
494
this.renderChatEditingSessionState();
495
}));
496
r.store.add(this.inputEditor.onDidChangeModelContent(() => {
497
if (this.getInput() === '') {
498
this.refreshParsedInput();
499
}
500
}));
501
this.renderChatEditingSessionState();
502
}));
503
504
this._register(codeEditorService.registerCodeEditorOpenHandler(async (input: ITextResourceEditorInput, _source: ICodeEditor | null, _sideBySide?: boolean): Promise<ICodeEditor | null> => {
505
const resource = input.resource;
506
if (resource.scheme !== Schemas.vscodeChatCodeBlock) {
507
return null;
508
}
509
510
const responseId = resource.path.split('/').at(1);
511
if (!responseId) {
512
return null;
513
}
514
515
const item = this.viewModel?.getItems().find(item => item.id === responseId);
516
if (!item) {
517
return null;
518
}
519
520
// TODO: needs to reveal the chat view
521
522
this.reveal(item);
523
524
await timeout(0); // wait for list to actually render
525
526
for (const codeBlockPart of this.renderer.editorsInUse()) {
527
if (extUri.isEqual(codeBlockPart.uri, resource, true)) {
528
const editor = codeBlockPart.editor;
529
530
let relativeTop = 0;
531
const editorDomNode = editor.getDomNode();
532
if (editorDomNode) {
533
const row = dom.findParentWithClass(editorDomNode, 'monaco-list-row');
534
if (row) {
535
relativeTop = dom.getTopLeftOffset(editorDomNode).top - dom.getTopLeftOffset(row).top;
536
}
537
}
538
539
if (input.options?.selection) {
540
const editorSelectionTopOffset = editor.getTopForPosition(input.options.selection.startLineNumber, input.options.selection.startColumn);
541
relativeTop += editorSelectionTopOffset;
542
543
editor.focus();
544
editor.setSelection({
545
startLineNumber: input.options.selection.startLineNumber,
546
startColumn: input.options.selection.startColumn,
547
endLineNumber: input.options.selection.endLineNumber ?? input.options.selection.startLineNumber,
548
endColumn: input.options.selection.endColumn ?? input.options.selection.startColumn
549
});
550
}
551
552
this.reveal(item, relativeTop);
553
554
return editor;
555
}
556
}
557
return null;
558
}));
559
560
this._register(this.onDidChangeParsedInput(() => this.updateChatInputContext()));
561
562
this._register(this.chatTodoListService.onDidUpdateTodos((sessionResource) => {
563
if (isEqual(this.viewModel?.sessionResource, sessionResource)) {
564
this.inputPart.renderChatTodoListWidget(sessionResource);
565
}
566
}));
567
}
568
569
private _lastSelectedAgent: IChatAgentData | undefined;
570
set lastSelectedAgent(agent: IChatAgentData | undefined) {
571
this.parsedChatRequest = undefined;
572
this._lastSelectedAgent = agent;
573
this._updateAgentCapabilitiesContextKeys(agent);
574
this._onDidChangeParsedInput.fire();
575
}
576
577
get lastSelectedAgent(): IChatAgentData | undefined {
578
return this._lastSelectedAgent;
579
}
580
581
private _updateAgentCapabilitiesContextKeys(agent: IChatAgentData | undefined): void {
582
// Check if the agent has capabilities defined directly
583
const capabilities = agent?.capabilities ?? (this._lockedAgent ? this.chatSessionsService.getCapabilitiesForSessionType(this._lockedAgent.id) : undefined);
584
this._attachmentCapabilities = capabilities ?? supportsAllAttachments;
585
586
const supportsAttachments = Object.keys(filter(this._attachmentCapabilities, (key, value) => value === true)).length > 0;
587
this._agentSupportsAttachmentsContextKey.set(supportsAttachments);
588
}
589
590
get supportsFileReferences(): boolean {
591
return !!this.viewOptions.supportsFileReferences;
592
}
593
594
get attachmentCapabilities(): IChatAgentAttachmentCapabilities {
595
return this._attachmentCapabilities;
596
}
597
598
get input(): ChatInputPart {
599
return this.viewModel?.editing && this.configurationService.getValue<string>('chat.editRequests') !== 'input' ? this.inlineInputPart : this.inputPart;
600
}
601
602
private get inputPart(): ChatInputPart {
603
return this.inputPartDisposable.value!;
604
}
605
606
private get inlineInputPart(): ChatInputPart {
607
return this.inlineInputPartDisposable.value!;
608
}
609
610
get inputEditor(): ICodeEditor {
611
return this.input.inputEditor;
612
}
613
614
get contentHeight(): number {
615
return this.input.contentHeight + this.tree.contentHeight + this.chatSuggestNextWidget.height;
616
}
617
618
get attachmentModel(): ChatAttachmentModel {
619
return this.input.attachmentModel;
620
}
621
622
render(parent: HTMLElement): void {
623
const viewId = isIChatViewViewContext(this.viewContext) ? this.viewContext.viewId : undefined;
624
this.editorOptions = this._register(this.instantiationService.createInstance(ChatEditorOptions, viewId, this.styles.listForeground, this.styles.inputEditorBackground, this.styles.resultEditorBackground));
625
const renderInputOnTop = this.viewOptions.renderInputOnTop ?? false;
626
const renderFollowups = this.viewOptions.renderFollowups ?? !renderInputOnTop;
627
const renderStyle = this.viewOptions.renderStyle;
628
const renderInputToolbarBelowInput = this.viewOptions.renderInputToolbarBelowInput ?? false;
629
630
this.container = dom.append(parent, $('.interactive-session'));
631
this.welcomeMessageContainer = dom.append(this.container, $('.chat-welcome-view-container', { style: 'display: none' }));
632
this._register(dom.addStandardDisposableListener(this.welcomeMessageContainer, dom.EventType.CLICK, () => this.focusInput()));
633
634
this._register(this.chatSuggestNextWidget.onDidChangeHeight(() => {
635
if (this.bodyDimension) {
636
this.layout(this.bodyDimension.height, this.bodyDimension.width);
637
}
638
}));
639
this._register(this.chatSuggestNextWidget.onDidSelectPrompt(({ handoff, agentId }) => {
640
this.handleNextPromptSelection(handoff, agentId);
641
}));
642
643
if (renderInputOnTop) {
644
this.createInput(this.container, { renderFollowups, renderStyle, renderInputToolbarBelowInput });
645
this.listContainer = dom.append(this.container, $(`.interactive-list`));
646
} else {
647
this.listContainer = dom.append(this.container, $(`.interactive-list`));
648
dom.append(this.container, this.chatSuggestNextWidget.domNode);
649
this.createInput(this.container, { renderFollowups, renderStyle, renderInputToolbarBelowInput });
650
}
651
652
this.renderWelcomeViewContentIfNeeded();
653
this.createList(this.listContainer, { editable: !isInlineChat(this) && !isQuickChat(this), ...this.viewOptions.rendererOptions, renderStyle });
654
655
const scrollDownButton = this._register(new Button(this.listContainer, {
656
supportIcons: true,
657
buttonBackground: asCssVariable(buttonSecondaryBackground),
658
buttonForeground: asCssVariable(buttonSecondaryForeground),
659
buttonHoverBackground: asCssVariable(buttonSecondaryHoverBackground),
660
}));
661
scrollDownButton.element.classList.add('chat-scroll-down');
662
scrollDownButton.label = `$(${Codicon.chevronDown.id})`;
663
scrollDownButton.setTitle(localize('scrollDownButtonLabel', "Scroll down"));
664
this._register(scrollDownButton.onDidClick(() => {
665
this.scrollLock = true;
666
this.scrollToEnd();
667
}));
668
669
// Update the font family and size
670
this._register(autorun(reader => {
671
const fontFamily = this.chatLayoutService.fontFamily.read(reader);
672
const fontSize = this.chatLayoutService.fontSize.read(reader);
673
674
this.container.style.setProperty('--vscode-chat-font-family', fontFamily);
675
this.container.style.fontSize = `${fontSize}px`;
676
677
if (this.visible) {
678
this.tree.rerender();
679
}
680
}));
681
682
this._register(Event.runAndSubscribe(this.editorOptions.onDidChange, () => this.onDidStyleChange()));
683
684
// Do initial render
685
if (this.viewModel) {
686
this.onDidChangeItems();
687
this.scrollToEnd();
688
}
689
690
this.contribs = ChatWidget.CONTRIBS.map(contrib => {
691
try {
692
return this._register(this.instantiationService.createInstance(contrib, this));
693
} catch (err) {
694
this.logService.error('Failed to instantiate chat widget contrib', toErrorMessage(err));
695
return undefined;
696
}
697
}).filter(isDefined);
698
699
this._register(this.chatWidgetService.register(this));
700
701
const parsedInput = observableFromEvent(this.onDidChangeParsedInput, () => this.parsedInput);
702
this._register(autorun(r => {
703
const input = parsedInput.read(r);
704
705
const newPromptAttachments = new Map<string, IChatRequestVariableEntry>();
706
const oldPromptAttachments = new Set<string>();
707
708
// get all attachments, know those that are prompt-referenced
709
for (const attachment of this.attachmentModel.attachments) {
710
if (attachment.range) {
711
oldPromptAttachments.add(attachment.id);
712
}
713
}
714
715
// update/insert prompt-referenced attachments
716
for (const part of input.parts) {
717
if (part instanceof ChatRequestToolPart || part instanceof ChatRequestToolSetPart || part instanceof ChatRequestDynamicVariablePart) {
718
const entry = part.toVariableEntry();
719
newPromptAttachments.set(entry.id, entry);
720
oldPromptAttachments.delete(entry.id);
721
}
722
}
723
724
this.attachmentModel.updateContext(oldPromptAttachments, newPromptAttachments.values());
725
}));
726
727
if (!this.focusedInputDOM) {
728
this.focusedInputDOM = this.container.appendChild(dom.$('.focused-input-dom'));
729
}
730
}
731
732
private scrollToEnd() {
733
if (this.lastItem) {
734
const offset = Math.max(this.lastItem.currentRenderedHeight ?? 0, 1e6);
735
if (this.tree.hasElement(this.lastItem)) {
736
this.tree.reveal(this.lastItem, offset);
737
}
738
}
739
}
740
741
focusInput(): void {
742
this.input.focus();
743
744
// Sometimes focusing the input part is not possible,
745
// but we'd like to be the last focused chat widget,
746
// so we emit an optimistic onDidFocus event nonetheless.
747
this._onDidFocus.fire();
748
}
749
750
hasInputFocus(): boolean {
751
return this.input.hasFocus();
752
}
753
754
refreshParsedInput() {
755
if (!this.viewModel) {
756
return;
757
}
758
this.parsedChatRequest = this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(this.viewModel.sessionResource, this.getInput(), this.location, { selectedAgent: this._lastSelectedAgent, mode: this.input.currentModeKind });
759
this._onDidChangeParsedInput.fire();
760
}
761
762
getSibling(item: ChatTreeItem, type: 'next' | 'previous'): ChatTreeItem | undefined {
763
if (!isResponseVM(item)) {
764
return;
765
}
766
const items = this.viewModel?.getItems();
767
if (!items) {
768
return;
769
}
770
const responseItems = items.filter(i => isResponseVM(i));
771
const targetIndex = responseItems.indexOf(item);
772
if (targetIndex === undefined) {
773
return;
774
}
775
const indexToFocus = type === 'next' ? targetIndex + 1 : targetIndex - 1;
776
if (indexToFocus < 0 || indexToFocus > responseItems.length - 1) {
777
return;
778
}
779
return responseItems[indexToFocus];
780
}
781
782
async clear(): Promise<void> {
783
this.logService.debug('ChatWidget#clear');
784
if (this._dynamicMessageLayoutData) {
785
this._dynamicMessageLayoutData.enabled = true;
786
}
787
788
if (this.viewModel?.editing) {
789
this.finishedEditing();
790
}
791
792
if (this.viewModel) {
793
this.viewModel.resetInputPlaceholder();
794
}
795
if (this._lockedAgent) {
796
this.lockToCodingAgent(this._lockedAgent.name, this._lockedAgent.displayName, this._lockedAgent.id);
797
} else {
798
this.unlockFromCodingAgent();
799
}
800
801
this.inputPart.clearTodoListWidget(this.viewModel?.sessionResource, true);
802
this.chatSuggestNextWidget.hide();
803
await this.viewOptions.clear?.();
804
}
805
806
private onDidChangeItems(skipDynamicLayout?: boolean) {
807
if (this._visible || !this.viewModel) {
808
const treeItems = (this.viewModel?.getItems() ?? [])
809
.map((item): ITreeElement<ChatTreeItem> => {
810
return {
811
element: item,
812
collapsed: false,
813
collapsible: false
814
};
815
});
816
817
818
if (treeItems.length > 0) {
819
this.updateChatViewVisibility();
820
} else {
821
this.renderWelcomeViewContentIfNeeded();
822
}
823
824
this._onWillMaybeChangeHeight.fire();
825
826
this.lastItem = treeItems.at(-1)?.element;
827
ChatContextKeys.lastItemId.bindTo(this.contextKeyService).set(this.lastItem ? [this.lastItem.id] : []);
828
this.tree.setChildren(null, treeItems, {
829
diffIdentityProvider: {
830
getId: (element) => {
831
return element.dataId +
832
// Ensure re-rendering an element once slash commands are loaded, so the colorization can be applied.
833
`${(isRequestVM(element)) /* && !!this.lastSlashCommands ? '_scLoaded' : '' */}` +
834
// If a response is in the process of progressive rendering, we need to ensure that it will
835
// be re-rendered so progressive rendering is restarted, even if the model wasn't updated.
836
`${isResponseVM(element) && element.renderData ? `_${this.visibleChangeCount}` : ''}` +
837
// Re-render once content references are loaded
838
(isResponseVM(element) ? `_${element.contentReferences.length}` : '') +
839
// Re-render if element becomes hidden due to undo/redo
840
`_${element.shouldBeRemovedOnSend ? `${element.shouldBeRemovedOnSend.afterUndoStop || '1'}` : '0'}` +
841
// Re-render if we have an element currently being edited
842
`_${this.viewModel?.editing ? '1' : '0'}` +
843
// Re-render if we have an element currently being checkpointed
844
`_${this.viewModel?.model.checkpoint ? '1' : '0'}` +
845
// Re-render all if invoked by setting change
846
`_setting${this.settingChangeCounter || '0'}` +
847
// Rerender request if we got new content references in the response
848
// since this may change how we render the corresponding attachments in the request
849
(isRequestVM(element) && element.contentReferences ? `_${element.contentReferences?.length}` : '');
850
},
851
}
852
});
853
854
if (!skipDynamicLayout && this._dynamicMessageLayoutData) {
855
this.layoutDynamicChatTreeItemMode();
856
}
857
858
this.renderFollowups();
859
}
860
}
861
862
/**
863
* Updates the DOM visibility of welcome view and chat list immediately
864
*/
865
private updateChatViewVisibility(): void {
866
if (!this.viewModel) {
867
return;
868
}
869
870
const numItems = this.viewModel.getItems().length;
871
dom.setVisibility(numItems === 0, this.welcomeMessageContainer);
872
dom.setVisibility(numItems !== 0, this.listContainer);
873
874
this._onDidChangeEmptyState.fire();
875
}
876
877
isEmpty(): boolean {
878
return (this.viewModel?.getItems().length ?? 0) === 0;
879
}
880
881
/**
882
* Renders the welcome view content when needed.
883
*/
884
private renderWelcomeViewContentIfNeeded() {
885
if (this._isRenderingWelcome) {
886
return;
887
}
888
889
this._isRenderingWelcome = true;
890
try {
891
if (this.viewOptions.renderStyle === 'compact' || this.viewOptions.renderStyle === 'minimal' || this.lifecycleService.willShutdown) {
892
return;
893
}
894
895
const numItems = this.viewModel?.getItems().length ?? 0;
896
if (!numItems) {
897
const defaultAgent = this.chatAgentService.getDefaultAgent(this.location, this.input.currentModeKind);
898
let additionalMessage: string | IMarkdownString | undefined;
899
if (this.chatEntitlementService.anonymous && !this.chatEntitlementService.sentiment.installed) {
900
const providers = product.defaultChatAgent.provider;
901
additionalMessage = new MarkdownString(localize({ key: 'settings', comment: ['{Locked="]({2})"}', '{Locked="]({3})"}'] }, "By continuing with {0} Copilot, you agree to {1}'s [Terms]({2}) and [Privacy Statement]({3}).", providers.default.name, providers.default.name, product.defaultChatAgent.termsStatementUrl, product.defaultChatAgent.privacyStatementUrl), { isTrusted: true });
902
} else {
903
additionalMessage = defaultAgent?.metadata.additionalWelcomeMessage;
904
}
905
if (!additionalMessage && !this._lockedAgent) {
906
additionalMessage = this._getGenerateInstructionsMessage();
907
}
908
const welcomeContent = this.getWelcomeViewContent(additionalMessage);
909
if (!this.welcomePart.value || this.welcomePart.value.needsRerender(welcomeContent)) {
910
dom.clearNode(this.welcomeMessageContainer);
911
912
this.welcomePart.value = this.instantiationService.createInstance(
913
ChatViewWelcomePart,
914
welcomeContent,
915
{
916
location: this.location,
917
isWidgetAgentWelcomeViewContent: this.input?.currentModeKind === ChatModeKind.Agent
918
}
919
);
920
dom.append(this.welcomeMessageContainer, this.welcomePart.value.element);
921
}
922
}
923
924
this.updateChatViewVisibility();
925
} finally {
926
this._isRenderingWelcome = false;
927
}
928
}
929
930
private _getGenerateInstructionsMessage(): IMarkdownString {
931
// Start checking for instruction files immediately if not already done
932
if (!this._instructionFilesCheckPromise) {
933
this._instructionFilesCheckPromise = this._checkForAgentInstructionFiles();
934
// Use VS Code's idiomatic pattern for disposal-safe promise callbacks
935
this._register(thenIfNotDisposed(this._instructionFilesCheckPromise, hasFiles => {
936
this._instructionFilesExist = hasFiles;
937
// Only re-render if the current view still doesn't have items and we're showing the welcome message
938
const hasViewModelItems = this.viewModel?.getItems().length ?? 0;
939
if (hasViewModelItems === 0) {
940
this.renderWelcomeViewContentIfNeeded();
941
}
942
}));
943
}
944
945
// If we already know the result, use it
946
if (this._instructionFilesExist === true) {
947
// Don't show generate instructions message if files exist
948
return new MarkdownString('');
949
} else if (this._instructionFilesExist === false) {
950
// Show generate instructions message if no files exist
951
const generateInstructionsCommand = 'workbench.action.chat.generateInstructions';
952
return new MarkdownString(localize(
953
'chatWidget.instructions',
954
"[Generate Agent Instructions]({0}) to onboard AI onto your codebase.",
955
`command:${generateInstructionsCommand}`
956
), { isTrusted: { enabledCommands: [generateInstructionsCommand] } });
957
}
958
959
// While checking, don't show the generate instructions message
960
return new MarkdownString('');
961
}
962
963
/**
964
* Checks if any agent instruction files (.github/copilot-instructions.md or AGENTS.md) exist in the workspace.
965
* Used to determine whether to show the "Generate Agent Instructions" hint.
966
*
967
* @returns true if instruction files exist OR if instruction features are disabled (to hide the hint)
968
*/
969
private async _checkForAgentInstructionFiles(): Promise<boolean> {
970
try {
971
const useCopilotInstructionsFiles = this.configurationService.getValue(PromptsConfig.USE_COPILOT_INSTRUCTION_FILES);
972
const useAgentMd = this.configurationService.getValue(PromptsConfig.USE_AGENT_MD);
973
if (!useCopilotInstructionsFiles && !useAgentMd) {
974
// If both settings are disabled, return true to hide the hint (since the features aren't enabled)
975
return true;
976
}
977
return (
978
(await this.promptsService.listCopilotInstructionsMDs(CancellationToken.None)).length > 0 ||
979
// Note: only checking for AGENTS.md files at the root folder, not ones in subfolders.
980
(await this.promptsService.listAgentMDs(CancellationToken.None, false)).length > 0
981
);
982
} catch (error) {
983
// On error, assume no instruction files exist to be safe
984
this.logService.warn('[ChatWidget] Error checking for instruction files:', error);
985
return false;
986
}
987
}
988
989
private getWelcomeViewContent(additionalMessage: string | IMarkdownString | undefined): IChatViewWelcomeContent {
990
if (this.isLockedToCodingAgent) {
991
// Check for provider-specific customizations from chat sessions service
992
const providerIcon = this._lockedAgent ? this.chatSessionsService.getIconForSessionType(this._lockedAgent.id) : undefined;
993
const providerTitle = this._lockedAgent ? this.chatSessionsService.getWelcomeTitleForSessionType(this._lockedAgent.id) : undefined;
994
const providerMessage = this._lockedAgent ? this.chatSessionsService.getWelcomeMessageForSessionType(this._lockedAgent.id) : undefined;
995
996
// Fallback to default messages if provider doesn't specify
997
const message = providerMessage
998
? new MarkdownString(providerMessage)
999
: (this._lockedAgent?.prefix === '@copilot '
1000
? new MarkdownString(localize('copilotCodingAgentMessage', "This chat session will be forwarded to the {0} [coding agent]({1}) where work is completed in the background. ", this._lockedAgent.prefix, 'https://aka.ms/coding-agent-docs') + DISCLAIMER, { isTrusted: true })
1001
: new MarkdownString(localize('genericCodingAgentMessage', "This chat session will be forwarded to the {0} coding agent where work is completed in the background. ", this._lockedAgent?.prefix) + DISCLAIMER));
1002
1003
return {
1004
title: providerTitle ?? localize('codingAgentTitle', "Delegate to {0}", this._lockedAgent?.prefix),
1005
message,
1006
icon: providerIcon ?? Codicon.sendToRemoteAgent,
1007
additionalMessage,
1008
useLargeIcon: !!providerIcon,
1009
};
1010
}
1011
1012
let title: string;
1013
if (this.input.currentModeKind === ChatModeKind.Ask) {
1014
title = localize('chatDescription', "Ask about your code");
1015
} else if (this.input.currentModeKind === ChatModeKind.Edit) {
1016
title = localize('editsTitle', "Edit in context");
1017
} else {
1018
title = localize('agentTitle', "Build with Agent");
1019
}
1020
1021
return {
1022
title,
1023
message: new MarkdownString(DISCLAIMER),
1024
icon: Codicon.chatSparkle,
1025
additionalMessage,
1026
suggestedPrompts: this.getPromptFileSuggestions()
1027
};
1028
}
1029
1030
private getPromptFileSuggestions(): IChatSuggestedPrompts[] {
1031
1032
// Use predefined suggestions for new users
1033
if (!this.chatEntitlementService.sentiment.installed) {
1034
const isEmpty = this.contextService.getWorkbenchState() === WorkbenchState.EMPTY;
1035
if (isEmpty) {
1036
return [
1037
{
1038
icon: Codicon.vscode,
1039
label: localize('chatWidget.suggestedPrompts.gettingStarted', "Ask @vscode"),
1040
prompt: localize('chatWidget.suggestedPrompts.gettingStartedPrompt', "@vscode How do I change the theme to light mode?"),
1041
},
1042
{
1043
icon: Codicon.newFolder,
1044
label: localize('chatWidget.suggestedPrompts.newProject', "Create Project"),
1045
prompt: localize('chatWidget.suggestedPrompts.newProjectPrompt', "Create a #new Hello World project in TypeScript"),
1046
}
1047
];
1048
} else {
1049
return [
1050
{
1051
icon: Codicon.debugAlt,
1052
label: localize('chatWidget.suggestedPrompts.buildWorkspace', "Build Workspace"),
1053
prompt: localize('chatWidget.suggestedPrompts.buildWorkspacePrompt', "How do I build this workspace?"),
1054
},
1055
{
1056
icon: Codicon.gear,
1057
label: localize('chatWidget.suggestedPrompts.findConfig', "Show Config"),
1058
prompt: localize('chatWidget.suggestedPrompts.findConfigPrompt', "Where is the configuration for this project defined?"),
1059
}
1060
];
1061
}
1062
}
1063
1064
// Get the current workspace folder context if available
1065
const activeEditor = this.editorService.activeEditor;
1066
const resource = activeEditor ? EditorResourceAccessor.getOriginalUri(activeEditor) : undefined;
1067
1068
// Get the prompt file suggestions configuration
1069
const suggestions = PromptsConfig.getPromptFilesRecommendationsValue(this.configurationService, resource);
1070
if (!suggestions) {
1071
return [];
1072
}
1073
1074
const result: IChatSuggestedPrompts[] = [];
1075
const promptsToLoad: string[] = [];
1076
1077
// First, collect all prompts that need loading (regardless of shouldInclude)
1078
for (const [promptName] of Object.entries(suggestions)) {
1079
const description = this.promptDescriptionsCache.get(promptName);
1080
if (description === undefined) {
1081
promptsToLoad.push(promptName);
1082
}
1083
}
1084
1085
// If we have prompts to load, load them asynchronously and don't return anything yet
1086
// But only if we're not already loading to prevent infinite loop
1087
if (promptsToLoad.length > 0 && !this._isLoadingPromptDescriptions) {
1088
this.loadPromptDescriptions(promptsToLoad);
1089
return [];
1090
}
1091
1092
// Now process the suggestions with loaded descriptions
1093
const promptsWithScores: { promptName: string; condition: boolean | string; score: number }[] = [];
1094
1095
for (const [promptName, condition] of Object.entries(suggestions)) {
1096
let score = 0;
1097
1098
// Handle boolean conditions
1099
if (typeof condition === 'boolean') {
1100
score = condition ? 1 : 0;
1101
}
1102
// Handle when clause conditions
1103
else if (typeof condition === 'string') {
1104
try {
1105
const whenClause = ContextKeyExpr.deserialize(condition);
1106
if (whenClause) {
1107
// Test against all open code editors
1108
const allEditors = this.codeEditorService.listCodeEditors();
1109
1110
if (allEditors.length > 0) {
1111
// Count how many editors match the when clause
1112
score = allEditors.reduce((count, editor) => {
1113
try {
1114
const editorContext = this.contextKeyService.getContext(editor.getDomNode());
1115
return count + (whenClause.evaluate(editorContext) ? 1 : 0);
1116
} catch (error) {
1117
// Log error for this specific editor but continue with others
1118
this.logService.warn('Failed to evaluate when clause for editor:', error);
1119
return count;
1120
}
1121
}, 0);
1122
} else {
1123
// Fallback to global context if no editors are open
1124
score = this.contextKeyService.contextMatchesRules(whenClause) ? 1 : 0;
1125
}
1126
} else {
1127
score = 0;
1128
}
1129
} catch (error) {
1130
// Log the error but don't fail completely
1131
this.logService.warn('Failed to parse when clause for prompt file suggestion:', condition, error);
1132
score = 0;
1133
}
1134
}
1135
1136
if (score > 0) {
1137
promptsWithScores.push({ promptName, condition, score });
1138
}
1139
}
1140
1141
// Sort by score (descending) and take top 5
1142
promptsWithScores.sort((a, b) => b.score - a.score);
1143
const topPrompts = promptsWithScores.slice(0, 5);
1144
1145
// Build the final result array
1146
for (const { promptName } of topPrompts) {
1147
const description = this.promptDescriptionsCache.get(promptName);
1148
const commandLabel = localize('chatWidget.promptFile.commandLabel', "{0}", promptName);
1149
const uri = this.promptUriCache.get(promptName);
1150
const descriptionText = description?.trim() ? description : undefined;
1151
result.push({
1152
icon: Codicon.run,
1153
label: commandLabel,
1154
description: descriptionText,
1155
prompt: `/${promptName} `,
1156
uri: uri
1157
});
1158
}
1159
1160
return result;
1161
}
1162
1163
private async loadPromptDescriptions(promptNames: string[]): Promise<void> {
1164
// Don't start loading if the widget is being disposed
1165
if (this._store.isDisposed) {
1166
return;
1167
}
1168
1169
// Set loading guard to prevent infinite loop
1170
this._isLoadingPromptDescriptions = true;
1171
try {
1172
// Get all available prompt files with their metadata
1173
const promptCommands = await this.promptsService.getPromptSlashCommands(CancellationToken.None);
1174
1175
let cacheUpdated = false;
1176
// Load descriptions only for the specified prompts
1177
for (const promptCommand of promptCommands) {
1178
if (promptNames.includes(promptCommand.name)) {
1179
const description = promptCommand.description;
1180
if (description) {
1181
this.promptDescriptionsCache.set(promptCommand.name, description);
1182
cacheUpdated = true;
1183
} else {
1184
// Set empty string to indicate we've checked this prompt
1185
this.promptDescriptionsCache.set(promptCommand.name, '');
1186
cacheUpdated = true;
1187
}
1188
}
1189
}
1190
1191
// Fire event to trigger a re-render of the welcome view only if cache was updated
1192
if (cacheUpdated) {
1193
this.renderWelcomeViewContentIfNeeded();
1194
}
1195
} catch (error) {
1196
this.logService.warn('Failed to load specific prompt descriptions:', error);
1197
} finally {
1198
// Always clear the loading guard, even on error
1199
this._isLoadingPromptDescriptions = false;
1200
}
1201
}
1202
1203
private async renderChatEditingSessionState() {
1204
if (!this.input) {
1205
return;
1206
}
1207
this.input.renderChatEditingSessionState(this._editingSession.get() ?? null);
1208
}
1209
1210
private async renderFollowups(): Promise<void> {
1211
if (this.lastItem && isResponseVM(this.lastItem) && this.lastItem.isComplete) {
1212
this.input.renderFollowups(this.lastItem.replyFollowups, this.lastItem);
1213
} else {
1214
this.input.renderFollowups(undefined, undefined);
1215
}
1216
1217
if (this.bodyDimension) {
1218
this.layout(this.bodyDimension.height, this.bodyDimension.width);
1219
}
1220
}
1221
1222
private renderChatSuggestNextWidget(): void {
1223
if (this.lifecycleService.willShutdown) {
1224
return;
1225
}
1226
1227
// Skip rendering in coding agent sessions
1228
if (this.isLockedToCodingAgent) {
1229
this.chatSuggestNextWidget.hide();
1230
return;
1231
}
1232
1233
const items = this.viewModel?.getItems() ?? [];
1234
if (!items.length) {
1235
return;
1236
}
1237
1238
const lastItem = items[items.length - 1];
1239
const lastResponseComplete = lastItem && isResponseVM(lastItem) && lastItem.isComplete;
1240
if (!lastResponseComplete) {
1241
return;
1242
}
1243
// Get the currently selected mode directly from the observable
1244
// Note: We use currentModeObs instead of currentModeKind because currentModeKind returns
1245
// the ChatModeKind enum (e.g., 'agent'), which doesn't distinguish between custom modes.
1246
// Custom modes all have kind='agent' but different IDs.
1247
const currentMode = this.input.currentModeObs.get();
1248
const handoffs = currentMode?.handOffs?.get();
1249
1250
// Only show if: mode has handoffs AND chat has content AND not quick chat
1251
const shouldShow = currentMode && handoffs && handoffs.length > 0;
1252
1253
if (shouldShow) {
1254
// Log telemetry only when widget transitions from hidden to visible
1255
const wasHidden = this.chatSuggestNextWidget.domNode.style.display === 'none';
1256
this.chatSuggestNextWidget.render(currentMode);
1257
1258
if (wasHidden) {
1259
this.telemetryService.publicLog2<ChatHandoffWidgetShownEvent, ChatHandoffWidgetShownClassification>('chat.handoffWidgetShown', {
1260
agent: currentMode.id,
1261
handoffCount: handoffs.length
1262
});
1263
}
1264
} else {
1265
this.chatSuggestNextWidget.hide();
1266
}
1267
1268
// Trigger layout update
1269
if (this.bodyDimension) {
1270
this.layout(this.bodyDimension.height, this.bodyDimension.width);
1271
}
1272
}
1273
1274
private handleNextPromptSelection(handoff: IHandOff, agentId?: string): void {
1275
// Hide the widget after selection
1276
this.chatSuggestNextWidget.hide();
1277
1278
const promptToUse = handoff.prompt;
1279
1280
// Log telemetry
1281
const currentMode = this.input.currentModeObs.get();
1282
const fromAgent = currentMode?.id ?? '';
1283
this.telemetryService.publicLog2<ChatHandoffClickEvent, ChatHandoffClickClassification>('chat.handoffClicked', {
1284
fromAgent: fromAgent,
1285
toAgent: agentId || handoff.agent || '',
1286
hasPrompt: Boolean(promptToUse),
1287
autoSend: Boolean(handoff.send)
1288
});
1289
1290
// If agentId is provided (from chevron dropdown), delegate to that chat session
1291
// Otherwise, switch to the handoff agent
1292
if (agentId) {
1293
// Delegate to chat session (e.g., @background or @cloud)
1294
this.input.setValue(`@${agentId} ${promptToUse}`, false);
1295
this.input.focus();
1296
// Auto-submit for delegated chat sessions
1297
this.acceptInput().catch(e => this.logService.error('Failed to handle handoff continueOn', e));
1298
} else if (handoff.agent) {
1299
// Regular handoff to specified agent
1300
this._switchToAgentByName(handoff.agent);
1301
// Insert the handoff prompt into the input
1302
this.input.setValue(promptToUse, false);
1303
this.input.focus();
1304
1305
// Auto-submit if send flag is true
1306
if (handoff.send) {
1307
this.acceptInput();
1308
}
1309
}
1310
}
1311
1312
async handleDelegationExitIfNeeded(sourceAgent: Pick<IChatAgentData, 'id' | 'name'> | undefined, targetAgent: IChatAgentData | undefined): Promise<void> {
1313
if (!this._shouldExitAfterDelegation(sourceAgent, targetAgent)) {
1314
return;
1315
}
1316
1317
try {
1318
await this._handleDelegationExit();
1319
} catch (e) {
1320
this.logService.error('Failed to handle delegation exit', e);
1321
}
1322
}
1323
1324
private _shouldExitAfterDelegation(sourceAgent: Pick<IChatAgentData, 'id' | 'name'> | undefined, targetAgent: IChatAgentData | undefined): boolean {
1325
if (!targetAgent) {
1326
// Undefined behavior
1327
return false;
1328
}
1329
1330
if (!this.configurationService.getValue<boolean>(ChatConfiguration.ExitAfterDelegation)) {
1331
return false;
1332
}
1333
1334
// Never exit if the source and target are the same (that means that you're providing a follow up, etc.)
1335
// NOTE: sourceAgent would be the chatWidget's 'lockedAgent'
1336
if (sourceAgent && sourceAgent.id === targetAgent.id) {
1337
return false;
1338
}
1339
1340
if (!isIChatViewViewContext(this.viewContext)) {
1341
return false;
1342
}
1343
1344
const contribution = this.chatSessionsService.getChatSessionContribution(targetAgent.id);
1345
if (!contribution) {
1346
return false;
1347
}
1348
1349
if (contribution.canDelegate !== true) {
1350
return false;
1351
}
1352
1353
return true;
1354
}
1355
1356
/**
1357
* Handles the exit of the panel chat when a delegation to another session occurs.
1358
* Waits for the response to complete and any pending confirmations to be resolved,
1359
* then clears the widget unless the final message is an error.
1360
*/
1361
private async _handleDelegationExit(): Promise<void> {
1362
const viewModel = this.viewModel;
1363
if (!viewModel) {
1364
return;
1365
}
1366
1367
const parentSessionResource = viewModel.sessionResource;
1368
1369
// Check if response is complete, not pending confirmation, and has no error
1370
const checkIfShouldClear = (): boolean => {
1371
const items = viewModel.getItems();
1372
const lastItem = items[items.length - 1];
1373
if (lastItem && isResponseVM(lastItem) && lastItem.model && lastItem.isComplete && !lastItem.model.isPendingConfirmation.get()) {
1374
const hasError = Boolean(lastItem.result?.errorDetails);
1375
return !hasError;
1376
}
1377
return false;
1378
};
1379
1380
if (checkIfShouldClear()) {
1381
await this.clear();
1382
this.archiveLocalParentSession(parentSessionResource);
1383
return;
1384
}
1385
1386
const shouldClear = await new Promise<boolean>(resolve => {
1387
const disposable = viewModel.onDidChange(() => {
1388
const result = checkIfShouldClear();
1389
if (result) {
1390
cleanup();
1391
resolve(true);
1392
}
1393
});
1394
const timeout = setTimeout(() => {
1395
cleanup();
1396
resolve(false);
1397
}, 30_000); // 30 second timeout
1398
const cleanup = () => {
1399
clearTimeout(timeout);
1400
disposable.dispose();
1401
};
1402
});
1403
1404
if (shouldClear) {
1405
await this.clear();
1406
this.archiveLocalParentSession(parentSessionResource);
1407
}
1408
}
1409
1410
private async archiveLocalParentSession(sessionResource: URI): Promise<void> {
1411
if (sessionResource.scheme !== Schemas.vscodeLocalChatSession) {
1412
return;
1413
}
1414
1415
// Implicitly keep parent session's changes as they've now been delegated to the new agent.
1416
await this.chatService.getSession(sessionResource)?.editingSession?.accept();
1417
1418
const session = this.agentSessionsService.getSession(sessionResource);
1419
session?.setArchived(true);
1420
}
1421
1422
setVisible(visible: boolean): void {
1423
const wasVisible = this._visible;
1424
this._visible = visible;
1425
this.visibleChangeCount++;
1426
this.renderer.setVisible(visible);
1427
this.input.setVisible(visible);
1428
1429
if (visible) {
1430
if (!wasVisible) {
1431
this.visibilityTimeoutDisposable.value = disposableTimeout(() => {
1432
// Progressive rendering paused while hidden, so start it up again.
1433
// Do it after a timeout because the container is not visible yet (it should be but offsetHeight returns 0 here)
1434
if (this._visible) {
1435
this.onDidChangeItems(true);
1436
}
1437
}, 0);
1438
1439
this.visibilityAnimationFrameDisposable.value = dom.scheduleAtNextAnimationFrame(dom.getWindow(this.listContainer), () => {
1440
this._onDidShow.fire();
1441
});
1442
}
1443
} else if (wasVisible) {
1444
this._onDidHide.fire();
1445
}
1446
}
1447
1448
private createList(listContainer: HTMLElement, options: IChatListItemRendererOptions): void {
1449
const scopedInstantiationService = this._register(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.contextKeyService])));
1450
const delegate = scopedInstantiationService.createInstance(ChatListDelegate, this.viewOptions.defaultElementHeight ?? 200);
1451
const rendererDelegate: IChatRendererDelegate = {
1452
getListLength: () => this.tree.getNode(null).visibleChildrenCount,
1453
onDidScroll: this.onDidScroll,
1454
container: listContainer,
1455
currentChatMode: () => this.input.currentModeKind,
1456
};
1457
1458
// Create a dom element to hold UI from editor widgets embedded in chat messages
1459
const overflowWidgetsContainer = document.createElement('div');
1460
overflowWidgetsContainer.classList.add('chat-overflow-widget-container', 'monaco-editor');
1461
listContainer.append(overflowWidgetsContainer);
1462
1463
this.renderer = this._register(scopedInstantiationService.createInstance(
1464
ChatListItemRenderer,
1465
this.editorOptions,
1466
options,
1467
rendererDelegate,
1468
this._codeBlockModelCollection,
1469
overflowWidgetsContainer,
1470
this.viewModel,
1471
));
1472
1473
this._register(this.renderer.onDidClickRequest(async item => {
1474
this.clickedRequest(item);
1475
}));
1476
1477
this._register(this.renderer.onDidRerender(item => {
1478
if (isRequestVM(item.currentElement) && this.configurationService.getValue<string>('chat.editRequests') !== 'input') {
1479
if (!item.rowContainer.contains(this.inputContainer)) {
1480
item.rowContainer.appendChild(this.inputContainer);
1481
}
1482
this.input.focus();
1483
}
1484
}));
1485
1486
this._register(this.renderer.onDidDispose((item) => {
1487
this.focusedInputDOM.appendChild(this.inputContainer);
1488
this.input.focus();
1489
}));
1490
1491
this._register(this.renderer.onDidFocusOutside(() => {
1492
this.finishedEditing();
1493
}));
1494
1495
this._register(this.renderer.onDidClickFollowup(item => {
1496
// is this used anymore?
1497
this.acceptInput(item.message);
1498
}));
1499
this._register(this.renderer.onDidClickRerunWithAgentOrCommandDetection(e => {
1500
const request = this.chatService.getSession(e.sessionResource)?.getRequests().find(candidate => candidate.id === e.requestId);
1501
if (request) {
1502
const options: IChatSendRequestOptions = {
1503
noCommandDetection: true,
1504
attempt: request.attempt + 1,
1505
location: this.location,
1506
userSelectedModelId: this.input.currentLanguageModel,
1507
modeInfo: this.input.currentModeInfo,
1508
};
1509
this.chatService.resendRequest(request, options).catch(e => this.logService.error('FAILED to rerun request', e));
1510
}
1511
}));
1512
1513
this.tree = this._register(scopedInstantiationService.createInstance(
1514
WorkbenchObjectTree<ChatTreeItem, FuzzyScore>,
1515
'Chat',
1516
listContainer,
1517
delegate,
1518
[this.renderer],
1519
{
1520
identityProvider: { getId: (e: ChatTreeItem) => e.id },
1521
horizontalScrolling: false,
1522
alwaysConsumeMouseWheel: false,
1523
supportDynamicHeights: true,
1524
hideTwistiesOfChildlessElements: true,
1525
accessibilityProvider: this.instantiationService.createInstance(ChatAccessibilityProvider),
1526
keyboardNavigationLabelProvider: { getKeyboardNavigationLabel: (e: ChatTreeItem) => isRequestVM(e) ? e.message : isResponseVM(e) ? e.response.value : '' }, // TODO
1527
setRowLineHeight: false,
1528
filter: this.viewOptions.filter ? { filter: this.viewOptions.filter.bind(this.viewOptions), } : undefined,
1529
scrollToActiveElement: true,
1530
overrideStyles: {
1531
listFocusBackground: this.styles.listBackground,
1532
listInactiveFocusBackground: this.styles.listBackground,
1533
listActiveSelectionBackground: this.styles.listBackground,
1534
listFocusAndSelectionBackground: this.styles.listBackground,
1535
listInactiveSelectionBackground: this.styles.listBackground,
1536
listHoverBackground: this.styles.listBackground,
1537
listBackground: this.styles.listBackground,
1538
listFocusForeground: this.styles.listForeground,
1539
listHoverForeground: this.styles.listForeground,
1540
listInactiveFocusForeground: this.styles.listForeground,
1541
listInactiveSelectionForeground: this.styles.listForeground,
1542
listActiveSelectionForeground: this.styles.listForeground,
1543
listFocusAndSelectionForeground: this.styles.listForeground,
1544
listActiveSelectionIconForeground: undefined,
1545
listInactiveSelectionIconForeground: undefined,
1546
}
1547
}));
1548
1549
this._register(this.tree.onDidChangeFocus(() => {
1550
const focused = this.tree.getFocus();
1551
if (focused && focused.length > 0) {
1552
const focusedItem = focused[0];
1553
const items = this.tree.getNode(null).children;
1554
const idx = items.findIndex(i => i.element === focusedItem);
1555
if (idx !== -1) {
1556
this._mostRecentlyFocusedItemIndex = idx;
1557
}
1558
}
1559
}));
1560
this._register(this.tree.onContextMenu(e => this.onContextMenu(e)));
1561
1562
this._register(this.tree.onDidChangeContentHeight(() => {
1563
this.onDidChangeTreeContentHeight();
1564
}));
1565
this._register(this.renderer.onDidChangeItemHeight(e => {
1566
if (this.tree.hasElement(e.element) && this.visible) {
1567
this.tree.updateElementHeight(e.element, e.height);
1568
}
1569
}));
1570
this._register(this.tree.onDidFocus(() => {
1571
this._onDidFocus.fire();
1572
}));
1573
this._register(this.tree.onDidScroll(() => {
1574
this._onDidScroll.fire();
1575
1576
const isScrolledDown = this.tree.scrollTop >= this.tree.scrollHeight - this.tree.renderHeight - 2;
1577
this.container.classList.toggle('show-scroll-down', !isScrolledDown && !this.scrollLock);
1578
}));
1579
}
1580
1581
startEditing(requestId: string): void {
1582
const editedRequest = this.renderer.getTemplateDataForRequestId(requestId);
1583
if (editedRequest) {
1584
this.clickedRequest(editedRequest);
1585
}
1586
}
1587
1588
private clickedRequest(item: IChatListItemTemplate) {
1589
1590
const currentElement = item.currentElement;
1591
if (isRequestVM(currentElement) && !this.viewModel?.editing) {
1592
1593
const requests = this.viewModel?.model.getRequests();
1594
if (!requests || !this.viewModel?.sessionResource) {
1595
return;
1596
}
1597
1598
// this will only ever be true if we restored a checkpoint
1599
if (this.viewModel?.model.checkpoint) {
1600
this.recentlyRestoredCheckpoint = true;
1601
}
1602
1603
this.viewModel?.model.setCheckpoint(currentElement.id);
1604
1605
// set contexts and request to false
1606
const currentContext: IChatRequestVariableEntry[] = [];
1607
const addedContextIds = new Set<string>();
1608
const addToContext = (entry: IChatRequestVariableEntry) => {
1609
if (addedContextIds.has(entry.id) || isWorkspaceVariableEntry(entry)) {
1610
return;
1611
}
1612
if ((isPromptFileVariableEntry(entry) || isPromptTextVariableEntry(entry)) && entry.automaticallyAdded) {
1613
return;
1614
}
1615
addedContextIds.add(entry.id);
1616
currentContext.push(entry);
1617
};
1618
for (let i = requests.length - 1; i >= 0; i -= 1) {
1619
const request = requests[i];
1620
if (request.id === currentElement.id) {
1621
request.setShouldBeBlocked(false); // unblocking just this request.
1622
request.attachedContext?.forEach(addToContext);
1623
currentElement.variables.forEach(addToContext);
1624
}
1625
}
1626
1627
// set states
1628
this.viewModel?.setEditing(currentElement);
1629
if (item?.contextKeyService) {
1630
ChatContextKeys.currentlyEditing.bindTo(item.contextKeyService).set(true);
1631
}
1632
1633
const isInput = this.configurationService.getValue<string>('chat.editRequests') === 'input';
1634
this.inputPart?.setEditing(!!this.viewModel?.editing && isInput);
1635
1636
if (!isInput) {
1637
const rowContainer = item.rowContainer;
1638
this.inputContainer = dom.$('.chat-edit-input-container');
1639
rowContainer.appendChild(this.inputContainer);
1640
this.createInput(this.inputContainer);
1641
this.input.setChatMode(this.inputPart.currentModeObs.get().id);
1642
} else {
1643
this.inputPart.element.classList.add('editing');
1644
}
1645
1646
this.inputPart.toggleChatInputOverlay(!isInput);
1647
if (currentContext.length > 0) {
1648
this.input.attachmentModel.addContext(...currentContext);
1649
}
1650
1651
1652
// rerenders
1653
this.inputPart.dnd.setDisabledOverlay(!isInput);
1654
this.input.renderAttachedContext();
1655
this.input.setValue(currentElement.messageText, false);
1656
this.renderer.updateItemHeightOnRender(currentElement, item);
1657
this.onDidChangeItems();
1658
this.input.inputEditor.focus();
1659
1660
this._register(this.inputPart.onDidClickOverlay(() => {
1661
if (this.viewModel?.editing && this.configurationService.getValue<string>('chat.editRequests') !== 'input') {
1662
this.finishedEditing();
1663
}
1664
}));
1665
1666
// listeners
1667
if (!isInput) {
1668
this._register(this.inlineInputPart.inputEditor.onDidChangeModelContent(() => {
1669
this.scrollToCurrentItem(currentElement);
1670
}));
1671
1672
this._register(this.inlineInputPart.inputEditor.onDidChangeCursorSelection((e) => {
1673
this.scrollToCurrentItem(currentElement);
1674
}));
1675
}
1676
}
1677
1678
type StartRequestEvent = { editRequestType: string };
1679
1680
type StartRequestEventClassification = {
1681
owner: 'justschen';
1682
comment: 'Event used to gain insights into when edits are being pressed.';
1683
editRequestType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Current entry point for editing a request.' };
1684
};
1685
1686
this.telemetryService.publicLog2<StartRequestEvent, StartRequestEventClassification>('chat.startEditingRequests', {
1687
editRequestType: this.configurationService.getValue<string>('chat.editRequests'),
1688
});
1689
}
1690
1691
finishedEditing(completedEdit?: boolean): void {
1692
// reset states
1693
const editedRequest = this.renderer.getTemplateDataForRequestId(this.viewModel?.editing?.id);
1694
if (this.recentlyRestoredCheckpoint) {
1695
this.recentlyRestoredCheckpoint = false;
1696
} else {
1697
this.viewModel?.model.setCheckpoint(undefined);
1698
}
1699
this.inputPart.dnd.setDisabledOverlay(false);
1700
if (editedRequest?.contextKeyService) {
1701
ChatContextKeys.currentlyEditing.bindTo(editedRequest.contextKeyService).set(false);
1702
}
1703
1704
const isInput = this.configurationService.getValue<string>('chat.editRequests') === 'input';
1705
1706
if (!isInput) {
1707
this.inputPart.setChatMode(this.input.currentModeObs.get().id);
1708
const currentModel = this.input.selectedLanguageModel;
1709
if (currentModel) {
1710
this.inputPart.switchModel(currentModel.metadata);
1711
}
1712
1713
this.inputPart?.toggleChatInputOverlay(false);
1714
try {
1715
if (editedRequest?.rowContainer?.contains(this.inputContainer)) {
1716
editedRequest.rowContainer.removeChild(this.inputContainer);
1717
} else if (this.inputContainer.parentElement) {
1718
this.inputContainer.parentElement.removeChild(this.inputContainer);
1719
}
1720
} catch (e) {
1721
this.logService.error('Error occurred while finishing editing:', e);
1722
}
1723
this.inputContainer = dom.$('.empty-chat-state');
1724
1725
// only dispose if we know the input is not the bottom input object.
1726
this.input.dispose();
1727
}
1728
1729
if (isInput) {
1730
this.inputPart.element.classList.remove('editing');
1731
}
1732
this.viewModel?.setEditing(undefined);
1733
1734
this.inputPart?.setEditing(!!this.viewModel?.editing && isInput);
1735
1736
this.onDidChangeItems();
1737
if (editedRequest?.currentElement) {
1738
this.renderer.updateItemHeightOnRender(editedRequest.currentElement, editedRequest);
1739
}
1740
1741
type CancelRequestEditEvent = {
1742
editRequestType: string;
1743
editCanceled: boolean;
1744
};
1745
1746
type CancelRequestEventEditClassification = {
1747
owner: 'justschen';
1748
editRequestType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Current entry point for editing a request.' };
1749
editCanceled: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Indicates whether the edit was canceled.' };
1750
comment: 'Event used to gain insights into when edits are being canceled.';
1751
};
1752
1753
this.telemetryService.publicLog2<CancelRequestEditEvent, CancelRequestEventEditClassification>('chat.editRequestsFinished', {
1754
editRequestType: this.configurationService.getValue<string>('chat.editRequests'),
1755
editCanceled: !completedEdit
1756
});
1757
1758
this.inputPart.focus();
1759
}
1760
1761
private scrollToCurrentItem(currentElement: IChatRequestViewModel): void {
1762
if (this.viewModel?.editing && currentElement) {
1763
const element = currentElement;
1764
if (!this.tree.hasElement(element)) {
1765
return;
1766
}
1767
const relativeTop = this.tree.getRelativeTop(element);
1768
if (relativeTop === null || relativeTop < 0 || relativeTop > 1) {
1769
this.tree.reveal(element, 0);
1770
}
1771
}
1772
}
1773
1774
private onContextMenu(e: ITreeContextMenuEvent<ChatTreeItem | null>): void {
1775
e.browserEvent.preventDefault();
1776
e.browserEvent.stopPropagation();
1777
1778
const selected = e.element;
1779
1780
// Check if the context menu was opened on a KaTeX element
1781
const target = e.browserEvent.target as HTMLElement;
1782
const isKatexElement = target.closest(`.${katexContainerClassName}`) !== null;
1783
1784
const scopedContextKeyService = this.contextKeyService.createOverlay([
1785
[ChatContextKeys.responseIsFiltered.key, isResponseVM(selected) && !!selected.errorDetails?.responseIsFiltered],
1786
[ChatContextKeys.isKatexMathElement.key, isKatexElement]
1787
]);
1788
this.contextMenuService.showContextMenu({
1789
menuId: MenuId.ChatContext,
1790
menuActionOptions: { shouldForwardArgs: true },
1791
contextKeyService: scopedContextKeyService,
1792
getAnchor: () => e.anchor,
1793
getActionsContext: () => selected,
1794
});
1795
}
1796
1797
private onDidChangeTreeContentHeight(): void {
1798
// If the list was previously scrolled all the way down, ensure it stays scrolled down, if scroll lock is on
1799
if (this.tree.scrollHeight !== this.previousTreeScrollHeight) {
1800
const lastItem = this.viewModel?.getItems().at(-1);
1801
const lastResponseIsRendering = isResponseVM(lastItem) && lastItem.renderData;
1802
if (!lastResponseIsRendering || this.scrollLock) {
1803
// Due to rounding, the scrollTop + renderHeight will not exactly match the scrollHeight.
1804
// Consider the tree to be scrolled all the way down if it is within 2px of the bottom.
1805
const lastElementWasVisible = this.tree.scrollTop + this.tree.renderHeight >= this.previousTreeScrollHeight - 2;
1806
if (lastElementWasVisible) {
1807
this.scrollAnimationFrameDisposable.value = dom.scheduleAtNextAnimationFrame(dom.getWindow(this.listContainer), () => {
1808
// Can't set scrollTop during this event listener, the list might overwrite the change
1809
1810
this.scrollToEnd();
1811
}, 0);
1812
}
1813
}
1814
}
1815
1816
// TODO@roblourens add `show-scroll-down` class when button should show
1817
// Show the button when content height changes, the list is not fully scrolled down, and (the latest response is currently rendering OR I haven't yet scrolled all the way down since the last response)
1818
// So for example it would not reappear if I scroll up and delete a message
1819
1820
this.previousTreeScrollHeight = this.tree.scrollHeight;
1821
this._onDidChangeContentHeight.fire();
1822
}
1823
1824
private getWidgetViewKindTag(): string {
1825
if (!this.viewContext) {
1826
return 'editor';
1827
} else if (isIChatViewViewContext(this.viewContext)) {
1828
return 'view';
1829
} else {
1830
return 'quick';
1831
}
1832
}
1833
1834
private createInput(container: HTMLElement, options?: { renderFollowups: boolean; renderStyle?: 'compact' | 'minimal'; renderInputToolbarBelowInput?: boolean }): void {
1835
const commonConfig: IChatInputPartOptions = {
1836
renderFollowups: options?.renderFollowups ?? true,
1837
renderStyle: options?.renderStyle === 'minimal' ? 'compact' : options?.renderStyle,
1838
renderInputToolbarBelowInput: options?.renderInputToolbarBelowInput ?? false,
1839
menus: {
1840
executeToolbar: MenuId.ChatExecute,
1841
telemetrySource: 'chatWidget',
1842
...this.viewOptions.menus
1843
},
1844
editorOverflowWidgetsDomNode: this.viewOptions.editorOverflowWidgetsDomNode,
1845
enableImplicitContext: this.viewOptions.enableImplicitContext,
1846
renderWorkingSet: this.viewOptions.enableWorkingSet === 'explicit',
1847
supportsChangingModes: this.viewOptions.supportsChangingModes,
1848
dndContainer: this.viewOptions.dndContainer,
1849
widgetViewKindTag: this.getWidgetViewKindTag(),
1850
defaultMode: this.viewOptions.defaultMode
1851
};
1852
1853
if (this.viewModel?.editing) {
1854
const editedRequest = this.renderer.getTemplateDataForRequestId(this.viewModel?.editing?.id);
1855
const scopedInstantiationService = this._register(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, editedRequest?.contextKeyService])));
1856
this.inlineInputPartDisposable.value = scopedInstantiationService.createInstance(ChatInputPart,
1857
this.location,
1858
commonConfig,
1859
this.styles,
1860
true
1861
);
1862
} else {
1863
this.inputPartDisposable.value = this.instantiationService.createInstance(ChatInputPart,
1864
this.location,
1865
commonConfig,
1866
this.styles,
1867
false
1868
);
1869
}
1870
1871
this.input.render(container, '', this);
1872
1873
this._register(this.input.onDidLoadInputState(() => {
1874
this.refreshParsedInput();
1875
}));
1876
this._register(this.input.onDidFocus(() => this._onDidFocus.fire()));
1877
this._register(this.input.onDidAcceptFollowup(e => {
1878
if (!this.viewModel) {
1879
return;
1880
}
1881
1882
let msg = '';
1883
if (e.followup.agentId && e.followup.agentId !== this.chatAgentService.getDefaultAgent(this.location, this.input.currentModeKind)?.id) {
1884
const agent = this.chatAgentService.getAgent(e.followup.agentId);
1885
if (!agent) {
1886
return;
1887
}
1888
1889
this.lastSelectedAgent = agent;
1890
msg = `${chatAgentLeader}${agent.name} `;
1891
if (e.followup.subCommand) {
1892
msg += `${chatSubcommandLeader}${e.followup.subCommand} `;
1893
}
1894
} else if (!e.followup.agentId && e.followup.subCommand && this.chatSlashCommandService.hasCommand(e.followup.subCommand)) {
1895
msg = `${chatSubcommandLeader}${e.followup.subCommand} `;
1896
}
1897
1898
msg += e.followup.message;
1899
this.acceptInput(msg);
1900
1901
if (!e.response) {
1902
// Followups can be shown by the welcome message, then there is no response associated.
1903
// At some point we probably want telemetry for these too.
1904
return;
1905
}
1906
1907
this.chatService.notifyUserAction({
1908
sessionResource: this.viewModel.sessionResource,
1909
requestId: e.response.requestId,
1910
agentId: e.response.agent?.id,
1911
command: e.response.slashCommand?.name,
1912
result: e.response.result,
1913
action: {
1914
kind: 'followUp',
1915
followup: e.followup
1916
},
1917
});
1918
}));
1919
this._register(this.input.onDidChangeHeight(() => {
1920
const editedRequest = this.renderer.getTemplateDataForRequestId(this.viewModel?.editing?.id);
1921
if (isRequestVM(editedRequest?.currentElement) && this.viewModel?.editing) {
1922
this.renderer.updateItemHeightOnRender(editedRequest?.currentElement, editedRequest);
1923
}
1924
1925
if (this.bodyDimension) {
1926
this.layout(this.bodyDimension.height, this.bodyDimension.width);
1927
}
1928
1929
this._onDidChangeContentHeight.fire();
1930
}));
1931
this._register(this.inputEditor.onDidChangeModelContent(() => {
1932
this.parsedChatRequest = undefined;
1933
this.updateChatInputContext();
1934
}));
1935
this._register(this.chatAgentService.onDidChangeAgents(() => {
1936
this.parsedChatRequest = undefined;
1937
// Tools agent loads -> welcome content changes
1938
this.renderWelcomeViewContentIfNeeded();
1939
}));
1940
this._register(this.input.onDidChangeCurrentChatMode(() => {
1941
this.renderWelcomeViewContentIfNeeded();
1942
this.refreshParsedInput();
1943
this.renderFollowups();
1944
this.renderChatSuggestNextWidget();
1945
}));
1946
1947
this._register(autorun(r => {
1948
const toolSetIds = new Set<string>();
1949
const toolIds = new Set<string>();
1950
for (const [entry, enabled] of this.input.selectedToolsModel.entriesMap.read(r)) {
1951
if (enabled) {
1952
if (entry instanceof ToolSet) {
1953
toolSetIds.add(entry.id);
1954
} else {
1955
toolIds.add(entry.id);
1956
}
1957
}
1958
}
1959
const disabledTools = this.input.attachmentModel.attachments
1960
.filter(a => a.kind === 'tool' && !toolIds.has(a.id) || a.kind === 'toolset' && !toolSetIds.has(a.id))
1961
.map(a => a.id);
1962
1963
this.input.attachmentModel.updateContext(disabledTools, Iterable.empty());
1964
this.refreshParsedInput();
1965
}));
1966
}
1967
1968
private onDidStyleChange(): void {
1969
this.container.style.setProperty('--vscode-interactive-result-editor-background-color', this.editorOptions.configuration.resultEditor.backgroundColor?.toString() ?? '');
1970
this.container.style.setProperty('--vscode-interactive-session-foreground', this.editorOptions.configuration.foreground?.toString() ?? '');
1971
this.container.style.setProperty('--vscode-chat-list-background', this.themeService.getColorTheme().getColor(this.styles.listBackground)?.toString() ?? '');
1972
}
1973
1974
1975
setModel(model: IChatModel | undefined): void {
1976
if (!this.container) {
1977
throw new Error('Call render() before setModel()');
1978
}
1979
1980
if (!model) {
1981
if (this.viewModel?.editing) {
1982
this.finishedEditing();
1983
}
1984
this.viewModel = undefined;
1985
this.onDidChangeItems();
1986
return;
1987
}
1988
1989
if (isEqual(model.sessionResource, this.viewModel?.sessionResource)) {
1990
return;
1991
}
1992
1993
if (this.viewModel?.editing) {
1994
this.finishedEditing();
1995
}
1996
this.inputPart.clearTodoListWidget(model.sessionResource, false);
1997
this.chatSuggestNextWidget.hide();
1998
1999
this._codeBlockModelCollection.clear();
2000
2001
this.container.setAttribute('data-session-id', model.sessionId);
2002
this.viewModel = this.instantiationService.createInstance(ChatViewModel, model, this._codeBlockModelCollection);
2003
2004
// Pass input model reference to input part for state syncing
2005
this.inputPart.setInputModel(model.inputModel, model.getRequests().length === 0);
2006
2007
if (this._lockedAgent) {
2008
let placeholder = this.chatSessionsService.getInputPlaceholderForSessionType(this._lockedAgent.id);
2009
if (!placeholder) {
2010
placeholder = localize('chat.input.placeholder.lockedToAgent', "Chat with {0}", this._lockedAgent.id);
2011
}
2012
this.viewModel.setInputPlaceholder(placeholder);
2013
this.inputEditor.updateOptions({ placeholder });
2014
} else if (this.viewModel.inputPlaceholder) {
2015
this.inputEditor.updateOptions({ placeholder: this.viewModel.inputPlaceholder });
2016
}
2017
2018
const renderImmediately = this.configurationService.getValue<boolean>('chat.experimental.renderMarkdownImmediately');
2019
const delay = renderImmediately ? MicrotaskDelay : 0;
2020
this.viewModelDisposables.add(Event.runAndSubscribe(Event.accumulate(this.viewModel.onDidChange, delay), (events => {
2021
if (!this.viewModel || this._store.isDisposed) {
2022
// See https://github.com/microsoft/vscode/issues/278969
2023
return;
2024
}
2025
2026
this.requestInProgress.set(this.viewModel.model.requestInProgress.get());
2027
2028
// Update the editor's placeholder text when it changes in the view model
2029
if (events?.some(e => e?.kind === 'changePlaceholder')) {
2030
this.inputEditor.updateOptions({ placeholder: this.viewModel.inputPlaceholder });
2031
}
2032
2033
this.onDidChangeItems();
2034
if (events?.some(e => e?.kind === 'addRequest') && this.visible) {
2035
this.scrollToEnd();
2036
}
2037
})));
2038
this.viewModelDisposables.add(this.viewModel.onDidDisposeModel(() => {
2039
// Ensure that view state is saved here, because we will load it again when a new model is assigned
2040
if (this.viewModel?.editing) {
2041
this.finishedEditing();
2042
}
2043
// Disposes the viewmodel and listeners
2044
this.viewModel = undefined;
2045
this.onDidChangeItems();
2046
}));
2047
this._sessionIsEmptyContextKey.set(model.getRequests().length === 0);
2048
2049
this.refreshParsedInput();
2050
this.viewModelDisposables.add(model.onDidChange((e) => {
2051
if (e.kind === 'setAgent') {
2052
this._onDidChangeAgent.fire({ agent: e.agent, slashCommand: e.command });
2053
// Update capabilities context keys when agent changes
2054
this._updateAgentCapabilitiesContextKeys(e.agent);
2055
}
2056
if (e.kind === 'addRequest') {
2057
this.inputPart.clearTodoListWidget(this.viewModel?.sessionResource, false);
2058
this._sessionIsEmptyContextKey.set(false);
2059
}
2060
// Hide widget on request removal
2061
if (e.kind === 'removeRequest') {
2062
this.inputPart.clearTodoListWidget(this.viewModel?.sessionResource, true);
2063
this.chatSuggestNextWidget.hide();
2064
this._sessionIsEmptyContextKey.set((this.viewModel?.model.getRequests().length ?? 0) === 0);
2065
}
2066
// Show next steps widget when response completes (not when request starts)
2067
if (e.kind === 'completedRequest') {
2068
const lastRequest = this.viewModel?.model.getRequests().at(-1);
2069
const wasCancelled = lastRequest?.response?.isCanceled ?? false;
2070
if (wasCancelled) {
2071
// Clear todo list when request is cancelled
2072
this.inputPart.clearTodoListWidget(this.viewModel?.sessionResource, true);
2073
}
2074
// Only show if response wasn't canceled
2075
this.renderChatSuggestNextWidget();
2076
}
2077
}));
2078
2079
if (this.tree && this.visible) {
2080
this.onDidChangeItems();
2081
this.scrollToEnd();
2082
}
2083
2084
this.renderer.updateViewModel(this.viewModel);
2085
this.updateChatInputContext();
2086
this.input.renderChatTodoListWidget(this.viewModel.sessionResource);
2087
}
2088
2089
getFocus(): ChatTreeItem | undefined {
2090
return this.tree.getFocus()[0] ?? undefined;
2091
}
2092
2093
reveal(item: ChatTreeItem, relativeTop?: number): void {
2094
this.tree.reveal(item, relativeTop);
2095
}
2096
2097
focus(item: ChatTreeItem): void {
2098
const items = this.tree.getNode(null).children;
2099
const node = items.find(i => i.element?.id === item.id);
2100
if (!node) {
2101
return;
2102
}
2103
2104
this._mostRecentlyFocusedItemIndex = items.indexOf(node);
2105
this.tree.setFocus([node.element]);
2106
this.tree.domFocus();
2107
}
2108
2109
setInputPlaceholder(placeholder: string): void {
2110
this.viewModel?.setInputPlaceholder(placeholder);
2111
}
2112
2113
resetInputPlaceholder(): void {
2114
this.viewModel?.resetInputPlaceholder();
2115
}
2116
2117
setInput(value = ''): void {
2118
this.input.setValue(value, false);
2119
this.refreshParsedInput();
2120
}
2121
2122
getInput(): string {
2123
return this.input.inputEditor.getValue();
2124
}
2125
2126
getContrib<T extends IChatWidgetContrib>(id: string): T | undefined {
2127
return this.contribs.find(c => c.id === id) as T | undefined;
2128
}
2129
2130
// Coding agent locking methods
2131
lockToCodingAgent(name: string, displayName: string, agentId: string): void {
2132
this._lockedAgent = {
2133
id: agentId,
2134
name,
2135
prefix: `@${name} `,
2136
displayName
2137
};
2138
this._lockedToCodingAgentContextKey.set(true);
2139
this.renderWelcomeViewContentIfNeeded();
2140
// Update capabilities for the locked agent
2141
const agent = this.chatAgentService.getAgent(agentId);
2142
this._updateAgentCapabilitiesContextKeys(agent);
2143
this.renderer.updateOptions({ restorable: false, editable: false, noFooter: true, progressMessageAtBottomOfResponse: true });
2144
if (this.visible) {
2145
this.tree.rerender();
2146
}
2147
}
2148
2149
unlockFromCodingAgent(): void {
2150
// Clear all state related to locking
2151
this._lockedAgent = undefined;
2152
this._lockedToCodingAgentContextKey.set(false);
2153
this._updateAgentCapabilitiesContextKeys(undefined);
2154
2155
// Explicitly update the DOM to reflect unlocked state
2156
this.renderWelcomeViewContentIfNeeded();
2157
2158
// Reset to default placeholder
2159
if (this.viewModel) {
2160
this.viewModel.resetInputPlaceholder();
2161
}
2162
this.inputEditor.updateOptions({ placeholder: undefined });
2163
this.renderer.updateOptions({ restorable: true, editable: true, noFooter: false, progressMessageAtBottomOfResponse: mode => mode !== ChatModeKind.Ask });
2164
if (this.visible) {
2165
this.tree.rerender();
2166
}
2167
}
2168
2169
get isLockedToCodingAgent(): boolean {
2170
return !!this._lockedAgent;
2171
}
2172
2173
get lockedAgentId(): string | undefined {
2174
return this._lockedAgent?.id;
2175
}
2176
2177
logInputHistory(): void {
2178
this.input.logInputHistory();
2179
}
2180
2181
async acceptInput(query?: string, options?: IChatAcceptInputOptions): Promise<IChatResponseModel | undefined> {
2182
return this._acceptInput(query ? { query } : undefined, options);
2183
}
2184
2185
async rerunLastRequest(): Promise<void> {
2186
if (!this.viewModel) {
2187
return;
2188
}
2189
2190
const sessionResource = this.viewModel.sessionResource;
2191
const lastRequest = this.chatService.getSession(sessionResource)?.getRequests().at(-1);
2192
if (!lastRequest) {
2193
return;
2194
}
2195
2196
const options: IChatSendRequestOptions = {
2197
attempt: lastRequest.attempt + 1,
2198
location: this.location,
2199
userSelectedModelId: this.input.currentLanguageModel
2200
};
2201
return await this.chatService.resendRequest(lastRequest, options);
2202
}
2203
2204
private async _applyPromptFileIfSet(requestInput: IChatRequestInputOptions): Promise<void> {
2205
// first check if the input has a prompt slash command
2206
const agentSlashPromptPart = this.parsedInput.parts.find((r): r is ChatRequestSlashPromptPart => r instanceof ChatRequestSlashPromptPart);
2207
if (!agentSlashPromptPart) {
2208
return;
2209
}
2210
2211
// need to resolve the slash command to get the prompt file
2212
const slashCommand = await this.promptsService.resolvePromptSlashCommand(agentSlashPromptPart.name, CancellationToken.None);
2213
if (!slashCommand) {
2214
return;
2215
}
2216
const parseResult = slashCommand.parsedPromptFile;
2217
// add the prompt file to the context
2218
const refs = parseResult.body?.variableReferences.map(({ name, offset }) => ({ name, range: new OffsetRange(offset, offset + name.length + 1) })) ?? [];
2219
const toolReferences = this.toolsService.toToolReferences(refs);
2220
requestInput.attachedContext.insertFirst(toPromptFileVariableEntry(parseResult.uri, PromptFileVariableKind.PromptFile, undefined, true, toolReferences));
2221
2222
// remove the slash command from the input
2223
requestInput.input = this.parsedInput.parts.filter(part => !(part instanceof ChatRequestSlashPromptPart)).map(part => part.text).join('').trim();
2224
2225
const input = requestInput.input.trim();
2226
requestInput.input = `Follow instructions in [${basename(parseResult.uri)}](${parseResult.uri.toString()}).`;
2227
if (input) {
2228
// if the input is not empty, append it to the prompt
2229
requestInput.input += `\n${input}`;
2230
}
2231
if (parseResult.header) {
2232
await this._applyPromptMetadata(parseResult.header, requestInput);
2233
}
2234
}
2235
2236
private async _acceptInput(query: { query: string } | undefined, options?: IChatAcceptInputOptions): Promise<IChatResponseModel | undefined> {
2237
if (this.viewModel?.model.requestInProgress.get()) {
2238
return;
2239
}
2240
2241
if (!query && this.input.generating) {
2242
// if the user submits the input and generation finishes quickly, just submit it for them
2243
const generatingAutoSubmitWindow = 500;
2244
const start = Date.now();
2245
await this.input.generating;
2246
if (Date.now() - start > generatingAutoSubmitWindow) {
2247
return;
2248
}
2249
}
2250
2251
while (!this._viewModel && !this._store.isDisposed) {
2252
await Event.toPromise(this.onDidChangeViewModel, this._store);
2253
}
2254
2255
if (!this.viewModel) {
2256
return;
2257
}
2258
2259
this._onDidAcceptInput.fire();
2260
this.scrollLock = this.isLockedToCodingAgent || !!checkModeOption(this.input.currentModeKind, this.viewOptions.autoScroll);
2261
2262
const editorValue = this.getInput();
2263
const requestInputs: IChatRequestInputOptions = {
2264
input: !query ? editorValue : query.query,
2265
attachedContext: options?.enableImplicitContext === false ? this.input.getAttachedContext(this.viewModel.sessionResource) : this.input.getAttachedAndImplicitContext(this.viewModel.sessionResource),
2266
};
2267
2268
const isUserQuery = !query;
2269
2270
if (this.viewModel?.editing) {
2271
this.finishedEditing(true);
2272
this.viewModel.model?.setCheckpoint(undefined);
2273
}
2274
2275
// process the prompt command
2276
await this._applyPromptFileIfSet(requestInputs);
2277
await this._autoAttachInstructions(requestInputs);
2278
2279
if (this.viewOptions.enableWorkingSet !== undefined && this.input.currentModeKind === ChatModeKind.Edit && !this.chatService.edits2Enabled) {
2280
const uniqueWorkingSetEntries = new ResourceSet(); // NOTE: this is used for bookkeeping so the UI can avoid rendering references in the UI that are already shown in the working set
2281
const editingSessionAttachedContext: ChatRequestVariableSet = requestInputs.attachedContext;
2282
2283
// Collect file variables from previous requests before sending the request
2284
const previousRequests = this.viewModel.model.getRequests();
2285
for (const request of previousRequests) {
2286
for (const variable of request.variableData.variables) {
2287
if (URI.isUri(variable.value) && variable.kind === 'file') {
2288
const uri = variable.value;
2289
if (!uniqueWorkingSetEntries.has(uri)) {
2290
editingSessionAttachedContext.add(variable);
2291
uniqueWorkingSetEntries.add(variable.value);
2292
}
2293
}
2294
}
2295
}
2296
requestInputs.attachedContext = editingSessionAttachedContext;
2297
2298
type ChatEditingWorkingSetClassification = {
2299
owner: 'joyceerhl';
2300
comment: 'Information about the working set size in a chat editing request';
2301
originalSize: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The number of files that the user tried to attach in their editing request.' };
2302
actualSize: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The number of files that were actually sent in their editing request.' };
2303
};
2304
type ChatEditingWorkingSetEvent = {
2305
originalSize: number;
2306
actualSize: number;
2307
};
2308
this.telemetryService.publicLog2<ChatEditingWorkingSetEvent, ChatEditingWorkingSetClassification>('chatEditing/workingSetSize', { originalSize: uniqueWorkingSetEntries.size, actualSize: uniqueWorkingSetEntries.size });
2309
}
2310
this.chatService.cancelCurrentRequestForSession(this.viewModel.sessionResource);
2311
if (this.currentRequest) {
2312
// We have to wait the current request to be properly cancelled so that it has a chance to update the model with its result metadata.
2313
// This is awkward, it's basically a limitation of the chat provider-based agent.
2314
await Promise.race([this.currentRequest, timeout(1000)]);
2315
}
2316
2317
this.input.validateAgentMode();
2318
2319
if (this.viewModel.model.checkpoint) {
2320
const requests = this.viewModel.model.getRequests();
2321
for (let i = requests.length - 1; i >= 0; i -= 1) {
2322
const request = requests[i];
2323
if (request.shouldBeBlocked) {
2324
this.chatService.removeRequest(this.viewModel.sessionResource, request.id);
2325
}
2326
}
2327
}
2328
if (this.viewModel.sessionResource) {
2329
this.chatAccessibilityService.acceptRequest(this._viewModel!.sessionResource);
2330
}
2331
2332
const result = await this.chatService.sendRequest(this.viewModel.sessionResource, requestInputs.input, {
2333
userSelectedModelId: this.input.currentLanguageModel,
2334
location: this.location,
2335
locationData: this._location.resolveData?.(),
2336
parserContext: { selectedAgent: this._lastSelectedAgent, mode: this.input.currentModeKind },
2337
attachedContext: requestInputs.attachedContext.asArray(),
2338
noCommandDetection: options?.noCommandDetection,
2339
...this.getModeRequestOptions(),
2340
modeInfo: this.input.currentModeInfo,
2341
agentIdSilent: this._lockedAgent?.id,
2342
});
2343
2344
if (!result) {
2345
this.chatAccessibilityService.disposeRequest(this.viewModel.sessionResource);
2346
return;
2347
}
2348
2349
// visibility sync before we accept input to hide the welcome view
2350
this.updateChatViewVisibility();
2351
2352
this.input.acceptInput(options?.storeToHistory ?? isUserQuery);
2353
this._onDidSubmitAgent.fire({ agent: result.agent, slashCommand: result.slashCommand });
2354
this.handleDelegationExitIfNeeded(this._lockedAgent, result.agent);
2355
this.currentRequest = result.responseCompletePromise.then(() => {
2356
const responses = this.viewModel?.getItems().filter(isResponseVM);
2357
const lastResponse = responses?.[responses.length - 1];
2358
this.chatAccessibilityService.acceptResponse(this, this.container, lastResponse, this.viewModel?.sessionResource, options?.isVoiceInput);
2359
if (lastResponse?.result?.nextQuestion) {
2360
const { prompt, participant, command } = lastResponse.result.nextQuestion;
2361
const question = formatChatQuestion(this.chatAgentService, this.location, prompt, participant, command);
2362
if (question) {
2363
this.input.setValue(question, false);
2364
}
2365
}
2366
this.currentRequest = undefined;
2367
});
2368
2369
return result.responseCreatedPromise;
2370
}
2371
2372
getModeRequestOptions(): Partial<IChatSendRequestOptions> {
2373
return {
2374
modeInfo: this.input.currentModeInfo,
2375
userSelectedTools: this.input.selectedToolsModel.userSelectedTools,
2376
};
2377
}
2378
2379
getCodeBlockInfosForResponse(response: IChatResponseViewModel): IChatCodeBlockInfo[] {
2380
return this.renderer.getCodeBlockInfosForResponse(response);
2381
}
2382
2383
getCodeBlockInfoForEditor(uri: URI): IChatCodeBlockInfo | undefined {
2384
return this.renderer.getCodeBlockInfoForEditor(uri);
2385
}
2386
2387
getFileTreeInfosForResponse(response: IChatResponseViewModel): IChatFileTreeInfo[] {
2388
return this.renderer.getFileTreeInfosForResponse(response);
2389
}
2390
2391
getLastFocusedFileTreeForResponse(response: IChatResponseViewModel): IChatFileTreeInfo | undefined {
2392
return this.renderer.getLastFocusedFileTreeForResponse(response);
2393
}
2394
2395
focusResponseItem(lastFocused?: boolean): void {
2396
if (!this.viewModel) {
2397
return;
2398
}
2399
const items = this.tree.getNode(null).children;
2400
let item;
2401
if (lastFocused) {
2402
item = items[this._mostRecentlyFocusedItemIndex] ?? items[items.length - 1];
2403
} else {
2404
item = items[items.length - 1];
2405
}
2406
if (!item) {
2407
return;
2408
}
2409
2410
this.tree.setFocus([item.element]);
2411
this.tree.domFocus();
2412
}
2413
2414
layout(height: number, width: number): void {
2415
width = Math.min(width, this.viewOptions.renderStyle === 'minimal' ? width : 950); // no min width of inline chat
2416
2417
const heightUpdated = this.bodyDimension && this.bodyDimension.height !== height;
2418
this.bodyDimension = new dom.Dimension(width, height);
2419
2420
const layoutHeight = this._dynamicMessageLayoutData?.enabled ? this._dynamicMessageLayoutData.maxHeight : height;
2421
if (this.viewModel?.editing) {
2422
this.inlineInputPart?.layout(layoutHeight, width);
2423
}
2424
2425
this.inputPart.layout(layoutHeight, width);
2426
2427
const inputHeight = this.inputPart.inputPartHeight;
2428
const chatSuggestNextWidgetHeight = this.chatSuggestNextWidget.height;
2429
const lastElementVisible = this.tree.scrollTop + this.tree.renderHeight >= this.tree.scrollHeight - 2;
2430
const lastItem = this.viewModel?.getItems().at(-1);
2431
2432
const contentHeight = Math.max(0, height - inputHeight - chatSuggestNextWidgetHeight);
2433
if (this.viewOptions.renderStyle === 'compact' || this.viewOptions.renderStyle === 'minimal') {
2434
this.listContainer.style.removeProperty('--chat-current-response-min-height');
2435
} else {
2436
this.listContainer.style.setProperty('--chat-current-response-min-height', contentHeight * .75 + 'px');
2437
if (heightUpdated && lastItem && this.visible && this.tree.hasElement(lastItem)) {
2438
this.tree.updateElementHeight(lastItem, undefined);
2439
}
2440
}
2441
this.tree.layout(contentHeight, width);
2442
2443
this.welcomeMessageContainer.style.height = `${contentHeight}px`;
2444
2445
this.renderer.layout(width);
2446
2447
const lastResponseIsRendering = isResponseVM(lastItem) && lastItem.renderData;
2448
if (lastElementVisible && (!lastResponseIsRendering || checkModeOption(this.input.currentModeKind, this.viewOptions.autoScroll))) {
2449
this.scrollToEnd();
2450
}
2451
this.listContainer.style.height = `${contentHeight}px`;
2452
2453
this._onDidChangeHeight.fire(height);
2454
}
2455
2456
private _dynamicMessageLayoutData?: { numOfMessages: number; maxHeight: number; enabled: boolean };
2457
2458
// An alternative to layout, this allows you to specify the number of ChatTreeItems
2459
// you want to show, and the max height of the container. It will then layout the
2460
// tree to show that many items.
2461
// TODO@TylerLeonhardt: This could use some refactoring to make it clear which layout strategy is being used
2462
setDynamicChatTreeItemLayout(numOfChatTreeItems: number, maxHeight: number) {
2463
this._dynamicMessageLayoutData = { numOfMessages: numOfChatTreeItems, maxHeight, enabled: true };
2464
this._register(this.renderer.onDidChangeItemHeight(() => this.layoutDynamicChatTreeItemMode()));
2465
2466
const mutableDisposable = this._register(new MutableDisposable());
2467
this._register(this.tree.onDidScroll((e) => {
2468
// TODO@TylerLeonhardt this should probably just be disposed when this is disabled
2469
// and then set up again when it is enabled again
2470
if (!this._dynamicMessageLayoutData?.enabled) {
2471
return;
2472
}
2473
mutableDisposable.value = dom.scheduleAtNextAnimationFrame(dom.getWindow(this.listContainer), () => {
2474
if (!e.scrollTopChanged || e.heightChanged || e.scrollHeightChanged) {
2475
return;
2476
}
2477
const renderHeight = e.height;
2478
const diff = e.scrollHeight - renderHeight - e.scrollTop;
2479
if (diff === 0) {
2480
return;
2481
}
2482
2483
const possibleMaxHeight = (this._dynamicMessageLayoutData?.maxHeight ?? maxHeight);
2484
const width = this.bodyDimension?.width ?? this.container.offsetWidth;
2485
this.input.layout(possibleMaxHeight, width);
2486
const inputPartHeight = this.input.inputPartHeight;
2487
const chatSuggestNextWidgetHeight = this.chatSuggestNextWidget.height;
2488
const newHeight = Math.min(renderHeight + diff, possibleMaxHeight - inputPartHeight - chatSuggestNextWidgetHeight);
2489
this.layout(newHeight + inputPartHeight + chatSuggestNextWidgetHeight, width);
2490
});
2491
}));
2492
}
2493
2494
updateDynamicChatTreeItemLayout(numOfChatTreeItems: number, maxHeight: number) {
2495
this._dynamicMessageLayoutData = { numOfMessages: numOfChatTreeItems, maxHeight, enabled: true };
2496
let hasChanged = false;
2497
let height = this.bodyDimension!.height;
2498
let width = this.bodyDimension!.width;
2499
if (maxHeight < this.bodyDimension!.height) {
2500
height = maxHeight;
2501
hasChanged = true;
2502
}
2503
const containerWidth = this.container.offsetWidth;
2504
if (this.bodyDimension?.width !== containerWidth) {
2505
width = containerWidth;
2506
hasChanged = true;
2507
}
2508
if (hasChanged) {
2509
this.layout(height, width);
2510
}
2511
}
2512
2513
get isDynamicChatTreeItemLayoutEnabled(): boolean {
2514
return this._dynamicMessageLayoutData?.enabled ?? false;
2515
}
2516
2517
set isDynamicChatTreeItemLayoutEnabled(value: boolean) {
2518
if (!this._dynamicMessageLayoutData) {
2519
return;
2520
}
2521
this._dynamicMessageLayoutData.enabled = value;
2522
}
2523
2524
layoutDynamicChatTreeItemMode(): void {
2525
if (!this.viewModel || !this._dynamicMessageLayoutData?.enabled) {
2526
return;
2527
}
2528
2529
const width = this.bodyDimension?.width ?? this.container.offsetWidth;
2530
this.input.layout(this._dynamicMessageLayoutData.maxHeight, width);
2531
const inputHeight = this.input.inputPartHeight;
2532
const chatSuggestNextWidgetHeight = this.chatSuggestNextWidget.height;
2533
2534
const totalMessages = this.viewModel.getItems();
2535
// grab the last N messages
2536
const messages = totalMessages.slice(-this._dynamicMessageLayoutData.numOfMessages);
2537
2538
const needsRerender = messages.some(m => m.currentRenderedHeight === undefined);
2539
const listHeight = needsRerender
2540
? this._dynamicMessageLayoutData.maxHeight
2541
: messages.reduce((acc, message) => acc + message.currentRenderedHeight!, 0);
2542
2543
this.layout(
2544
Math.min(
2545
// we add an additional 18px in order to show that there is scrollable content
2546
inputHeight + chatSuggestNextWidgetHeight + listHeight + (totalMessages.length > 2 ? 18 : 0),
2547
this._dynamicMessageLayoutData.maxHeight
2548
),
2549
width
2550
);
2551
2552
if (needsRerender || !listHeight) {
2553
this.scrollToEnd();
2554
}
2555
}
2556
2557
saveState(): void {
2558
// no-op
2559
}
2560
2561
getViewState(): IChatModelInputState | undefined {
2562
return this.input.getCurrentInputState();
2563
}
2564
2565
private updateChatInputContext() {
2566
const currentAgent = this.parsedInput.parts.find(part => part instanceof ChatRequestAgentPart);
2567
this.agentInInput.set(!!currentAgent);
2568
}
2569
2570
private async _switchToAgentByName(agentName: string): Promise<void> {
2571
const currentAgent = this.input.currentModeObs.get();
2572
2573
// switch to appropriate agent if needed
2574
if (agentName !== currentAgent.name.get()) {
2575
// Find the mode object to get its kind
2576
const agent = this.chatModeService.findModeByName(agentName);
2577
if (agent) {
2578
if (currentAgent.kind !== agent.kind) {
2579
const chatModeCheck = await this.instantiationService.invokeFunction(handleModeSwitch, currentAgent.kind, agent.kind, this.viewModel?.model.getRequests().length ?? 0, this.viewModel?.model);
2580
if (!chatModeCheck) {
2581
return;
2582
}
2583
2584
if (chatModeCheck.needToClearSession) {
2585
await this.clear();
2586
}
2587
}
2588
this.input.setChatMode(agent.id);
2589
}
2590
}
2591
}
2592
2593
private async _applyPromptMetadata({ agent, tools, model }: PromptHeader, requestInput: IChatRequestInputOptions): Promise<void> {
2594
2595
if (tools !== undefined && !agent && this.input.currentModeKind !== ChatModeKind.Agent) {
2596
agent = ChatMode.Agent.name.get();
2597
}
2598
// switch to appropriate agent if needed
2599
if (agent) {
2600
this._switchToAgentByName(agent);
2601
}
2602
2603
// if not tools to enable are present, we are done
2604
if (tools !== undefined && this.input.currentModeKind === ChatModeKind.Agent) {
2605
const enablementMap = this.toolsService.toToolAndToolSetEnablementMap(tools, Target.VSCode);
2606
this.input.selectedToolsModel.set(enablementMap, true);
2607
}
2608
2609
if (model !== undefined) {
2610
this.input.switchModelByQualifiedName(model);
2611
}
2612
}
2613
2614
/**
2615
* Adds additional instructions to the context
2616
* - instructions that have a 'applyTo' pattern that matches the current input
2617
* - instructions referenced in the copilot settings 'copilot-instructions'
2618
* - instructions referenced in an already included instruction file
2619
*/
2620
private async _autoAttachInstructions({ attachedContext }: IChatRequestInputOptions): Promise<void> {
2621
this.logService.debug(`ChatWidget#_autoAttachInstructions: prompt files are always enabled`);
2622
const enabledTools = this.input.currentModeKind === ChatModeKind.Agent ? this.input.selectedToolsModel.entriesMap.get() : undefined;
2623
2624
const computer = this.instantiationService.createInstance(ComputeAutomaticInstructions, enabledTools);
2625
await computer.collect(attachedContext, CancellationToken.None);
2626
}
2627
2628
delegateScrollFromMouseWheelEvent(browserEvent: IMouseWheelEvent): void {
2629
this.tree.delegateScrollFromMouseWheelEvent(browserEvent);
2630
}
2631
}
2632
2633