Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/common/model/chatViewModel.ts
4780 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import { Codicon } from '../../../../../base/common/codicons.js';
7
import { Emitter, Event } from '../../../../../base/common/event.js';
8
import { hash } from '../../../../../base/common/hash.js';
9
import { IMarkdownString } from '../../../../../base/common/htmlContent.js';
10
import { Disposable, dispose } from '../../../../../base/common/lifecycle.js';
11
import * as marked from '../../../../../base/common/marked/marked.js';
12
import { IObservable } from '../../../../../base/common/observable.js';
13
import { ThemeIcon } from '../../../../../base/common/themables.js';
14
import { URI } from '../../../../../base/common/uri.js';
15
import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
16
import { IChatRequestVariableEntry } from '../attachments/chatVariableEntries.js';
17
import { ChatAgentVoteDirection, ChatAgentVoteDownReason, IChatCodeCitation, IChatContentReference, IChatFollowup, IChatMcpServersStarting, IChatProgressMessage, IChatResponseErrorDetails, IChatTask, IChatUsedContext } from '../chatService/chatService.js';
18
import { getFullyQualifiedId, IChatAgentCommand, IChatAgentData, IChatAgentNameService, IChatAgentResult } from '../participants/chatAgents.js';
19
import { IParsedChatRequest } from '../requestParser/chatParserTypes.js';
20
import { annotateVulnerabilitiesInText } from '../widget/annotations.js';
21
import { CodeBlockModelCollection } from '../widget/codeBlockModelCollection.js';
22
import { IChatModel, IChatProgressRenderableResponseContent, IChatRequestDisablement, IChatRequestModel, IChatResponseModel, IChatTextEditGroup, IResponse } from './chatModel.js';
23
import { ChatStreamStatsTracker, IChatStreamStats } from './chatStreamStats.js';
24
import { countWords } from './chatWordCounter.js';
25
26
export function isRequestVM(item: unknown): item is IChatRequestViewModel {
27
return !!item && typeof item === 'object' && 'message' in item;
28
}
29
30
export function isResponseVM(item: unknown): item is IChatResponseViewModel {
31
return !!item && typeof (item as IChatResponseViewModel).setVote !== 'undefined';
32
}
33
34
export function isChatTreeItem(item: unknown): item is IChatRequestViewModel | IChatResponseViewModel {
35
return isRequestVM(item) || isResponseVM(item);
36
}
37
38
export function assertIsResponseVM(item: unknown): asserts item is IChatResponseViewModel {
39
if (!isResponseVM(item)) {
40
throw new Error('Expected item to be IChatResponseViewModel');
41
}
42
}
43
44
export type IChatViewModelChangeEvent = IChatAddRequestEvent | IChangePlaceholderEvent | IChatSessionInitEvent | IChatSetHiddenEvent | IChatSetCheckpointEvent | null;
45
46
export interface IChatAddRequestEvent {
47
kind: 'addRequest';
48
}
49
50
export interface IChangePlaceholderEvent {
51
kind: 'changePlaceholder';
52
}
53
54
export interface IChatSessionInitEvent {
55
kind: 'initialize';
56
}
57
58
export interface IChatSetHiddenEvent {
59
kind: 'setHidden';
60
}
61
62
export interface IChatSetCheckpointEvent {
63
kind: 'setCheckpoint';
64
}
65
66
export interface IChatViewModel {
67
readonly model: IChatModel;
68
readonly sessionResource: URI;
69
readonly onDidDisposeModel: Event<void>;
70
readonly onDidChange: Event<IChatViewModelChangeEvent>;
71
readonly inputPlaceholder?: string;
72
getItems(): (IChatRequestViewModel | IChatResponseViewModel)[];
73
setInputPlaceholder(text: string): void;
74
resetInputPlaceholder(): void;
75
editing?: IChatRequestViewModel;
76
setEditing(editing: IChatRequestViewModel): void;
77
}
78
79
export interface IChatRequestViewModel {
80
readonly id: string;
81
/** @deprecated */
82
readonly sessionId: string;
83
readonly sessionResource: URI;
84
/** This ID updates every time the underlying data changes */
85
readonly dataId: string;
86
readonly username: string;
87
readonly avatarIcon?: URI | ThemeIcon;
88
readonly message: IParsedChatRequest | IChatFollowup;
89
readonly messageText: string;
90
readonly attempt: number;
91
readonly variables: readonly IChatRequestVariableEntry[];
92
currentRenderedHeight: number | undefined;
93
readonly contentReferences?: ReadonlyArray<IChatContentReference>;
94
readonly confirmation?: string;
95
readonly shouldBeRemovedOnSend: IChatRequestDisablement | undefined;
96
readonly isComplete: boolean;
97
readonly isCompleteAddedRequest: boolean;
98
readonly slashCommand: IChatAgentCommand | undefined;
99
readonly agentOrSlashCommandDetected: boolean;
100
readonly shouldBeBlocked: IObservable<boolean>;
101
readonly modelId?: string;
102
}
103
104
export interface IChatResponseMarkdownRenderData {
105
renderedWordCount: number;
106
lastRenderTime: number;
107
isFullyRendered: boolean;
108
originalMarkdown: IMarkdownString;
109
}
110
111
export interface IChatResponseMarkdownRenderData2 {
112
renderedWordCount: number;
113
lastRenderTime: number;
114
isFullyRendered: boolean;
115
originalMarkdown: IMarkdownString;
116
}
117
118
export interface IChatProgressMessageRenderData {
119
progressMessage: IChatProgressMessage;
120
121
/**
122
* Indicates whether this is part of a group of progress messages that are at the end of the response.
123
* (Not whether this particular item is the very last one in the response).
124
* Need to re-render and add to partsToRender when this changes.
125
*/
126
isAtEndOfResponse: boolean;
127
128
/**
129
* Whether this progress message the very last item in the response.
130
* Need to re-render to update spinner vs check when this changes.
131
*/
132
isLast: boolean;
133
}
134
135
export interface IChatTaskRenderData {
136
task: IChatTask;
137
isSettled: boolean;
138
progressLength: number;
139
}
140
141
export interface IChatResponseRenderData {
142
renderedParts: IChatRendererContent[];
143
144
renderedWordCount: number;
145
lastRenderTime: number;
146
}
147
148
/**
149
* Content type for references used during rendering, not in the model
150
*/
151
export interface IChatReferences {
152
references: ReadonlyArray<IChatContentReference>;
153
kind: 'references';
154
}
155
156
/**
157
* Content type for the "Working" progress message
158
*/
159
export interface IChatWorkingProgress {
160
kind: 'working';
161
}
162
163
164
/**
165
* Content type for citations used during rendering, not in the model
166
*/
167
export interface IChatCodeCitations {
168
citations: ReadonlyArray<IChatCodeCitation>;
169
kind: 'codeCitations';
170
}
171
172
export interface IChatErrorDetailsPart {
173
kind: 'errorDetails';
174
errorDetails: IChatResponseErrorDetails;
175
isLast: boolean;
176
}
177
178
export interface IChatChangesSummaryPart {
179
readonly kind: 'changesSummary';
180
readonly requestId: string;
181
readonly sessionResource: URI;
182
}
183
184
/**
185
* Type for content parts rendered by IChatListRenderer (not necessarily in the model)
186
*/
187
export type IChatRendererContent = IChatProgressRenderableResponseContent | IChatReferences | IChatCodeCitations | IChatErrorDetailsPart | IChatChangesSummaryPart | IChatWorkingProgress | IChatMcpServersStarting;
188
189
export interface IChatResponseViewModel {
190
readonly model: IChatResponseModel;
191
readonly id: string;
192
readonly session: IChatViewModel;
193
/** @deprecated */
194
readonly sessionId: string;
195
readonly sessionResource: URI;
196
/** This ID updates every time the underlying data changes */
197
readonly dataId: string;
198
/** The ID of the associated IChatRequestViewModel */
199
readonly requestId: string;
200
readonly username: string;
201
readonly avatarIcon?: URI | ThemeIcon;
202
readonly agent?: IChatAgentData;
203
readonly slashCommand?: IChatAgentCommand;
204
readonly agentOrSlashCommandDetected: boolean;
205
readonly response: IResponse;
206
readonly usedContext: IChatUsedContext | undefined;
207
readonly contentReferences: ReadonlyArray<IChatContentReference>;
208
readonly codeCitations: ReadonlyArray<IChatCodeCitation>;
209
readonly progressMessages: ReadonlyArray<IChatProgressMessage>;
210
readonly isComplete: boolean;
211
readonly isCanceled: boolean;
212
readonly isStale: boolean;
213
readonly vote: ChatAgentVoteDirection | undefined;
214
readonly voteDownReason: ChatAgentVoteDownReason | undefined;
215
readonly replyFollowups?: IChatFollowup[];
216
readonly errorDetails?: IChatResponseErrorDetails;
217
readonly result?: IChatAgentResult;
218
readonly contentUpdateTimings?: IChatStreamStats;
219
readonly shouldBeRemovedOnSend: IChatRequestDisablement | undefined;
220
readonly isCompleteAddedRequest: boolean;
221
renderData?: IChatResponseRenderData;
222
currentRenderedHeight: number | undefined;
223
setVote(vote: ChatAgentVoteDirection): void;
224
setVoteDownReason(reason: ChatAgentVoteDownReason | undefined): void;
225
usedReferencesExpanded?: boolean;
226
vulnerabilitiesListExpanded: boolean;
227
setEditApplied(edit: IChatTextEditGroup, editCount: number): void;
228
readonly shouldBeBlocked: IObservable<boolean>;
229
}
230
231
export class ChatViewModel extends Disposable implements IChatViewModel {
232
233
private readonly _onDidDisposeModel = this._register(new Emitter<void>());
234
readonly onDidDisposeModel = this._onDidDisposeModel.event;
235
236
private readonly _onDidChange = this._register(new Emitter<IChatViewModelChangeEvent>());
237
readonly onDidChange = this._onDidChange.event;
238
239
private readonly _items: (ChatRequestViewModel | ChatResponseViewModel)[] = [];
240
241
private _inputPlaceholder: string | undefined = undefined;
242
get inputPlaceholder(): string | undefined {
243
return this._inputPlaceholder;
244
}
245
246
get model(): IChatModel {
247
return this._model;
248
}
249
250
setInputPlaceholder(text: string): void {
251
this._inputPlaceholder = text;
252
this._onDidChange.fire({ kind: 'changePlaceholder' });
253
}
254
255
resetInputPlaceholder(): void {
256
this._inputPlaceholder = undefined;
257
this._onDidChange.fire({ kind: 'changePlaceholder' });
258
}
259
260
get sessionResource(): URI {
261
return this._model.sessionResource;
262
}
263
264
constructor(
265
private readonly _model: IChatModel,
266
public readonly codeBlockModelCollection: CodeBlockModelCollection,
267
@IInstantiationService private readonly instantiationService: IInstantiationService,
268
) {
269
super();
270
271
_model.getRequests().forEach((request, i) => {
272
const requestModel = this.instantiationService.createInstance(ChatRequestViewModel, request);
273
this._items.push(requestModel);
274
this.updateCodeBlockTextModels(requestModel);
275
276
if (request.response) {
277
this.onAddResponse(request.response);
278
}
279
});
280
281
this._register(_model.onDidDispose(() => this._onDidDisposeModel.fire()));
282
this._register(_model.onDidChange(e => {
283
if (e.kind === 'addRequest') {
284
const requestModel = this.instantiationService.createInstance(ChatRequestViewModel, e.request);
285
this._items.push(requestModel);
286
this.updateCodeBlockTextModels(requestModel);
287
288
if (e.request.response) {
289
this.onAddResponse(e.request.response);
290
}
291
} else if (e.kind === 'addResponse') {
292
this.onAddResponse(e.response);
293
} else if (e.kind === 'removeRequest') {
294
const requestIdx = this._items.findIndex(item => isRequestVM(item) && item.id === e.requestId);
295
if (requestIdx >= 0) {
296
this._items.splice(requestIdx, 1);
297
}
298
299
const responseIdx = e.responseId && this._items.findIndex(item => isResponseVM(item) && item.id === e.responseId);
300
if (typeof responseIdx === 'number' && responseIdx >= 0) {
301
const items = this._items.splice(responseIdx, 1);
302
const item = items[0];
303
if (item instanceof ChatResponseViewModel) {
304
item.dispose();
305
}
306
}
307
}
308
309
const modelEventToVmEvent: IChatViewModelChangeEvent =
310
e.kind === 'addRequest' ? { kind: 'addRequest' }
311
: e.kind === 'initialize' ? { kind: 'initialize' }
312
: e.kind === 'setHidden' ? { kind: 'setHidden' }
313
: null;
314
this._onDidChange.fire(modelEventToVmEvent);
315
}));
316
}
317
318
private onAddResponse(responseModel: IChatResponseModel) {
319
const response = this.instantiationService.createInstance(ChatResponseViewModel, responseModel, this);
320
this._register(response.onDidChange(() => {
321
if (response.isComplete) {
322
this.updateCodeBlockTextModels(response);
323
}
324
return this._onDidChange.fire(null);
325
}));
326
this._items.push(response);
327
this.updateCodeBlockTextModels(response);
328
}
329
330
getItems(): (IChatRequestViewModel | IChatResponseViewModel)[] {
331
return this._items.filter((item) => !item.shouldBeRemovedOnSend || item.shouldBeRemovedOnSend.afterUndoStop);
332
}
333
334
335
private _editing: IChatRequestViewModel | undefined = undefined;
336
get editing(): IChatRequestViewModel | undefined {
337
return this._editing;
338
}
339
340
setEditing(editing: IChatRequestViewModel | undefined): void {
341
if (this.editing && editing && this.editing.id === editing.id) {
342
return; // already editing this request
343
}
344
345
this._editing = editing;
346
}
347
348
override dispose() {
349
super.dispose();
350
dispose(this._items.filter((item): item is ChatResponseViewModel => item instanceof ChatResponseViewModel));
351
}
352
353
updateCodeBlockTextModels(model: IChatRequestViewModel | IChatResponseViewModel) {
354
let content: string;
355
if (isRequestVM(model)) {
356
content = model.messageText;
357
} else {
358
content = annotateVulnerabilitiesInText(model.response.value).map(x => x.content.value).join('');
359
}
360
361
let codeBlockIndex = 0;
362
marked.walkTokens(marked.lexer(content), token => {
363
if (token.type === 'code') {
364
const lang = token.lang || '';
365
const text = token.text;
366
this.codeBlockModelCollection.update(this._model.sessionResource, model, codeBlockIndex++, { text, languageId: lang, isComplete: true });
367
}
368
});
369
}
370
}
371
372
const variablesHash = new WeakMap<readonly IChatRequestVariableEntry[], number>();
373
374
export class ChatRequestViewModel implements IChatRequestViewModel {
375
get id() {
376
return this._model.id;
377
}
378
379
get dataId() {
380
let varsHash = variablesHash.get(this.variables);
381
if (typeof varsHash !== 'number') {
382
varsHash = hash(this.variables);
383
variablesHash.set(this.variables, varsHash);
384
}
385
386
return `${this.id}_${this.isComplete ? '1' : '0'}_${varsHash}`;
387
}
388
389
/** @deprecated */
390
get sessionId() {
391
return this._model.session.sessionId;
392
}
393
394
get sessionResource() {
395
return this._model.session.sessionResource;
396
}
397
398
get username() {
399
return 'User';
400
}
401
402
get avatarIcon(): ThemeIcon {
403
return Codicon.account;
404
}
405
406
get message() {
407
return this._model.message;
408
}
409
410
get messageText() {
411
return this.message.text;
412
}
413
414
get attempt() {
415
return this._model.attempt;
416
}
417
418
get variables() {
419
return this._model.variableData.variables;
420
}
421
422
get contentReferences() {
423
return this._model.response?.contentReferences;
424
}
425
426
get confirmation() {
427
return this._model.confirmation;
428
}
429
430
get isComplete() {
431
return this._model.response?.isComplete ?? false;
432
}
433
434
get isCompleteAddedRequest() {
435
return this._model.isCompleteAddedRequest;
436
}
437
438
get shouldBeRemovedOnSend() {
439
return this._model.shouldBeRemovedOnSend;
440
}
441
442
get shouldBeBlocked() {
443
return this._model.shouldBeBlocked;
444
}
445
446
get slashCommand(): IChatAgentCommand | undefined {
447
return this._model.response?.slashCommand;
448
}
449
450
get agentOrSlashCommandDetected(): boolean {
451
return this._model.response?.agentOrSlashCommandDetected ?? false;
452
}
453
454
currentRenderedHeight: number | undefined;
455
456
get modelId() {
457
return this._model.modelId;
458
}
459
460
constructor(
461
private readonly _model: IChatRequestModel,
462
) { }
463
}
464
465
export class ChatResponseViewModel extends Disposable implements IChatResponseViewModel {
466
private _modelChangeCount = 0;
467
468
private readonly _onDidChange = this._register(new Emitter<void>());
469
readonly onDidChange = this._onDidChange.event;
470
471
get model() {
472
return this._model;
473
}
474
475
get id() {
476
return this._model.id;
477
}
478
479
get dataId() {
480
return this._model.id +
481
`_${this._modelChangeCount}` +
482
(this.isLast ? '_last' : '');
483
}
484
485
/** @deprecated */
486
get sessionId() {
487
return this._model.session.sessionId;
488
}
489
490
get sessionResource(): URI {
491
return this._model.session.sessionResource;
492
}
493
494
get username() {
495
if (this.agent) {
496
const isAllowed = this.chatAgentNameService.getAgentNameRestriction(this.agent);
497
if (isAllowed) {
498
return this.agent.fullName || this.agent.name;
499
} else {
500
return getFullyQualifiedId(this.agent);
501
}
502
}
503
504
return this._model.username;
505
}
506
507
get avatarIcon() {
508
return this._model.avatarIcon;
509
}
510
511
get agent() {
512
return this._model.agent;
513
}
514
515
get slashCommand() {
516
return this._model.slashCommand;
517
}
518
519
get agentOrSlashCommandDetected() {
520
return this._model.agentOrSlashCommandDetected;
521
}
522
523
get response(): IResponse {
524
return this._model.response;
525
}
526
527
get usedContext(): IChatUsedContext | undefined {
528
return this._model.usedContext;
529
}
530
531
get contentReferences(): ReadonlyArray<IChatContentReference> {
532
return this._model.contentReferences;
533
}
534
535
get codeCitations(): ReadonlyArray<IChatCodeCitation> {
536
return this._model.codeCitations;
537
}
538
539
get progressMessages(): ReadonlyArray<IChatProgressMessage> {
540
return this._model.progressMessages;
541
}
542
543
get isComplete() {
544
return this._model.isComplete;
545
}
546
547
get isCanceled() {
548
return this._model.isCanceled;
549
}
550
551
get shouldBeBlocked() {
552
return this._model.shouldBeBlocked;
553
}
554
555
get shouldBeRemovedOnSend() {
556
return this._model.shouldBeRemovedOnSend;
557
}
558
559
get isCompleteAddedRequest() {
560
return this._model.isCompleteAddedRequest;
561
}
562
563
get replyFollowups() {
564
return this._model.followups?.filter((f): f is IChatFollowup => f.kind === 'reply');
565
}
566
567
get result() {
568
return this._model.result;
569
}
570
571
get errorDetails(): IChatResponseErrorDetails | undefined {
572
return this.result?.errorDetails;
573
}
574
575
get vote() {
576
return this._model.vote;
577
}
578
579
get voteDownReason() {
580
return this._model.voteDownReason;
581
}
582
583
get requestId() {
584
return this._model.requestId;
585
}
586
587
get isStale() {
588
return this._model.isStale;
589
}
590
591
get isLast(): boolean {
592
return this.session.getItems().at(-1) === this;
593
}
594
595
renderData: IChatResponseRenderData | undefined = undefined;
596
currentRenderedHeight: number | undefined;
597
598
private _usedReferencesExpanded: boolean | undefined;
599
get usedReferencesExpanded(): boolean | undefined {
600
if (typeof this._usedReferencesExpanded === 'boolean') {
601
return this._usedReferencesExpanded;
602
}
603
604
return undefined;
605
}
606
607
set usedReferencesExpanded(v: boolean) {
608
this._usedReferencesExpanded = v;
609
}
610
611
private _vulnerabilitiesListExpanded: boolean = false;
612
get vulnerabilitiesListExpanded(): boolean {
613
return this._vulnerabilitiesListExpanded;
614
}
615
616
set vulnerabilitiesListExpanded(v: boolean) {
617
this._vulnerabilitiesListExpanded = v;
618
}
619
620
private readonly liveUpdateTracker: ChatStreamStatsTracker | undefined;
621
622
get contentUpdateTimings(): IChatStreamStats | undefined {
623
return this.liveUpdateTracker?.data;
624
}
625
626
constructor(
627
private readonly _model: IChatResponseModel,
628
public readonly session: IChatViewModel,
629
@IInstantiationService private readonly instantiationService: IInstantiationService,
630
@IChatAgentNameService private readonly chatAgentNameService: IChatAgentNameService,
631
) {
632
super();
633
634
if (!_model.isComplete) {
635
this.liveUpdateTracker = this.instantiationService.createInstance(ChatStreamStatsTracker);
636
}
637
638
this._register(_model.onDidChange(() => {
639
if (this.liveUpdateTracker) {
640
const wordCount = countWords(_model.entireResponse.getMarkdown());
641
this.liveUpdateTracker.update({ totalWordCount: wordCount });
642
}
643
644
// new data -> new id, new content to render
645
this._modelChangeCount++;
646
647
this._onDidChange.fire();
648
}));
649
}
650
651
setVote(vote: ChatAgentVoteDirection): void {
652
this._modelChangeCount++;
653
this._model.setVote(vote);
654
}
655
656
setVoteDownReason(reason: ChatAgentVoteDownReason | undefined): void {
657
this._modelChangeCount++;
658
this._model.setVoteDownReason(reason);
659
}
660
661
setEditApplied(edit: IChatTextEditGroup, editCount: number) {
662
this._modelChangeCount++;
663
this._model.setEditApplied(edit, editCount);
664
}
665
}
666
667