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