chatViewModel.ts ×80

Frontier kind: Code frontier

unlabeled · c_c1e03711da58

31 tests · 33451 LOC · 154 files · introduces 0 tests · 450 LOC · 1 file

Introduces — evidence that enters the hierarchy at this concept

Code
80 ranges450 lines · 1 files
Tests
0 tests

Contains — complete concept membership

All code (extent)
3048 ranges33451 lines · 154 files · Browse complete extent
All tests (intent)
31 testsBrowse complete intent

Neighbourhood graph

The orange circle is the focus. Violet and green circles are every ancestor and descendant, broader and narrower, at any distance; blue squares and pink diamonds are the introduced files and exact introduced tests of every visible concept, not only the focus's. Arrows point from broader to narrower concepts and bridge only concepts omitted from this view. Undirected links show source or test introduction. Concept and file size follows LOC; exact test nodes use test-count units.

Introduced files, introduced tests, and structurally relevant concept specialization

In the embedded map, ordinary wheel input scrolls the page; use the visible controls to zoom and drag to pan. Open the full-screen map for canvas navigation: wheel pans, Ctrl/Command plus wheel zooms, and arrow keys pan when this region is focused. On touch screens, open the full-screen map to pan or pinch. If JavaScript or WebGL is unavailable, use the native relationship evidence on this page.

Graph controls are ready.

Interactive rendering requires JavaScript and WebGL. Use the native relationship evidence on this page while the interactive map is unavailable.

Native relationship evidence

Every exact file and test below is linked only from the concept that introduces it.

Introduced tests

Every collected test enters the hierarchy at exactly one concept.

No tests are introduced at this concept. Its intent tests are introduced by other concepts.

Introduced code

Every collected source range enters the hierarchy at exactly one concept.

1 file ranked by introduced lines: 450 introduced LOC across 80 ranges. Expand a file to inspect source; the > gutter marks introduced lines.

src/vs/workbench/contrib/chat/common/model/chatViewModel.ts 450 introduced LOC · 80 ranges

Open complete file

1 > /*--------------------------------------------------------------------------------------------- chatViewModel.ts
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 { IMarkdownString } from '../../../../../base/common/htmlContent.js';
9 > import { Disposable, dispose } from '../../../../../base/common/lifecycle.js';
10 > import { RunOnceScheduler } from '../../../../../base/common/async.js';
11 > import { IObservable } from '../../../../../base/common/observable.js';
12 > import { ThemeIcon } from '../../../../../base/common/themables.js';
13 > import { URI } from '../../../../../base/common/uri.js';
14 > import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';
15 > import { IChatRequestVariableEntry } from '../attachments/chatVariableEntries.js';
16 > import { ChatAgentVoteDirection, ChatRequestQueueKind, IChatCodeCitation, IChatContentReference, IChatDisabledClaudeHooksPart, IChatFollowup, IChatMcpAuthenticationRequired, IChatMcpServersStarting, IChatMcpServersStartingSlow, IChatPlanReview, IChatProgressMessage, IChatQuestionCarousel, IChatResponseErrorDetails, IChatTask, IChatUsage, IChatUsedContext } from '../chatService/chatService.js';
17 > import { getFullyQualifiedId, IChatAgentCommand, IChatAgentData, IChatAgentNameService, IChatAgentResult } from '../participants/chatAgents.js';
18 > import { IParsedChatRequest } from '../requestParser/chatParserTypes.js';
19 > import { IChatModel, IChatProgressRenderableResponseContent, IChatRequestDisablement, IChatRequestModel, IChatResponseModel, IChatTextEditGroup, IResponse } from './chatModel.js';
20 > import { ChatStreamStatsTracker, IChatStreamStats } from './chatStreamStats.js';
21 > import { countWords } from './chatWordCounter.js';
22 >
23 > export function isRequestVM(item: unknown): item is IChatRequestViewModel {
24 return !!item && typeof item === 'object' && 'message' in item;
25 }
27 > export function isResponseVM(item: unknown): item is IChatResponseViewModel {
28 return !!item && typeof (item as IChatResponseViewModel).setVote !== 'undefined';
29 }
31 > export function isPendingDividerVM(item: unknown): item is IChatPendingDividerViewModel {
32 return !!item && typeof item === 'object' && (item as IChatPendingDividerViewModel).kind === 'pendingDivider';
33 }
35 > interface IChatViewModelItemWithPendingState {
36 > readonly id: string;
37 > readonly kind?: string;
38 > readonly pendingKind?: ChatRequestQueueKind;
39 > }
40 >
41 function isPendingChatViewModelItem(item: IChatViewModelItemWithPendingState): boolean {
42 return item.kind === 'pendingDivider' || item.pendingKind !== undefined;
43 }
45 > /**
46 > * The active response that content streams into: the last non-pending item, ignoring
47 > * trailing queued/steering rows (and their dividers). Falls back to the last item when
48 > * everything is pending.
49 > */
50 > export function getStickyScrollTargetItem<T extends IChatViewModelItemWithPendingState>(items: readonly T[]): T | undefined {
51 for (let i = items.length - 1; i >= 0; i--) {
52 const item = items[i];
57 return items.at(-1);
58 }
60 > export function isChatTreeItem(item: unknown): item is IChatRequestViewModel | IChatResponseViewModel {
61 return isRequestVM(item) || isResponseVM(item);
62 }
64 > export function assertIsResponseVM(item: unknown): asserts item is IChatResponseViewModel {
65 if (!isResponseVM(item)) {
66 throw new Error('Expected item to be IChatResponseViewModel');
67 }
68 }
70 > export type IChatViewModelChangeEvent = IChatAddRequestEvent | IChangePlaceholderEvent | IChatSessionInitEvent | IChatSetHiddenEvent | null;
71 >
72 > export interface IChatAddRequestEvent {
73 > kind: 'addRequest';
74 > }
75 >
76 > export interface IChangePlaceholderEvent {
77 > kind: 'changePlaceholder';
78 > }
79 >
80 > export interface IChatSessionInitEvent {
81 > kind: 'initialize';
82 > }
83 >
84 > export interface IChatSetHiddenEvent {
85 > kind: 'setHidden';
86 > }
87 >
88 > export interface IChatViewModel {
89 > readonly model: IChatModel;
90 > readonly sessionResource: URI;
91 > readonly onDidDisposeModel: Event<void>;
92 > readonly onDidChange: Event<IChatViewModelChangeEvent>;
93 > readonly inputPlaceholder?: string;
94 > getItems(): (IChatRequestViewModel | IChatResponseViewModel | IChatPendingDividerViewModel)[];
95 > setInputPlaceholder(text: string): void;
96 > resetInputPlaceholder(): void;
97 > editing?: IChatRequestViewModel;
98 > setEditing(editing: IChatRequestViewModel): void;
99 > }
100 >
101 > export interface IChatRequestViewModel {
102 > readonly id: string;
103 > readonly sessionResource: URI;
104 > /** This ID updates every time the underlying data changes */
105 > readonly dataId: string;
106 > readonly username: string;
107 > readonly avatarIcon?: URI | ThemeIcon;
108 > readonly message: IParsedChatRequest | IChatFollowup;
109 > readonly messageText: string;
110 > readonly attempt: number;
111 > readonly variables: readonly IChatRequestVariableEntry[];
112 > currentRenderedHeight: number | undefined;
113 > readonly contentReferences?: ReadonlyArray<IChatContentReference>;
114 > readonly confirmation?: string;
115 > readonly shouldBeRemovedOnSend: IChatRequestDisablement | undefined;
116 > readonly isComplete: boolean;
117 > readonly isCompleteAddedRequest: boolean;
118 > readonly isTerminalCommand: boolean;
119 > readonly slashCommand: IChatAgentCommand | undefined;
120 > readonly agentOrSlashCommandDetected: boolean;
121 > readonly shouldBeBlocked: IObservable<boolean>;
122 > readonly attachedContext?: readonly IChatRequestVariableEntry[];
123 > readonly modelId?: string;
124 > readonly resolvedModelId?: string;
125 > readonly timestamp: number;
126 > readonly requestTimestamp: number | undefined;
127 > /** The kind of pending request, or undefined if not pending */
128 > readonly pendingKind?: ChatRequestQueueKind;
129 > readonly isSystemInitiated?: boolean;
130 > readonly systemInitiatedLabel?: string;
131 > }
132 >
133 > export interface IChatResponseMarkdownRenderData {
134 > renderedWordCount: number;
135 > lastRenderTime: number;
136 > isFullyRendered: boolean;
137 > originalMarkdown: IMarkdownString;
138 > }
139 >
140 > export interface IChatResponseMarkdownRenderData2 {
141 > renderedWordCount: number;
142 > lastRenderTime: number;
143 > isFullyRendered: boolean;
144 > originalMarkdown: IMarkdownString;
145 > }
146 >
147 > export interface IChatProgressMessageRenderData {
148 > progressMessage: IChatProgressMessage;
149 >
150 > /**
151 > * Indicates whether this is part of a group of progress messages that are at the end of the response.
152 > * (Not whether this particular item is the very last one in the response).
153 > * Need to re-render and add to partsToRender when this changes.
154 > */
155 > isAtEndOfResponse: boolean;
156 >
157 > /**
158 > * Whether this progress message the very last item in the response.
159 > * Need to re-render to update spinner vs check when this changes.
160 > */
161 > isLast: boolean;
162 > }
163 >
164 > export interface IChatTaskRenderData {
165 > task: IChatTask;
166 > isSettled: boolean;
167 > progressLength: number;
168 > }
169 >
170 > export interface IChatResponseRenderData {
171 > renderedParts: IChatRendererContent[];
172 >
173 > renderedWordCount: number;
174 > lastRenderTime: number;
175 > }
176 >
177 > /**
178 > * Content type for references used during rendering, not in the model
179 > */
180 > export interface IChatReferences {
181 > references: ReadonlyArray<IChatContentReference>;
182 > kind: 'references';
183 > }
184 >
185 > /**
186 > * Content type for the "Working" progress message
187 > */
188 > export interface IChatWorkingProgress {
189 > kind: 'working';
190 > content?: IMarkdownString;
191 > }
192 >
193 >
194 > /**
195 > * Content type for citations used during rendering, not in the model
196 > */
197 > export interface IChatCodeCitations {
198 > citations: ReadonlyArray<IChatCodeCitation>;
199 > kind: 'codeCitations';
200 > }
201 >
202 > export interface IChatErrorDetailsPart {
203 > kind: 'errorDetails';
204 > errorDetails: IChatResponseErrorDetails;
205 > isLast: boolean;
206 > }
207 >
208 > export interface IChatChangesSummaryPart {
209 > readonly kind: 'changesSummary';
210 > readonly requestId: string;
211 > readonly sessionResource: URI;
212 > }
213 >
214 > export interface IChatTurnPillsPart {
215 > readonly kind: 'turnPills';
216 > readonly requestId: string;
217 > readonly sessionResource: URI;
218 > }
219 >
220 > /**
221 > * Type for content parts rendered by IChatListRenderer (not necessarily in the model)
222 > */
223 > export type IChatRendererContent = IChatProgressRenderableResponseContent | IChatReferences | IChatCodeCitations | IChatErrorDetailsPart | IChatChangesSummaryPart | IChatWorkingProgress | IChatMcpServersStarting | IChatMcpAuthenticationRequired | IChatMcpServersStartingSlow | IChatQuestionCarousel | IChatPlanReview | IChatDisabledClaudeHooksPart | IChatTurnPillsPart;
224 >
225 > export interface IChatResponseViewModel {
226 > readonly model: IChatResponseModel;
227 > readonly id: string;
228 > readonly session: IChatViewModel;
229 > readonly sessionResource: URI;
230 > /** This ID updates every time the underlying data changes */
231 > readonly dataId: string;
232 > /** The ID of the associated IChatRequestViewModel */
233 > readonly requestId: string;
234 > readonly username: string;
235 > readonly agent?: IChatAgentData;
236 > readonly slashCommand?: IChatAgentCommand;
237 > readonly agentOrSlashCommandDetected: boolean;
238 > readonly response: IResponse;
239 > readonly usedContext: IChatUsedContext | undefined;
240 > readonly contentReferences: ReadonlyArray<IChatContentReference>;
241 > readonly codeCitations: ReadonlyArray<IChatCodeCitation>;
242 > readonly progressMessages: ReadonlyArray<IChatProgressMessage>;
243 > readonly isComplete: boolean;
244 > readonly isCanceled: boolean;
245 > readonly isStale: boolean;
246 > readonly vote: ChatAgentVoteDirection | undefined;
247 > readonly replyFollowups?: IChatFollowup[];
248 > readonly errorDetails?: IChatResponseErrorDetails;
249 > readonly result?: IChatAgentResult;
250 > readonly contentUpdateTimings?: IChatStreamStats;
251 > readonly confirmationAdjustedTimestamp: IObservable<number>;
252 > readonly usageObs: IObservable<IChatUsage | undefined>;
253 > readonly completionTokenCountObs: IObservable<number | undefined>;
254 > readonly shouldBeRemovedOnSend: IChatRequestDisablement | undefined;
255 > readonly isCompleteAddedRequest: boolean;
256 > readonly isTerminalCommand: boolean;
257 > renderData?: IChatResponseRenderData;
258 > currentRenderedHeight: number | undefined;
259 > setVote(vote: ChatAgentVoteDirection): void;
260 > usedReferencesExpanded?: boolean;
261 > vulnerabilitiesListExpanded: boolean;
262 > setEditApplied(edit: IChatTextEditGroup, editCount: number): void;
263 > readonly shouldBeBlocked: IObservable<boolean>;
264 > }
265 >
266 > export interface IChatPendingDividerViewModel {
267 > readonly kind: 'pendingDivider';
268 > readonly id: string; // e.g., 'pending-divider-steering' or 'pending-divider-queued'
269 > readonly sessionResource: URI;
270 > readonly isComplete: true;
271 > readonly dividerKind: ChatRequestQueueKind;
272 > readonly isSystemInitiated?: boolean;
273 > currentRenderedHeight: number | undefined;
274 > }
275 >
276 > export interface IChatViewModelOptions {
277 > /**
278 > * Maximum number of items to return from getItems().
279 > * When set, only the last N items are returned (most recent request/response pairs).
280 > */
281 > readonly maxVisibleItems?: number;
282 > }
283 >
284 > export class ChatViewModel extends Disposable implements IChatViewModel {
285 >
286 > private readonly _onDidDisposeModel = this._register(new Emitter<void>());
287 > readonly onDidDisposeModel = this._onDidDisposeModel.event;
288 >
289 > private readonly _onDidChange = this._register(new Emitter<IChatViewModelChangeEvent>());
290 > readonly onDidChange = this._onDidChange.event;
291 >
292 > private readonly _items: (ChatRequestViewModel | ChatResponseViewModel)[] = [];
293 >
294 > private _inputPlaceholder: string | undefined = undefined;
295 > get inputPlaceholder(): string | undefined {
296 > return this._inputPlaceholder;
297 > }
298 >
299 > get model(): IChatModel {
300 return this._model;
301 }
303 > setInputPlaceholder(text: string): void {
304 this._inputPlaceholder = text;
305 this._onDidChange.fire({ kind: 'changePlaceholder' });
306 }
308 > resetInputPlaceholder(): void {
309 this._inputPlaceholder = undefined;
310 this._onDidChange.fire({ kind: 'changePlaceholder' });
311 }
313 > get sessionResource(): URI {
314 return this._model.sessionResource;
315 }
317 > constructor(
318 private readonly _model: IChatModel,
319 private readonly _options: IChatViewModelOptions | undefined,
367 }));
368 }
370 > private onAddResponse(responseModel: IChatResponseModel) {
371 const response = this.instantiationService.createInstance(ChatResponseViewModel, responseModel, this);
372 this._register(response.onDidChange(() => {
375 this._items.push(response);
376 }
378 > getItems(): (IChatRequestViewModel | IChatResponseViewModel | IChatPendingDividerViewModel)[] {
379 let items: (IChatRequestViewModel | IChatResponseViewModel | IChatPendingDividerViewModel)[] = this._items.filter((item) => {
380 if (item.shouldBeRemovedOnSend && !item.shouldBeRemovedOnSend.afterUndoStop) {
415 return items;
416 }
418 >
419 > private _editing: IChatRequestViewModel | undefined = undefined;
420 > get editing(): IChatRequestViewModel | undefined {
421 return this._editing;
422 }
424 > setEditing(editing: IChatRequestViewModel | undefined): void {
425 if (this.editing && editing && this.editing.id === editing.id) {
426 return; // already editing this request
429 this._editing = editing;
430 }
432 > override dispose() {
433 super.dispose();
434 dispose(this._items.filter((item): item is ChatResponseViewModel => item instanceof ChatResponseViewModel));
435 this._items.length = 0;
436 }
438 >
439 > export class ChatRequestViewModel implements IChatRequestViewModel {
440 > get id() {
441 > return this._model.id;
442 > }
443 >
444 > /**
445 > * An ID that changes when the request should be re-rendered.
446 > */
447 > get dataId() {
448 return `${this.id}_${this._model.version + (this._model.response?.isComplete ? 1 : 0)}`;
449 }
451 > get sessionResource() {
452 return this._model.session.sessionResource;
453 }
455 > get username() {
456 return 'User';
457 }
459 > get avatarIcon(): ThemeIcon {
460 return Codicon.account;
461 }
463 > get message() {
464 return this._model.message;
465 }
467 > get messageText() {
468 return this.message.text;
469 }
471 > get attempt() {
472 return this._model.attempt;
473 }
475 > get variables() {
476 return this._model.variableData.variables;
477 }
479 > get contentReferences() {
480 return this._model.response?.contentReferences;
481 }
483 > get confirmation() {
484 return this._model.confirmation;
485 }
487 > get isComplete() {
488 return this._model.response?.isComplete ?? false;
489 }
491 > get isCompleteAddedRequest() {
492 return this._model.isCompleteAddedRequest;
493 }
495 > get isTerminalCommand() {
496 return this._model.isTerminalCommand;
497 }
499 > get shouldBeRemovedOnSend() {
500 return this._model.shouldBeRemovedOnSend;
501 }
503 > get shouldBeBlocked() {
504 return this._model.shouldBeBlocked;
505 }
507 > get slashCommand(): IChatAgentCommand | undefined {
508 return this._model.response?.slashCommand;
509 }
511 > get agentOrSlashCommandDetected(): boolean {
512 return this._model.response?.agentOrSlashCommandDetected ?? false;
513 }
515 > currentRenderedHeight: number | undefined;
516 >
517 > get attachedContext() {
518 return this._model.attachedContext;
519 }
521 > get modelId() {
522 return this._model.modelId;
523 }
525 > get resolvedModelId() {
526 const resolvedModel = this._model.response?.result?.metadata?.resolvedModel;
527 return typeof resolvedModel === 'string' ? resolvedModel : undefined;
528 }
530 > get timestamp() {
531 return this._model.timestamp;
532 }
534 > get requestTimestamp() {
535 return this._model.requestTimestamp;
536 }
538 > get pendingKind() {
539 return this._pendingKind;
540 }
542 > get isSystemInitiated() {
543 return this._model.isSystemInitiated;
544 }
546 > get systemInitiatedLabel() {
547 return this._model.systemInitiatedLabel;
548 }
550 > constructor(
551 private readonly _model: IChatRequestModel,
552 private readonly _pendingKind?: ChatRequestQueueKind,
553 ) { }
555 >
556 > export class ChatResponseViewModel extends Disposable implements IChatResponseViewModel {
557 > private _modelChangeCount = 0;
558 >
559 > private readonly _onDidChange = this._register(new Emitter<void>());
560 > readonly onDidChange = this._onDidChange.event;
561 >
562 > get model() {
563 > return this._model;
564 > }
565 >
566 > get id() {
567 return this._model.id;
568 }
570 > get dataId() {
571 return this._model.id +
572 `_${this._modelChangeCount}` +
573 (this.isLast ? '_last' : '');
574 }
576 > get sessionResource(): URI {
577 return this._model.session.sessionResource;
578 }
580 > get username() {
581 if (this.agent) {
582 const isAllowed = this.chatAgentNameService.getAgentNameRestriction(this.agent);
590 return this._model.username;
591 }
593 > get agent() {
594 return this._model.agent;
595 }
597 > get slashCommand() {
598 return this._model.slashCommand;
599 }
601 > get agentOrSlashCommandDetected() {
602 return this._model.agentOrSlashCommandDetected;
603 }
605 > get response(): IResponse {
606 return this._model.response;
607 }
609 > get usedContext(): IChatUsedContext | undefined {
610 return this._model.usedContext;
611 }
613 > get contentReferences(): ReadonlyArray<IChatContentReference> {
614 return this._model.contentReferences;
615 }
617 > get codeCitations(): ReadonlyArray<IChatCodeCitation> {
618 return this._model.codeCitations;
619 }
621 > get progressMessages(): ReadonlyArray<IChatProgressMessage> {
622 return this._model.progressMessages;
623 }
625 > get isComplete() {
626 return this._model.isComplete;
627 }
629 > get isCanceled() {
630 return this._model.isCanceled;
631 }
633 > get shouldBeBlocked() {
634 return this._model.shouldBeBlocked;
635 }
637 > get shouldBeRemovedOnSend() {
638 return this._model.shouldBeRemovedOnSend;
639 }
641 > get isCompleteAddedRequest() {
642 return this._model.isCompleteAddedRequest;
643 }
645 > get isTerminalCommand() {
646 return this._model.request?.isTerminalCommand ?? false;
647 }
649 > get replyFollowups() {
650 return this._model.followups?.filter((f): f is IChatFollowup => f.kind === 'reply');
651 }
653 > get result() {
654 return this._model.result;
655 }
657 > get errorDetails(): IChatResponseErrorDetails | undefined {
658 return this.result?.errorDetails;
659 }
661 > get vote() {
662 return this._model.vote;
663 }
665 > get requestId() {
666 return this._model.requestId;
667 }
669 > get isStale() {
670 return this._model.isStale;
671 }
673 > get isLast(): boolean {
674 // NOTE: this is used in `dataId` to force a re-render when the response transitions
675 // between being the last row and not, e.g. when a queued/steering row is added below
679 return this.session.getItems().at(-1) === this;
680 }
682 > renderData: IChatResponseRenderData | undefined = undefined;
683 > currentRenderedHeight: number | undefined;
684 >
685 > private _usedReferencesExpanded: boolean | undefined;
686 > get usedReferencesExpanded(): boolean | undefined {
687 if (typeof this._usedReferencesExpanded === 'boolean') {
688 return this._usedReferencesExpanded;
691 return undefined;
692 }
694 > set usedReferencesExpanded(v: boolean) {
695 this._usedReferencesExpanded = v;
696 }
698 > private _vulnerabilitiesListExpanded: boolean = false;
699 > get vulnerabilitiesListExpanded(): boolean {
700 return this._vulnerabilitiesListExpanded;
701 }
703 > set vulnerabilitiesListExpanded(v: boolean) {
704 this._vulnerabilitiesListExpanded = v;
705 }
707 > private readonly liveUpdateTracker: ChatStreamStatsTracker | undefined;
708 >
709 > get contentUpdateTimings(): IChatStreamStats | undefined {
710 return this.liveUpdateTracker?.data;
711 }
713 > get confirmationAdjustedTimestamp(): IObservable<number> {
714 return this._model.confirmationAdjustedTimestamp;
715 }
717 > get usageObs(): IObservable<IChatUsage | undefined> {
718 return this._model.usageObs;
719 }
721 > get completionTokenCountObs(): IObservable<number | undefined> {
722 return this._model.completionTokenCountObs;
723 }
725 > constructor(
726 private readonly _model: IChatResponseModel,
727 public readonly session: IChatViewModel,
749 }));
750 }
752 > setVote(vote: ChatAgentVoteDirection): void {
753 this._modelChangeCount++;
754 this._model.setVote(vote);
755 }
757 > setEditApplied(edit: IChatTextEditGroup, editCount: number) {
758 this._modelChangeCount++;
759 this._model.setEditApplied(edit, editCount);
760 }