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