Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/common/model/chatModel.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 { asArray } from '../../../../../base/common/arrays.js';
7
import { softAssertNever } from '../../../../../base/common/assert.js';
8
import { VSBuffer, decodeHex, encodeHex } from '../../../../../base/common/buffer.js';
9
import { BugIndicatingError } from '../../../../../base/common/errors.js';
10
import { Emitter, Event } from '../../../../../base/common/event.js';
11
import { IMarkdownString, MarkdownString, isMarkdownString } from '../../../../../base/common/htmlContent.js';
12
import { Disposable, IDisposable, MutableDisposable } from '../../../../../base/common/lifecycle.js';
13
import { ResourceMap } from '../../../../../base/common/map.js';
14
import { revive } from '../../../../../base/common/marshalling.js';
15
import { Schemas } from '../../../../../base/common/network.js';
16
import { equals } from '../../../../../base/common/objects.js';
17
import { IObservable, autorun, autorunSelfDisposable, derived, observableFromEvent, observableSignalFromEvent, observableValue, observableValueOpts } from '../../../../../base/common/observable.js';
18
import { basename, isEqual } from '../../../../../base/common/resources.js';
19
import { ThemeIcon } from '../../../../../base/common/themables.js';
20
import { WithDefinedProps } from '../../../../../base/common/types.js';
21
import { URI, UriComponents, UriDto, isUriComponents } from '../../../../../base/common/uri.js';
22
import { generateUuid } from '../../../../../base/common/uuid.js';
23
import { IRange } from '../../../../../editor/common/core/range.js';
24
import { OffsetRange } from '../../../../../editor/common/core/ranges/offsetRange.js';
25
import { ISelection } from '../../../../../editor/common/core/selection.js';
26
import { TextEdit } from '../../../../../editor/common/languages.js';
27
import { EditSuggestionId } from '../../../../../editor/common/textModelEditSource.js';
28
import { localize } from '../../../../../nls.js';
29
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
30
import { ILogService } from '../../../../../platform/log/common/log.js';
31
import { CellUri, ICellEditOperation } from '../../../notebook/common/notebookCommon.js';
32
import { ChatRequestToolReferenceEntry, IChatRequestVariableEntry } from '../attachments/chatVariableEntries.js';
33
import { migrateLegacyTerminalToolSpecificData } from '../chat.js';
34
import { ChatAgentVoteDirection, ChatAgentVoteDownReason, ChatResponseClearToPreviousToolInvocationReason, ElicitationState, IChatAgentMarkdownContentWithVulnerability, IChatClearToPreviousToolInvocation, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatEditingSessionAction, IChatElicitationRequest, IChatElicitationRequestSerialized, IChatExtensionsContent, IChatFollowup, IChatLocationData, IChatMarkdownContent, IChatMcpServersStarting, IChatModelReference, IChatMultiDiffData, IChatMultiDiffDataSerialized, IChatNotebookEdit, IChatPrepareToolInvocationPart, IChatProgress, IChatProgressMessage, IChatPullRequestContent, IChatResponseCodeblockUriPart, IChatResponseProgressFileTreeData, IChatService, IChatSessionContext, IChatSessionTiming, IChatTask, IChatTaskSerialized, IChatTextEdit, IChatThinkingPart, IChatToolInvocation, IChatToolInvocationSerialized, IChatTreeData, IChatUndoStop, IChatUsedContext, IChatWarningMessage, ResponseModelState, isIUsedContext } from '../chatService/chatService.js';
35
import { ChatAgentLocation, ChatModeKind } from '../constants.js';
36
import { IChatEditingService, IChatEditingSession, ModifiedFileEntryState, editEntriesToMultiDiffData } from '../editing/chatEditingService.js';
37
import { ILanguageModelChatMetadata, ILanguageModelChatMetadataAndIdentifier } from '../languageModels.js';
38
import { IChatAgentCommand, IChatAgentData, IChatAgentResult, IChatAgentService, UserSelectedTools, reviveSerializedAgent } from '../participants/chatAgents.js';
39
import { ChatRequestTextPart, IParsedChatRequest, reviveParsedChatRequest } from '../requestParser/chatParserTypes.js';
40
import { LocalChatSessionUri } from './chatUri.js';
41
42
43
export const CHAT_ATTACHABLE_IMAGE_MIME_TYPES: Record<string, string> = {
44
png: 'image/png',
45
jpg: 'image/jpeg',
46
jpeg: 'image/jpeg',
47
gif: 'image/gif',
48
webp: 'image/webp',
49
};
50
51
export function getAttachableImageExtension(mimeType: string): string | undefined {
52
return Object.entries(CHAT_ATTACHABLE_IMAGE_MIME_TYPES).find(([_, value]) => value === mimeType)?.[0];
53
}
54
55
export interface IChatRequestVariableData {
56
variables: IChatRequestVariableEntry[];
57
}
58
59
export namespace IChatRequestVariableData {
60
export function toExport(data: IChatRequestVariableData): IChatRequestVariableData {
61
return { variables: data.variables.map(IChatRequestVariableEntry.toExport) };
62
}
63
}
64
65
export interface IChatRequestModel {
66
readonly id: string;
67
readonly timestamp: number;
68
readonly modeInfo?: IChatRequestModeInfo;
69
readonly session: IChatModel;
70
readonly message: IParsedChatRequest;
71
readonly attempt: number;
72
readonly variableData: IChatRequestVariableData;
73
readonly confirmation?: string;
74
readonly locationData?: IChatLocationData;
75
readonly attachedContext?: IChatRequestVariableEntry[];
76
readonly isCompleteAddedRequest: boolean;
77
readonly response?: IChatResponseModel;
78
readonly editedFileEvents?: IChatAgentEditedFileEvent[];
79
shouldBeRemovedOnSend: IChatRequestDisablement | undefined;
80
readonly shouldBeBlocked: IObservable<boolean>;
81
setShouldBeBlocked(value: boolean): void;
82
readonly modelId?: string;
83
readonly userSelectedTools?: UserSelectedTools;
84
}
85
86
export interface ICodeBlockInfo {
87
readonly suggestionId: EditSuggestionId;
88
}
89
90
export interface IChatTextEditGroupState {
91
sha1: string;
92
applied: number;
93
}
94
95
export interface IChatTextEditGroup {
96
uri: URI;
97
edits: TextEdit[][];
98
state?: IChatTextEditGroupState;
99
kind: 'textEditGroup';
100
done: boolean | undefined;
101
isExternalEdit?: boolean;
102
}
103
104
export function isCellTextEditOperation(value: unknown): value is ICellTextEditOperation {
105
const candidate = value as ICellTextEditOperation;
106
return !!candidate && !!candidate.edit && !!candidate.uri && URI.isUri(candidate.uri);
107
}
108
109
export function isCellTextEditOperationArray(value: ICellTextEditOperation[] | ICellEditOperation[]): value is ICellTextEditOperation[] {
110
return value.some(isCellTextEditOperation);
111
}
112
113
export interface ICellTextEditOperation {
114
edit: TextEdit;
115
uri: URI;
116
}
117
118
export interface IChatNotebookEditGroup {
119
uri: URI;
120
edits: (ICellTextEditOperation[] | ICellEditOperation[])[];
121
state?: IChatTextEditGroupState;
122
kind: 'notebookEditGroup';
123
done: boolean | undefined;
124
isExternalEdit?: boolean;
125
}
126
127
/**
128
* Progress kinds that are included in the history of a response.
129
* Excludes "internal" types that are included in history.
130
*/
131
export type IChatProgressHistoryResponseContent =
132
| IChatMarkdownContent
133
| IChatAgentMarkdownContentWithVulnerability
134
| IChatResponseCodeblockUriPart
135
| IChatTreeData
136
| IChatMultiDiffDataSerialized
137
| IChatContentInlineReference
138
| IChatProgressMessage
139
| IChatCommandButton
140
| IChatWarningMessage
141
| IChatTask
142
| IChatTaskSerialized
143
| IChatTextEditGroup
144
| IChatNotebookEditGroup
145
| IChatConfirmation
146
| IChatExtensionsContent
147
| IChatThinkingPart
148
| IChatPullRequestContent;
149
150
/**
151
* "Normal" progress kinds that are rendered as parts of the stream of content.
152
*/
153
export type IChatProgressResponseContent =
154
| IChatProgressHistoryResponseContent
155
| IChatToolInvocation
156
| IChatToolInvocationSerialized
157
| IChatMultiDiffData
158
| IChatUndoStop
159
| IChatPrepareToolInvocationPart
160
| IChatElicitationRequest
161
| IChatElicitationRequestSerialized
162
| IChatClearToPreviousToolInvocation
163
| IChatMcpServersStarting;
164
165
const nonHistoryKinds = new Set(['toolInvocation', 'toolInvocationSerialized', 'undoStop', 'prepareToolInvocation']);
166
function isChatProgressHistoryResponseContent(content: IChatProgressResponseContent): content is IChatProgressHistoryResponseContent {
167
return !nonHistoryKinds.has(content.kind);
168
}
169
170
export function toChatHistoryContent(content: ReadonlyArray<IChatProgressResponseContent>): IChatProgressHistoryResponseContent[] {
171
return content.filter(isChatProgressHistoryResponseContent);
172
}
173
174
export type IChatProgressRenderableResponseContent = Exclude<IChatProgressResponseContent, IChatContentInlineReference | IChatAgentMarkdownContentWithVulnerability | IChatResponseCodeblockUriPart>;
175
176
export interface IResponse {
177
readonly value: ReadonlyArray<IChatProgressResponseContent>;
178
getMarkdown(): string;
179
toString(): string;
180
}
181
182
export interface IChatResponseModel {
183
readonly onDidChange: Event<ChatResponseModelChangeReason>;
184
readonly id: string;
185
readonly requestId: string;
186
readonly request: IChatRequestModel | undefined;
187
readonly username: string;
188
readonly avatarIcon?: ThemeIcon | URI;
189
readonly session: IChatModel;
190
readonly agent?: IChatAgentData;
191
readonly usedContext: IChatUsedContext | undefined;
192
readonly contentReferences: ReadonlyArray<IChatContentReference>;
193
readonly codeCitations: ReadonlyArray<IChatCodeCitation>;
194
readonly progressMessages: ReadonlyArray<IChatProgressMessage>;
195
readonly slashCommand?: IChatAgentCommand;
196
readonly agentOrSlashCommandDetected: boolean;
197
/** View of the response shown to the user, may have parts omitted from undo stops. */
198
readonly response: IResponse;
199
/** Entire response from the model. */
200
readonly entireResponse: IResponse;
201
/** Milliseconds timestamp when this chat response was created. */
202
readonly timestamp: number;
203
/** Milliseconds timestamp when this chat response was completed or cancelled. */
204
readonly completedAt?: number;
205
/** The state of this response */
206
readonly state: ResponseModelState;
207
/**
208
* Adjusted millisecond timestamp that excludes the duration during which
209
* the model was pending user confirmation. `Date.now() - confirmationAdjustedTimestamp`
210
* will return the amount of time the response was busy generating content.
211
* This is updated only when `isPendingConfirmation` changes state.
212
*/
213
readonly confirmationAdjustedTimestamp: IObservable<number>;
214
readonly isComplete: boolean;
215
readonly isCanceled: boolean;
216
readonly isPendingConfirmation: IObservable<{ startedWaitingAt: number; detail?: string } | undefined>;
217
readonly isInProgress: IObservable<boolean>;
218
readonly shouldBeRemovedOnSend: IChatRequestDisablement | undefined;
219
readonly shouldBeBlocked: IObservable<boolean>;
220
readonly isCompleteAddedRequest: boolean;
221
/** A stale response is one that has been persisted and rehydrated, so e.g. Commands that have their arguments stored in the EH are gone. */
222
readonly isStale: boolean;
223
readonly vote: ChatAgentVoteDirection | undefined;
224
readonly voteDownReason: ChatAgentVoteDownReason | undefined;
225
readonly followups?: IChatFollowup[] | undefined;
226
readonly result?: IChatAgentResult;
227
readonly codeBlockInfos: ICodeBlockInfo[] | undefined;
228
229
initializeCodeBlockInfos(codeBlockInfo: ICodeBlockInfo[]): void;
230
addUndoStop(undoStop: IChatUndoStop): void;
231
setVote(vote: ChatAgentVoteDirection): void;
232
setVoteDownReason(reason: ChatAgentVoteDownReason | undefined): void;
233
setEditApplied(edit: IChatTextEditGroup, editCount: number): boolean;
234
updateContent(progress: IChatProgressResponseContent | IChatTextEdit | IChatNotebookEdit | IChatTask, quiet?: boolean): void;
235
/**
236
* Adopts any partially-undo {@link response} as the {@link entireResponse}.
237
* Only valid when {@link isComplete}. This is needed because otherwise an
238
* undone and then diverged state would start showing old data because the
239
* undo stops would no longer exist in the model.
240
*/
241
finalizeUndoState(): void;
242
}
243
244
export type ChatResponseModelChangeReason =
245
| { reason: 'other' }
246
| { reason: 'completedRequest' }
247
| { reason: 'undoStop'; id: string };
248
249
export const defaultChatResponseModelChangeReason: ChatResponseModelChangeReason = { reason: 'other' };
250
251
export interface IChatRequestModeInfo {
252
kind: ChatModeKind | undefined; // is undefined in case of modeId == 'apply'
253
isBuiltin: boolean;
254
modeInstructions: IChatRequestModeInstructions | undefined;
255
modeId: 'ask' | 'agent' | 'edit' | 'custom' | 'applyCodeBlock' | undefined;
256
applyCodeBlockSuggestionId: EditSuggestionId | undefined;
257
}
258
259
export interface IChatRequestModeInstructions {
260
readonly name: string;
261
readonly content: string;
262
readonly toolReferences: readonly ChatRequestToolReferenceEntry[];
263
readonly metadata?: Record<string, boolean | string | number>;
264
}
265
266
export interface IChatRequestModelParameters {
267
session: ChatModel;
268
message: IParsedChatRequest;
269
variableData: IChatRequestVariableData;
270
timestamp: number;
271
attempt?: number;
272
modeInfo?: IChatRequestModeInfo;
273
confirmation?: string;
274
locationData?: IChatLocationData;
275
attachedContext?: IChatRequestVariableEntry[];
276
isCompleteAddedRequest?: boolean;
277
modelId?: string;
278
restoredId?: string;
279
editedFileEvents?: IChatAgentEditedFileEvent[];
280
userSelectedTools?: UserSelectedTools;
281
}
282
283
export class ChatRequestModel implements IChatRequestModel {
284
public readonly id: string;
285
public response: ChatResponseModel | undefined;
286
public shouldBeRemovedOnSend: IChatRequestDisablement | undefined;
287
public readonly timestamp: number;
288
public readonly message: IParsedChatRequest;
289
public readonly isCompleteAddedRequest: boolean;
290
public readonly modelId?: string;
291
public readonly modeInfo?: IChatRequestModeInfo;
292
public readonly userSelectedTools?: UserSelectedTools;
293
294
private readonly _shouldBeBlocked = observableValue<boolean>(this, false);
295
public get shouldBeBlocked(): IObservable<boolean> {
296
return this._shouldBeBlocked;
297
}
298
299
public setShouldBeBlocked(value: boolean): void {
300
this._shouldBeBlocked.set(value, undefined);
301
}
302
303
private _session: ChatModel;
304
private readonly _attempt: number;
305
private _variableData: IChatRequestVariableData;
306
private readonly _confirmation?: string;
307
private readonly _locationData?: IChatLocationData;
308
private readonly _attachedContext?: IChatRequestVariableEntry[];
309
private readonly _editedFileEvents?: IChatAgentEditedFileEvent[];
310
311
public get session(): ChatModel {
312
return this._session;
313
}
314
315
public get attempt(): number {
316
return this._attempt;
317
}
318
319
public get variableData(): IChatRequestVariableData {
320
return this._variableData;
321
}
322
323
public set variableData(v: IChatRequestVariableData) {
324
this._variableData = v;
325
}
326
327
public get confirmation(): string | undefined {
328
return this._confirmation;
329
}
330
331
public get locationData(): IChatLocationData | undefined {
332
return this._locationData;
333
}
334
335
public get attachedContext(): IChatRequestVariableEntry[] | undefined {
336
return this._attachedContext;
337
}
338
339
public get editedFileEvents(): IChatAgentEditedFileEvent[] | undefined {
340
return this._editedFileEvents;
341
}
342
343
constructor(params: IChatRequestModelParameters) {
344
this._session = params.session;
345
this.message = params.message;
346
this._variableData = params.variableData;
347
this.timestamp = params.timestamp;
348
this._attempt = params.attempt ?? 0;
349
this.modeInfo = params.modeInfo;
350
this._confirmation = params.confirmation;
351
this._locationData = params.locationData;
352
this._attachedContext = params.attachedContext;
353
this.isCompleteAddedRequest = params.isCompleteAddedRequest ?? false;
354
this.modelId = params.modelId;
355
this.id = params.restoredId ?? 'request_' + generateUuid();
356
this._editedFileEvents = params.editedFileEvents;
357
this.userSelectedTools = params.userSelectedTools;
358
}
359
360
adoptTo(session: ChatModel) {
361
this._session = session;
362
}
363
}
364
365
class AbstractResponse implements IResponse {
366
protected _responseParts: IChatProgressResponseContent[];
367
368
/**
369
* A stringified representation of response data which might be presented to a screenreader or used when copying a response.
370
*/
371
protected _responseRepr = '';
372
373
/**
374
* Just the markdown content of the response, used for determining the rendering rate of markdown
375
*/
376
protected _markdownContent = '';
377
378
get value(): IChatProgressResponseContent[] {
379
return this._responseParts;
380
}
381
382
constructor(value: IChatProgressResponseContent[]) {
383
this._responseParts = value;
384
this._updateRepr();
385
}
386
387
toString(): string {
388
return this._responseRepr;
389
}
390
391
/**
392
* _Just_ the content of markdown parts in the response
393
*/
394
getMarkdown(): string {
395
return this._markdownContent;
396
}
397
398
protected _updateRepr() {
399
this._responseRepr = this.partsToRepr(this._responseParts);
400
401
this._markdownContent = this._responseParts.map(part => {
402
if (part.kind === 'inlineReference') {
403
return this.inlineRefToRepr(part);
404
} else if (part.kind === 'markdownContent' || part.kind === 'markdownVuln') {
405
return part.content.value;
406
} else {
407
return '';
408
}
409
})
410
.filter(s => s.length > 0)
411
.join('');
412
}
413
414
private partsToRepr(parts: readonly IChatProgressResponseContent[]): string {
415
const blocks: string[] = [];
416
let currentBlockSegments: string[] = [];
417
let hasEditGroupsAfterLastClear = false;
418
419
for (const part of parts) {
420
let segment: { text: string; isBlock?: boolean } | undefined;
421
switch (part.kind) {
422
case 'clearToPreviousToolInvocation':
423
currentBlockSegments = [];
424
blocks.length = 0;
425
hasEditGroupsAfterLastClear = false; // Reset edit groups flag when clearing
426
continue;
427
case 'treeData':
428
case 'progressMessage':
429
case 'codeblockUri':
430
case 'extensions':
431
case 'pullRequest':
432
case 'undoStop':
433
case 'prepareToolInvocation':
434
case 'elicitation2':
435
case 'elicitationSerialized':
436
case 'thinking':
437
case 'multiDiffData':
438
case 'mcpServersStarting':
439
// Ignore
440
continue;
441
case 'toolInvocation':
442
case 'toolInvocationSerialized':
443
// Include tool invocations in the copy text
444
segment = this.getToolInvocationText(part);
445
break;
446
case 'inlineReference':
447
segment = { text: this.inlineRefToRepr(part) };
448
break;
449
case 'command':
450
segment = { text: part.command.title, isBlock: true };
451
break;
452
case 'textEditGroup':
453
case 'notebookEditGroup':
454
// Mark that we have edit groups after the last clear
455
hasEditGroupsAfterLastClear = true;
456
// Skip individual edit groups to avoid duplication
457
continue;
458
case 'confirmation':
459
if (part.message instanceof MarkdownString) {
460
segment = { text: `${part.title}\n${part.message.value}`, isBlock: true };
461
break;
462
}
463
segment = { text: `${part.title}\n${part.message}`, isBlock: true };
464
break;
465
case 'markdownContent':
466
case 'markdownVuln':
467
case 'progressTask':
468
case 'progressTaskSerialized':
469
case 'warning':
470
segment = { text: part.content.value };
471
break;
472
default:
473
// Ignore any unknown/obsolete parts, but assert that all are handled:
474
softAssertNever(part);
475
continue;
476
}
477
478
if (segment.isBlock) {
479
if (currentBlockSegments.length) {
480
blocks.push(currentBlockSegments.join(''));
481
currentBlockSegments = [];
482
}
483
blocks.push(segment.text);
484
} else {
485
currentBlockSegments.push(segment.text);
486
}
487
}
488
489
if (currentBlockSegments.length) {
490
blocks.push(currentBlockSegments.join(''));
491
}
492
493
// Add consolidated edit summary at the end if there were any edit groups after the last clear
494
if (hasEditGroupsAfterLastClear) {
495
blocks.push(localize('editsSummary', "Made changes."));
496
}
497
498
return blocks.join('\n\n');
499
}
500
501
private inlineRefToRepr(part: IChatContentInlineReference) {
502
if ('uri' in part.inlineReference) {
503
return this.uriToRepr(part.inlineReference.uri);
504
}
505
506
return 'name' in part.inlineReference
507
? '`' + part.inlineReference.name + '`'
508
: this.uriToRepr(part.inlineReference);
509
}
510
511
private getToolInvocationText(toolInvocation: IChatToolInvocation | IChatToolInvocationSerialized): { text: string; isBlock?: boolean } {
512
// Extract the message and input details
513
let message = '';
514
let input = '';
515
516
if (toolInvocation.pastTenseMessage) {
517
message = typeof toolInvocation.pastTenseMessage === 'string'
518
? toolInvocation.pastTenseMessage
519
: toolInvocation.pastTenseMessage.value;
520
} else {
521
message = typeof toolInvocation.invocationMessage === 'string'
522
? toolInvocation.invocationMessage
523
: toolInvocation.invocationMessage.value;
524
}
525
526
// Handle different types of tool invocations
527
if (toolInvocation.toolSpecificData) {
528
if (toolInvocation.toolSpecificData.kind === 'terminal') {
529
message = 'Ran terminal command';
530
const terminalData = migrateLegacyTerminalToolSpecificData(toolInvocation.toolSpecificData);
531
input = terminalData.commandLine.userEdited ?? terminalData.commandLine.toolEdited ?? terminalData.commandLine.original;
532
}
533
}
534
535
// Format the tool invocation text
536
let text = message;
537
if (input) {
538
text += `: ${input}`;
539
}
540
541
// For completed tool invocations, also include the result details if available
542
if (toolInvocation.kind === 'toolInvocationSerialized' || (toolInvocation.kind === 'toolInvocation' && IChatToolInvocation.isComplete(toolInvocation))) {
543
const resultDetails = IChatToolInvocation.resultDetails(toolInvocation);
544
if (resultDetails && 'input' in resultDetails) {
545
const resultPrefix = toolInvocation.kind === 'toolInvocationSerialized' || IChatToolInvocation.isComplete(toolInvocation) ? 'Completed' : 'Errored';
546
text += `\n${resultPrefix} with input: ${resultDetails.input}`;
547
}
548
}
549
550
return { text, isBlock: true };
551
}
552
553
private uriToRepr(uri: URI): string {
554
if (uri.scheme === Schemas.http || uri.scheme === Schemas.https) {
555
return uri.toString(false);
556
}
557
558
return basename(uri);
559
}
560
}
561
562
/** A view of a subset of a response */
563
class ResponseView extends AbstractResponse {
564
constructor(
565
_response: IResponse,
566
public readonly undoStop: string,
567
) {
568
let idx = _response.value.findIndex(v => v.kind === 'undoStop' && v.id === undoStop);
569
// Undo stops are inserted before `codeblockUri`'s, which are preceeded by a
570
// markdownContent containing the opening code fence. Adjust the index
571
// backwards to avoid a buggy response if it looked like this happened.
572
if (_response.value[idx + 1]?.kind === 'codeblockUri' && _response.value[idx - 1]?.kind === 'markdownContent') {
573
idx--;
574
}
575
576
super(idx === -1 ? _response.value.slice() : _response.value.slice(0, idx));
577
}
578
}
579
580
export class Response extends AbstractResponse implements IDisposable {
581
private _onDidChangeValue = new Emitter<void>();
582
public get onDidChangeValue() {
583
return this._onDidChangeValue.event;
584
}
585
586
private _citations: IChatCodeCitation[] = [];
587
588
589
constructor(value: IMarkdownString | ReadonlyArray<IMarkdownString | IChatResponseProgressFileTreeData | IChatContentInlineReference | IChatAgentMarkdownContentWithVulnerability | IChatResponseCodeblockUriPart | IChatThinkingPart>) {
590
super(asArray(value).map((v) => (
591
'kind' in v ? v :
592
isMarkdownString(v) ? { content: v, kind: 'markdownContent' } satisfies IChatMarkdownContent :
593
{ kind: 'treeData', treeData: v }
594
)));
595
}
596
597
dispose(): void {
598
this._onDidChangeValue.dispose();
599
}
600
601
602
clear(): void {
603
this._responseParts = [];
604
this._updateRepr(true);
605
}
606
607
clearToPreviousToolInvocation(message?: string): void {
608
// look through the response parts and find the last tool invocation, then slice the response parts to that point
609
let lastToolInvocationIndex = -1;
610
for (let i = this._responseParts.length - 1; i >= 0; i--) {
611
const part = this._responseParts[i];
612
if (part.kind === 'toolInvocation' || part.kind === 'toolInvocationSerialized') {
613
lastToolInvocationIndex = i;
614
break;
615
}
616
}
617
if (lastToolInvocationIndex !== -1) {
618
this._responseParts = this._responseParts.slice(0, lastToolInvocationIndex + 1);
619
} else {
620
this._responseParts = [];
621
}
622
if (message) {
623
this._responseParts.push({ kind: 'warning', content: new MarkdownString(message) });
624
}
625
this._updateRepr(true);
626
}
627
628
updateContent(progress: IChatProgressResponseContent | IChatTextEdit | IChatNotebookEdit | IChatTask, quiet?: boolean): void {
629
if (progress.kind === 'clearToPreviousToolInvocation') {
630
if (progress.reason === ChatResponseClearToPreviousToolInvocationReason.CopyrightContentRetry) {
631
this.clearToPreviousToolInvocation(localize('copyrightContentRetry', "Response cleared due to possible match to public code, retrying with modified prompt."));
632
} else if (progress.reason === ChatResponseClearToPreviousToolInvocationReason.FilteredContentRetry) {
633
this.clearToPreviousToolInvocation(localize('filteredContentRetry', "Response cleared due to content safety filters, retrying with modified prompt."));
634
} else {
635
this.clearToPreviousToolInvocation();
636
}
637
return;
638
} else if (progress.kind === 'markdownContent') {
639
640
// last response which is NOT a text edit group because we do want to support heterogenous streaming but not have
641
// the MD be chopped up by text edit groups (and likely other non-renderable parts)
642
const lastResponsePart = this._responseParts
643
.filter(p => p.kind !== 'textEditGroup')
644
.at(-1);
645
646
if (!lastResponsePart || lastResponsePart.kind !== 'markdownContent' || !canMergeMarkdownStrings(lastResponsePart.content, progress.content)) {
647
// The last part can't be merged with- not markdown, or markdown with different permissions
648
this._responseParts.push(progress);
649
} else {
650
// Don't modify the current object, since it's being diffed by the renderer
651
const idx = this._responseParts.indexOf(lastResponsePart);
652
this._responseParts[idx] = { ...lastResponsePart, content: appendMarkdownString(lastResponsePart.content, progress.content) };
653
}
654
this._updateRepr(quiet);
655
} else if (progress.kind === 'thinking') {
656
657
// tries to split thinking chunks if it is an array. only while certain models give us array chunks.
658
const lastResponsePart = this._responseParts
659
.filter(p => p.kind !== 'textEditGroup')
660
.at(-1);
661
662
const lastText = lastResponsePart && lastResponsePart.kind === 'thinking'
663
? (Array.isArray(lastResponsePart.value) ? lastResponsePart.value.join('') : (lastResponsePart.value || ''))
664
: '';
665
const currText = Array.isArray(progress.value) ? progress.value.join('') : (progress.value || '');
666
const isEmpty = (s: string) => s.length === 0;
667
668
// Do not merge if either the current or last thinking chunk is empty; empty chunks separate thinking
669
if (!lastResponsePart
670
|| lastResponsePart.kind !== 'thinking'
671
|| isEmpty(currText)
672
|| isEmpty(lastText)
673
|| !canMergeMarkdownStrings(new MarkdownString(lastText), new MarkdownString(currText))) {
674
this._responseParts.push(progress);
675
} else {
676
const idx = this._responseParts.indexOf(lastResponsePart);
677
this._responseParts[idx] = {
678
...lastResponsePart,
679
value: appendMarkdownString(new MarkdownString(lastText), new MarkdownString(currText)).value
680
};
681
}
682
this._updateRepr(quiet);
683
} else if (progress.kind === 'textEdit' || progress.kind === 'notebookEdit') {
684
// merge edits for the same file no matter when they come in
685
const notebookUri = CellUri.parse(progress.uri)?.notebook;
686
const uri = notebookUri ?? progress.uri;
687
const isExternalEdit = progress.isExternalEdit;
688
689
if (progress.kind === 'textEdit' && !notebookUri) {
690
// Text edits to a regular (non-notebook) file
691
this._mergeOrPushTextEditGroup(uri, progress.edits, progress.done, isExternalEdit);
692
} else if (progress.kind === 'textEdit') {
693
// Text edits to a notebook cell - convert to ICellTextEditOperation
694
const cellEdits = progress.edits.map(edit => ({ uri: progress.uri, edit }));
695
this._mergeOrPushNotebookEditGroup(uri, cellEdits, progress.done, isExternalEdit);
696
} else {
697
// Notebook cell edits (ICellEditOperation)
698
this._mergeOrPushNotebookEditGroup(uri, progress.edits, progress.done, isExternalEdit);
699
}
700
this._updateRepr(quiet);
701
} else if (progress.kind === 'progressTask') {
702
// Add a new resolving part
703
const responsePosition = this._responseParts.push(progress) - 1;
704
this._updateRepr(quiet);
705
706
const disp = progress.onDidAddProgress(() => {
707
this._updateRepr(false);
708
});
709
710
progress.task?.().then((content) => {
711
// Stop listening for progress updates once the task settles
712
disp.dispose();
713
714
// Replace the resolving part's content with the resolved response
715
if (typeof content === 'string') {
716
(this._responseParts[responsePosition] as IChatTask).content = new MarkdownString(content);
717
}
718
this._updateRepr(false);
719
});
720
721
} else if (progress.kind === 'toolInvocation') {
722
autorunSelfDisposable(reader => {
723
progress.state.read(reader); // update repr when state changes
724
this._updateRepr(false);
725
726
if (IChatToolInvocation.isComplete(progress, reader)) {
727
reader.dispose();
728
}
729
});
730
this._responseParts.push(progress);
731
this._updateRepr(quiet);
732
} else {
733
this._responseParts.push(progress);
734
this._updateRepr(quiet);
735
}
736
}
737
738
public addCitation(citation: IChatCodeCitation) {
739
this._citations.push(citation);
740
this._updateRepr();
741
}
742
743
private _mergeOrPushTextEditGroup(uri: URI, edits: TextEdit[], done: boolean | undefined, isExternalEdit: boolean | undefined): void {
744
for (const candidate of this._responseParts) {
745
if (candidate.kind === 'textEditGroup' && !candidate.done && isEqual(candidate.uri, uri)) {
746
candidate.edits.push(edits);
747
candidate.done = done;
748
return;
749
}
750
}
751
this._responseParts.push({ kind: 'textEditGroup', uri, edits: [edits], done, isExternalEdit });
752
}
753
754
private _mergeOrPushNotebookEditGroup(uri: URI, edits: ICellTextEditOperation[] | ICellEditOperation[], done: boolean | undefined, isExternalEdit: boolean | undefined): void {
755
for (const candidate of this._responseParts) {
756
if (candidate.kind === 'notebookEditGroup' && !candidate.done && isEqual(candidate.uri, uri)) {
757
candidate.edits.push(edits);
758
candidate.done = done;
759
return;
760
}
761
}
762
this._responseParts.push({ kind: 'notebookEditGroup', uri, edits: [edits], done, isExternalEdit });
763
}
764
765
protected override _updateRepr(quiet?: boolean) {
766
super._updateRepr();
767
if (!this._onDidChangeValue) {
768
return; // called from parent constructor
769
}
770
771
this._responseRepr += this._citations.length ? '\n\n' + getCodeCitationsMessage(this._citations) : '';
772
773
if (!quiet) {
774
this._onDidChangeValue.fire();
775
}
776
}
777
}
778
779
export interface IChatResponseModelParameters {
780
responseContent: IMarkdownString | ReadonlyArray<IMarkdownString | IChatResponseProgressFileTreeData | IChatContentInlineReference | IChatAgentMarkdownContentWithVulnerability | IChatResponseCodeblockUriPart | IChatThinkingPart>;
781
session: ChatModel;
782
agent?: IChatAgentData;
783
slashCommand?: IChatAgentCommand;
784
requestId: string;
785
timestamp?: number;
786
vote?: ChatAgentVoteDirection;
787
voteDownReason?: ChatAgentVoteDownReason;
788
result?: IChatAgentResult;
789
followups?: ReadonlyArray<IChatFollowup>;
790
isCompleteAddedRequest?: boolean;
791
shouldBeRemovedOnSend?: IChatRequestDisablement;
792
shouldBeBlocked?: boolean;
793
restoredId?: string;
794
modelState?: ResponseModelStateT;
795
timeSpentWaiting?: number;
796
/**
797
* undefined means it will be set later.
798
*/
799
codeBlockInfos: ICodeBlockInfo[] | undefined;
800
}
801
802
type ResponseModelStateT =
803
| { value: ResponseModelState.Pending }
804
| { value: ResponseModelState.NeedsInput }
805
| { value: ResponseModelState.Complete | ResponseModelState.Cancelled | ResponseModelState.Failed; completedAt: number };
806
807
export class ChatResponseModel extends Disposable implements IChatResponseModel {
808
private readonly _onDidChange = this._register(new Emitter<ChatResponseModelChangeReason>());
809
readonly onDidChange = this._onDidChange.event;
810
811
public readonly id: string;
812
public readonly requestId: string;
813
private _session: ChatModel;
814
private _agent: IChatAgentData | undefined;
815
private _slashCommand: IChatAgentCommand | undefined;
816
private _modelState = observableValue<ResponseModelStateT>(this, { value: ResponseModelState.Pending });
817
private _vote?: ChatAgentVoteDirection;
818
private _voteDownReason?: ChatAgentVoteDownReason;
819
private _result?: IChatAgentResult;
820
private _shouldBeRemovedOnSend: IChatRequestDisablement | undefined;
821
public readonly isCompleteAddedRequest: boolean;
822
private readonly _shouldBeBlocked = observableValue<boolean>(this, false);
823
private readonly _timestamp: number;
824
private _timeSpentWaitingAccumulator: number;
825
826
public confirmationAdjustedTimestamp: IObservable<number>;
827
828
public get shouldBeBlocked(): IObservable<boolean> {
829
return this._shouldBeBlocked;
830
}
831
832
public get request(): IChatRequestModel | undefined {
833
return this.session.getRequests().find(r => r.id === this.requestId);
834
}
835
836
public get session() {
837
return this._session;
838
}
839
840
public get shouldBeRemovedOnSend() {
841
return this._shouldBeRemovedOnSend;
842
}
843
844
public get isComplete(): boolean {
845
return this._modelState.get().value !== ResponseModelState.Pending && this._modelState.get().value !== ResponseModelState.NeedsInput;
846
}
847
848
public get timestamp(): number {
849
return this._timestamp;
850
}
851
852
public set shouldBeRemovedOnSend(disablement: IChatRequestDisablement | undefined) {
853
this._shouldBeRemovedOnSend = disablement;
854
this._onDidChange.fire(defaultChatResponseModelChangeReason);
855
}
856
857
public get isCanceled(): boolean {
858
return this._modelState.get().value === ResponseModelState.Cancelled;
859
}
860
861
public get completedAt(): number | undefined {
862
const state = this._modelState.get();
863
if (state.value === ResponseModelState.Complete || state.value === ResponseModelState.Cancelled || state.value === ResponseModelState.Failed) {
864
return state.completedAt;
865
}
866
return undefined;
867
}
868
869
public get state(): ResponseModelState {
870
const state = this._modelState.get().value;
871
if (state === ResponseModelState.Complete && !!this._result?.errorDetails && this.result?.errorDetails?.code !== 'canceled') {
872
// This check covers sessions created in previous vscode versions which saved a failed response as 'Complete'
873
return ResponseModelState.Failed;
874
}
875
876
return state;
877
}
878
879
public get vote(): ChatAgentVoteDirection | undefined {
880
return this._vote;
881
}
882
883
public get voteDownReason(): ChatAgentVoteDownReason | undefined {
884
return this._voteDownReason;
885
}
886
887
public get followups(): IChatFollowup[] | undefined {
888
return this._followups;
889
}
890
891
private _response: Response;
892
private _finalizedResponse?: IResponse;
893
public get entireResponse(): IResponse {
894
return this._finalizedResponse || this._response;
895
}
896
897
public get result(): IChatAgentResult | undefined {
898
return this._result;
899
}
900
901
public get username(): string {
902
return this.session.responderUsername;
903
}
904
905
public get avatarIcon(): ThemeIcon | URI | undefined {
906
return this.session.responderAvatarIcon;
907
}
908
909
private _followups?: IChatFollowup[];
910
911
public get agent(): IChatAgentData | undefined {
912
return this._agent;
913
}
914
915
public get slashCommand(): IChatAgentCommand | undefined {
916
return this._slashCommand;
917
}
918
919
private _agentOrSlashCommandDetected: boolean | undefined;
920
public get agentOrSlashCommandDetected(): boolean {
921
return this._agentOrSlashCommandDetected ?? false;
922
}
923
924
private _usedContext: IChatUsedContext | undefined;
925
public get usedContext(): IChatUsedContext | undefined {
926
return this._usedContext;
927
}
928
929
private readonly _contentReferences: IChatContentReference[] = [];
930
public get contentReferences(): ReadonlyArray<IChatContentReference> {
931
return Array.from(this._contentReferences);
932
}
933
934
private readonly _codeCitations: IChatCodeCitation[] = [];
935
public get codeCitations(): ReadonlyArray<IChatCodeCitation> {
936
return this._codeCitations;
937
}
938
939
private readonly _progressMessages: IChatProgressMessage[] = [];
940
public get progressMessages(): ReadonlyArray<IChatProgressMessage> {
941
return this._progressMessages;
942
}
943
944
private _isStale: boolean = false;
945
public get isStale(): boolean {
946
return this._isStale;
947
}
948
949
950
readonly isPendingConfirmation: IObservable<{ startedWaitingAt: number; detail?: string } | undefined>;
951
952
readonly isInProgress: IObservable<boolean>;
953
954
private _responseView?: ResponseView;
955
public get response(): IResponse {
956
const undoStop = this._shouldBeRemovedOnSend?.afterUndoStop;
957
if (!undoStop) {
958
return this._finalizedResponse || this._response;
959
}
960
961
if (this._responseView?.undoStop !== undoStop) {
962
this._responseView = new ResponseView(this._response, undoStop);
963
}
964
965
return this._responseView;
966
}
967
968
private _codeBlockInfos: ICodeBlockInfo[] | undefined;
969
public get codeBlockInfos(): ICodeBlockInfo[] | undefined {
970
return this._codeBlockInfos;
971
}
972
973
constructor(params: IChatResponseModelParameters) {
974
super();
975
976
this._session = params.session;
977
this._agent = params.agent;
978
this._slashCommand = params.slashCommand;
979
this.requestId = params.requestId;
980
this._timestamp = params.timestamp || Date.now();
981
if (params.modelState) {
982
this._modelState.set(params.modelState, undefined);
983
}
984
this._timeSpentWaitingAccumulator = params.timeSpentWaiting || 0;
985
this._vote = params.vote;
986
this._voteDownReason = params.voteDownReason;
987
this._result = params.result;
988
this._followups = params.followups ? [...params.followups] : undefined;
989
this.isCompleteAddedRequest = params.isCompleteAddedRequest ?? false;
990
this._shouldBeRemovedOnSend = params.shouldBeRemovedOnSend;
991
this._shouldBeBlocked.set(params.shouldBeBlocked ?? false, undefined);
992
993
// If we are creating a response with some existing content, consider it stale
994
this._isStale = Array.isArray(params.responseContent) && (params.responseContent.length !== 0 || isMarkdownString(params.responseContent) && params.responseContent.value.length !== 0);
995
996
this._response = this._register(new Response(params.responseContent));
997
this._codeBlockInfos = params.codeBlockInfos ? [...params.codeBlockInfos] : undefined;
998
999
const signal = observableSignalFromEvent(this, this.onDidChange);
1000
1001
const _pendingInfo = signal.map((_value, r): string | undefined => {
1002
signal.read(r);
1003
1004
for (const part of this._response.value) {
1005
if (part.kind === 'toolInvocation' && part.state.read(r).type === IChatToolInvocation.StateKind.WaitingForConfirmation) {
1006
const title = part.confirmationMessages?.title;
1007
return title ? (isMarkdownString(title) ? title.value : title) : undefined;
1008
}
1009
if (part.kind === 'confirmation' && !part.isUsed) {
1010
return part.title;
1011
}
1012
if (part.kind === 'elicitation2' && part.state.read(r) === ElicitationState.Pending) {
1013
const title = part.title;
1014
return isMarkdownString(title) ? title.value : title;
1015
}
1016
}
1017
1018
return undefined;
1019
});
1020
1021
const _startedWaitingAt = _pendingInfo.map(p => !!p).map(p => p ? Date.now() : undefined);
1022
this.isPendingConfirmation = _startedWaitingAt.map((waiting, r) => waiting ? { startedWaitingAt: waiting, detail: _pendingInfo.read(r) } : undefined);
1023
1024
this.isInProgress = signal.map((_value, r) => {
1025
1026
signal.read(r);
1027
1028
return !_pendingInfo.read(r)
1029
&& !this.shouldBeRemovedOnSend
1030
&& (this._modelState.read(r).value === ResponseModelState.Pending || this._modelState.read(r).value === ResponseModelState.NeedsInput);
1031
});
1032
1033
this._register(this._response.onDidChangeValue(() => this._onDidChange.fire(defaultChatResponseModelChangeReason)));
1034
this.id = params.restoredId ?? 'response_' + generateUuid();
1035
1036
this._register(this._session.onDidChange((e) => {
1037
if (e.kind === 'setCheckpoint') {
1038
const isDisabled = e.disabledResponseIds.has(this.id);
1039
this._shouldBeBlocked.set(isDisabled, undefined);
1040
}
1041
}));
1042
1043
let lastStartedWaitingAt: number | undefined = undefined;
1044
this.confirmationAdjustedTimestamp = derived(reader => {
1045
const pending = this.isPendingConfirmation.read(reader);
1046
if (pending) {
1047
this._modelState.set({ value: ResponseModelState.NeedsInput }, undefined);
1048
if (!lastStartedWaitingAt) {
1049
lastStartedWaitingAt = pending.startedWaitingAt;
1050
}
1051
} else if (lastStartedWaitingAt) {
1052
// Restore state to Pending if it was set to NeedsInput by this observable
1053
if (this._modelState.read(reader).value === ResponseModelState.NeedsInput) {
1054
this._modelState.set({ value: ResponseModelState.Pending }, undefined);
1055
}
1056
this._timeSpentWaitingAccumulator += Date.now() - lastStartedWaitingAt;
1057
lastStartedWaitingAt = undefined;
1058
}
1059
1060
return this._timestamp + this._timeSpentWaitingAccumulator;
1061
}).recomputeInitiallyAndOnChange(this._store);
1062
}
1063
1064
initializeCodeBlockInfos(codeBlockInfo: ICodeBlockInfo[]): void {
1065
if (this._codeBlockInfos) {
1066
throw new BugIndicatingError('Code block infos have already been initialized');
1067
}
1068
this._codeBlockInfos = [...codeBlockInfo];
1069
}
1070
1071
/**
1072
* Apply a progress update to the actual response content.
1073
*/
1074
updateContent(responsePart: IChatProgressResponseContent | IChatTextEdit | IChatNotebookEdit, quiet?: boolean) {
1075
this._response.updateContent(responsePart, quiet);
1076
}
1077
1078
/**
1079
* Adds an undo stop at the current position in the stream.
1080
*/
1081
addUndoStop(undoStop: IChatUndoStop) {
1082
this._onDidChange.fire({ reason: 'undoStop', id: undoStop.id });
1083
this._response.updateContent(undoStop, true);
1084
}
1085
1086
/**
1087
* Apply one of the progress updates that are not part of the actual response content.
1088
*/
1089
applyReference(progress: IChatUsedContext | IChatContentReference) {
1090
if (progress.kind === 'usedContext') {
1091
this._usedContext = progress;
1092
} else if (progress.kind === 'reference') {
1093
this._contentReferences.push(progress);
1094
this._onDidChange.fire(defaultChatResponseModelChangeReason);
1095
}
1096
}
1097
1098
applyCodeCitation(progress: IChatCodeCitation) {
1099
this._codeCitations.push(progress);
1100
this._response.addCitation(progress);
1101
this._onDidChange.fire(defaultChatResponseModelChangeReason);
1102
}
1103
1104
setAgent(agent: IChatAgentData, slashCommand?: IChatAgentCommand) {
1105
this._agent = agent;
1106
this._slashCommand = slashCommand;
1107
this._agentOrSlashCommandDetected = !agent.isDefault || !!slashCommand;
1108
this._onDidChange.fire(defaultChatResponseModelChangeReason);
1109
}
1110
1111
setResult(result: IChatAgentResult): void {
1112
this._result = result;
1113
this._onDidChange.fire(defaultChatResponseModelChangeReason);
1114
}
1115
1116
complete(): void {
1117
// No-op if it's already complete
1118
if (this.isComplete) {
1119
return;
1120
}
1121
if (this._result?.errorDetails?.responseIsRedacted) {
1122
this._response.clear();
1123
}
1124
1125
// Canceled sessions can be considered 'Complete'
1126
const state = !!this._result?.errorDetails && this._result.errorDetails.code !== 'canceled' ? ResponseModelState.Failed : ResponseModelState.Complete;
1127
this._modelState.set({ value: state, completedAt: Date.now() }, undefined);
1128
this._onDidChange.fire({ reason: 'completedRequest' });
1129
}
1130
1131
cancel(): void {
1132
this._modelState.set({ value: ResponseModelState.Cancelled, completedAt: Date.now() }, undefined);
1133
this._onDidChange.fire({ reason: 'completedRequest' });
1134
}
1135
1136
setFollowups(followups: IChatFollowup[] | undefined): void {
1137
this._followups = followups;
1138
this._onDidChange.fire(defaultChatResponseModelChangeReason); // Fire so that command followups get rendered on the row
1139
}
1140
1141
setVote(vote: ChatAgentVoteDirection): void {
1142
this._vote = vote;
1143
this._onDidChange.fire(defaultChatResponseModelChangeReason);
1144
}
1145
1146
setVoteDownReason(reason: ChatAgentVoteDownReason | undefined): void {
1147
this._voteDownReason = reason;
1148
this._onDidChange.fire(defaultChatResponseModelChangeReason);
1149
}
1150
1151
setEditApplied(edit: IChatTextEditGroup, editCount: number): boolean {
1152
if (!this.response.value.includes(edit)) {
1153
return false;
1154
}
1155
if (!edit.state) {
1156
return false;
1157
}
1158
edit.state.applied = editCount; // must not be edit.edits.length
1159
this._onDidChange.fire(defaultChatResponseModelChangeReason);
1160
return true;
1161
}
1162
1163
adoptTo(session: ChatModel) {
1164
this._session = session;
1165
this._onDidChange.fire(defaultChatResponseModelChangeReason);
1166
}
1167
1168
1169
finalizeUndoState(): void {
1170
this._finalizedResponse = this.response;
1171
this._responseView = undefined;
1172
this._shouldBeRemovedOnSend = undefined;
1173
}
1174
1175
toJSON(): ISerializableChatResponseData {
1176
const modelState = this._modelState.get();
1177
const pendingConfirmation = this.isPendingConfirmation.get();
1178
1179
return {
1180
responseId: this.id,
1181
result: this.result,
1182
responseMarkdownInfo: this.codeBlockInfos?.map<ISerializableMarkdownInfo>(info => ({ suggestionId: info.suggestionId })),
1183
followups: this.followups,
1184
modelState: modelState.value === ResponseModelState.Pending || modelState.value === ResponseModelState.NeedsInput ? { value: ResponseModelState.Cancelled, completedAt: Date.now() } : modelState,
1185
vote: this.vote,
1186
voteDownReason: this.voteDownReason,
1187
slashCommand: this.slashCommand,
1188
usedContext: this.usedContext,
1189
contentReferences: this.contentReferences,
1190
codeCitations: this.codeCitations,
1191
timestamp: this._timestamp,
1192
timeSpentWaiting: (pendingConfirmation ? Date.now() - pendingConfirmation.startedWaitingAt : 0) + this._timeSpentWaitingAccumulator,
1193
} satisfies WithDefinedProps<ISerializableChatResponseData>;
1194
}
1195
}
1196
1197
1198
export interface IChatRequestDisablement {
1199
requestId: string;
1200
afterUndoStop?: string;
1201
}
1202
1203
/**
1204
* Information about a chat request that needs user input to continue.
1205
*/
1206
export interface IChatRequestNeedsInputInfo {
1207
/** The chat session title */
1208
readonly title: string;
1209
/** Optional detail message, e.g., "<toolname> needs approval to run." */
1210
readonly detail?: string;
1211
}
1212
1213
export interface IChatModel extends IDisposable {
1214
readonly onDidDispose: Event<void>;
1215
readonly onDidChange: Event<IChatChangeEvent>;
1216
/** @deprecated Use {@link sessionResource} instead */
1217
readonly sessionId: string;
1218
/** Milliseconds timestamp this chat model was created. */
1219
readonly timestamp: number;
1220
readonly timing: IChatSessionTiming;
1221
readonly sessionResource: URI;
1222
readonly initialLocation: ChatAgentLocation;
1223
readonly title: string;
1224
readonly hasCustomTitle: boolean;
1225
/** True whenever a request is currently running */
1226
readonly requestInProgress: IObservable<boolean>;
1227
/** Provides session information when a request needs user interaction to continue */
1228
readonly requestNeedsInput: IObservable<IChatRequestNeedsInputInfo | undefined>;
1229
readonly inputPlaceholder?: string;
1230
readonly editingSession?: IChatEditingSession | undefined;
1231
readonly checkpoint: IChatRequestModel | undefined;
1232
startEditingSession(isGlobalEditingSession?: boolean, transferFromSession?: IChatEditingSession): void;
1233
/** Input model for managing input state */
1234
readonly inputModel: IInputModel;
1235
readonly hasRequests: boolean;
1236
readonly lastRequest: IChatRequestModel | undefined;
1237
/** Whether this model will be kept alive while it is running or has edits */
1238
readonly willKeepAlive: boolean;
1239
readonly lastRequestObs: IObservable<IChatRequestModel | undefined>;
1240
getRequests(): IChatRequestModel[];
1241
setCheckpoint(requestId: string | undefined): void;
1242
1243
toExport(): IExportableChatData;
1244
toJSON(): ISerializableChatData;
1245
readonly contributedChatSession: IChatSessionContext | undefined;
1246
}
1247
1248
export interface ISerializableChatsData {
1249
[sessionId: string]: ISerializableChatData;
1250
}
1251
1252
export type ISerializableChatAgentData = UriDto<IChatAgentData>;
1253
1254
interface ISerializableChatResponseData {
1255
responseId?: string;
1256
result?: IChatAgentResult; // Optional for backcompat
1257
responseMarkdownInfo?: ISerializableMarkdownInfo[];
1258
followups?: ReadonlyArray<IChatFollowup>;
1259
modelState?: ResponseModelStateT;
1260
vote?: ChatAgentVoteDirection;
1261
voteDownReason?: ChatAgentVoteDownReason;
1262
timestamp?: number;
1263
slashCommand?: IChatAgentCommand;
1264
/** For backward compat: should be optional */
1265
usedContext?: IChatUsedContext;
1266
contentReferences?: ReadonlyArray<IChatContentReference>;
1267
codeCitations?: ReadonlyArray<IChatCodeCitation>;
1268
timeSpentWaiting?: number;
1269
}
1270
1271
export interface ISerializableChatRequestData extends ISerializableChatResponseData {
1272
requestId: string;
1273
message: string | IParsedChatRequest; // string => old format
1274
/** Is really like "prompt data". This is the message in the format in which the agent gets it + variable values. */
1275
variableData: IChatRequestVariableData;
1276
response: ReadonlyArray<IMarkdownString | IChatResponseProgressFileTreeData | IChatContentInlineReference | IChatAgentMarkdownContentWithVulnerability | IChatThinkingPart> | undefined;
1277
1278
/**Old, persisted name for shouldBeRemovedOnSend */
1279
isHidden?: boolean;
1280
shouldBeRemovedOnSend?: IChatRequestDisablement;
1281
agent?: ISerializableChatAgentData;
1282
workingSet?: UriComponents[];
1283
// responseErrorDetails: IChatResponseErrorDetails | undefined;
1284
/** @deprecated modelState is used instead now */
1285
isCanceled?: boolean;
1286
timestamp?: number;
1287
confirmation?: string;
1288
editedFileEvents?: IChatAgentEditedFileEvent[];
1289
modelId?: string;
1290
}
1291
1292
export interface ISerializableMarkdownInfo {
1293
readonly suggestionId: EditSuggestionId;
1294
}
1295
1296
export interface IExportableChatData {
1297
initialLocation: ChatAgentLocation | undefined;
1298
requests: ISerializableChatRequestData[];
1299
responderUsername: string;
1300
responderAvatarIconUri: ThemeIcon | UriComponents | undefined; // Keeping Uri name for backcompat
1301
}
1302
1303
/*
1304
NOTE: every time the serialized data format is updated, we need to create a new interface, because we may need to handle any old data format when parsing.
1305
*/
1306
1307
export interface ISerializableChatData1 extends IExportableChatData {
1308
sessionId: string;
1309
creationDate: number;
1310
1311
/** Indicates that this session was created in this window. Is cleared after the chat has been written to storage once. Needed to sync chat creations/deletions between empty windows. */
1312
isNew?: boolean;
1313
}
1314
1315
export interface ISerializableChatData2 extends ISerializableChatData1 {
1316
version: 2;
1317
lastMessageDate: number;
1318
computedTitle: string | undefined;
1319
}
1320
1321
export interface ISerializableChatData3 extends Omit<ISerializableChatData2, 'version' | 'computedTitle'> {
1322
version: 3;
1323
customTitle: string | undefined;
1324
/**
1325
* Whether the session had pending edits when it was stored.
1326
* todo@connor4312 This will be cleaned up with the globalization of edits.
1327
*/
1328
hasPendingEdits?: boolean;
1329
/** Current draft input state (added later, fully backwards compatible) */
1330
inputState?: ISerializableChatModelInputState;
1331
}
1332
1333
/**
1334
* Input model for managing chat input state independently from the chat model.
1335
* This keeps display logic separated from the core chat model.
1336
*
1337
* The input model:
1338
* - Manages the current draft state (text, attachments, mode, model selection, cursor/selection)
1339
* - Provides an observable interface for reactive UI updates
1340
* - Automatically persists through the chat model's serialization
1341
* - Enables bidirectional sync between the UI (ChatInputPart) and the model
1342
* - Uses `undefined` state to indicate no persisted state (new/empty chat)
1343
*
1344
* This architecture ensures that:
1345
* - Input state is preserved when moving chats between editor/sidebar/window
1346
* - No manual state transfer is needed when switching contexts
1347
* - The UI stays in sync with the persisted state
1348
* - New chats use UI defaults (persisted preferences) instead of hardcoded values
1349
*/
1350
export interface IInputModel {
1351
/** Observable for current input state (undefined for new/uninitialized chats) */
1352
readonly state: IObservable<IChatModelInputState | undefined>;
1353
1354
/** Update the input state (partial update) */
1355
setState(state: Partial<IChatModelInputState>): void;
1356
1357
/** Clear input state (after sending or clearing) */
1358
clearState(): void;
1359
1360
/** Serializes the state */
1361
toJSON(): ISerializableChatModelInputState | undefined;
1362
}
1363
1364
/**
1365
* Represents the current state of the chat input that hasn't been sent yet.
1366
* This is the "draft" state that should be preserved across sessions.
1367
*/
1368
export interface IChatModelInputState {
1369
/** Current attachments in the input */
1370
attachments: readonly IChatRequestVariableEntry[];
1371
1372
/** Currently selected chat mode */
1373
mode: {
1374
/** Mode ID (e.g., 'ask', 'edit', 'agent', or custom mode ID) */
1375
id: string;
1376
/** Mode kind for builtin modes */
1377
kind: ChatModeKind | undefined;
1378
};
1379
1380
/** Currently selected language model, if any */
1381
selectedModel: ILanguageModelChatMetadataAndIdentifier | undefined;
1382
1383
/** Current input text */
1384
inputText: string;
1385
1386
/** Current selection ranges */
1387
selections: ISelection[];
1388
1389
/** Contributed stored state */
1390
contrib: Record<string, unknown>;
1391
}
1392
1393
/**
1394
* Serializable version of IChatModelInputState
1395
*/
1396
export interface ISerializableChatModelInputState {
1397
attachments: readonly IChatRequestVariableEntry[];
1398
mode: {
1399
id: string;
1400
kind: ChatModeKind | undefined;
1401
};
1402
selectedModel: {
1403
identifier: string;
1404
metadata: ILanguageModelChatMetadata;
1405
} | undefined;
1406
inputText: string;
1407
selections: ISelection[];
1408
contrib: Record<string, unknown>;
1409
}
1410
1411
/**
1412
* Chat data that has been parsed and normalized to the current format.
1413
*/
1414
export type ISerializableChatData = ISerializableChatData3;
1415
1416
/**
1417
* Chat data that has been loaded but not normalized, and could be any format
1418
*/
1419
export type ISerializableChatDataIn = ISerializableChatData1 | ISerializableChatData2 | ISerializableChatData3;
1420
1421
/**
1422
* Normalize chat data from storage to the current format.
1423
* TODO- ChatModel#_deserialize and reviveSerializedAgent also still do some normalization and maybe that should be done in here too.
1424
*/
1425
export function normalizeSerializableChatData(raw: ISerializableChatDataIn): ISerializableChatData {
1426
normalizeOldFields(raw);
1427
1428
if (!('version' in raw)) {
1429
return {
1430
version: 3,
1431
...raw,
1432
lastMessageDate: raw.creationDate,
1433
customTitle: undefined,
1434
};
1435
}
1436
1437
if (raw.version === 2) {
1438
return {
1439
...raw,
1440
version: 3,
1441
customTitle: raw.computedTitle
1442
};
1443
}
1444
1445
return raw;
1446
}
1447
1448
function normalizeOldFields(raw: ISerializableChatDataIn): void {
1449
// Fill in fields that very old chat data may be missing
1450
if (!raw.sessionId) {
1451
raw.sessionId = generateUuid();
1452
}
1453
1454
if (!raw.creationDate) {
1455
raw.creationDate = getLastYearDate();
1456
}
1457
1458
if ('version' in raw && (raw.version === 2 || raw.version === 3)) {
1459
if (!raw.lastMessageDate) {
1460
// A bug led to not porting creationDate properly, and that was copied to lastMessageDate, so fix that up if missing.
1461
raw.lastMessageDate = getLastYearDate();
1462
}
1463
}
1464
1465
// eslint-disable-next-line @typescript-eslint/no-explicit-any, local/code-no-any-casts
1466
if ((raw.initialLocation as any) === 'editing-session') {
1467
raw.initialLocation = ChatAgentLocation.Chat;
1468
}
1469
}
1470
1471
function getLastYearDate(): number {
1472
const lastYearDate = new Date();
1473
lastYearDate.setFullYear(lastYearDate.getFullYear() - 1);
1474
return lastYearDate.getTime();
1475
}
1476
1477
export function isExportableSessionData(obj: unknown): obj is IExportableChatData {
1478
return !!obj &&
1479
Array.isArray((obj as IExportableChatData).requests) &&
1480
typeof (obj as IExportableChatData).responderUsername === 'string';
1481
}
1482
1483
export function isSerializableSessionData(obj: unknown): obj is ISerializableChatData {
1484
const data = obj as ISerializableChatData;
1485
return isExportableSessionData(obj) &&
1486
typeof data.creationDate === 'number' &&
1487
typeof data.sessionId === 'string' &&
1488
obj.requests.every((request: ISerializableChatRequestData) =>
1489
!request.usedContext /* for backward compat allow missing usedContext */ || isIUsedContext(request.usedContext)
1490
);
1491
}
1492
1493
export type IChatChangeEvent =
1494
| IChatInitEvent
1495
| IChatAddRequestEvent | IChatChangedRequestEvent | IChatRemoveRequestEvent
1496
| IChatAddResponseEvent
1497
| IChatSetAgentEvent
1498
| IChatMoveEvent
1499
| IChatSetHiddenEvent
1500
| IChatCompletedRequestEvent
1501
| IChatSetCheckpointEvent
1502
| IChatSetCustomTitleEvent
1503
;
1504
1505
export interface IChatAddRequestEvent {
1506
kind: 'addRequest';
1507
request: IChatRequestModel;
1508
}
1509
1510
export interface IChatSetCheckpointEvent {
1511
kind: 'setCheckpoint';
1512
disabledRequestIds: Set<string>;
1513
disabledResponseIds: Set<string>;
1514
}
1515
1516
export interface IChatChangedRequestEvent {
1517
kind: 'changedRequest';
1518
request: IChatRequestModel;
1519
}
1520
1521
export interface IChatCompletedRequestEvent {
1522
kind: 'completedRequest';
1523
request: IChatRequestModel;
1524
}
1525
1526
export interface IChatAddResponseEvent {
1527
kind: 'addResponse';
1528
response: IChatResponseModel;
1529
}
1530
1531
export const enum ChatRequestRemovalReason {
1532
/**
1533
* "Normal" remove
1534
*/
1535
Removal,
1536
1537
/**
1538
* Removed because the request will be resent
1539
*/
1540
Resend,
1541
1542
/**
1543
* Remove because the request is moving to another model
1544
*/
1545
Adoption
1546
}
1547
1548
export interface IChatRemoveRequestEvent {
1549
kind: 'removeRequest';
1550
requestId: string;
1551
responseId?: string;
1552
reason: ChatRequestRemovalReason;
1553
}
1554
1555
export interface IChatSetHiddenEvent {
1556
kind: 'setHidden';
1557
}
1558
1559
export interface IChatMoveEvent {
1560
kind: 'move';
1561
target: URI;
1562
range: IRange;
1563
}
1564
1565
export interface IChatSetAgentEvent {
1566
kind: 'setAgent';
1567
agent: IChatAgentData;
1568
command?: IChatAgentCommand;
1569
}
1570
1571
export interface IChatSetCustomTitleEvent {
1572
kind: 'setCustomTitle';
1573
title: string;
1574
}
1575
1576
export interface IChatInitEvent {
1577
kind: 'initialize';
1578
}
1579
1580
/**
1581
* Internal implementation of IInputModel
1582
*/
1583
class InputModel implements IInputModel {
1584
private readonly _state: ReturnType<typeof observableValue<IChatModelInputState | undefined>>;
1585
readonly state: IObservable<IChatModelInputState | undefined>;
1586
1587
constructor(initialState: IChatModelInputState | undefined) {
1588
this._state = observableValueOpts({ debugName: 'inputModelState', equalsFn: equals }, initialState);
1589
this.state = this._state;
1590
}
1591
1592
setState(state: Partial<IChatModelInputState>): void {
1593
const current = this._state.get();
1594
this._state.set({
1595
// If current is undefined, provide defaults for required fields
1596
attachments: [],
1597
mode: { id: 'agent', kind: ChatModeKind.Agent },
1598
selectedModel: undefined,
1599
inputText: '',
1600
selections: [],
1601
contrib: {},
1602
...current,
1603
...state
1604
}, undefined);
1605
}
1606
1607
clearState(): void {
1608
this._state.set(undefined, undefined);
1609
}
1610
1611
toJSON(): ISerializableChatModelInputState | undefined {
1612
const value = this.state.get();
1613
if (!value) {
1614
return undefined;
1615
}
1616
1617
return {
1618
contrib: value.contrib,
1619
attachments: value.attachments,
1620
mode: value.mode,
1621
selectedModel: value.selectedModel ? {
1622
identifier: value.selectedModel.identifier,
1623
metadata: value.selectedModel.metadata
1624
} : undefined,
1625
inputText: value.inputText,
1626
selections: value.selections
1627
};
1628
}
1629
}
1630
1631
export class ChatModel extends Disposable implements IChatModel {
1632
static getDefaultTitle(requests: (ISerializableChatRequestData | IChatRequestModel)[]): string {
1633
const firstRequestMessage = requests.at(0)?.message ?? '';
1634
const message = typeof firstRequestMessage === 'string' ?
1635
firstRequestMessage :
1636
firstRequestMessage.text;
1637
return message.split('\n')[0].substring(0, 200);
1638
}
1639
1640
private readonly _onDidDispose = this._register(new Emitter<void>());
1641
readonly onDidDispose = this._onDidDispose.event;
1642
1643
private readonly _onDidChange = this._register(new Emitter<IChatChangeEvent>());
1644
readonly onDidChange = this._onDidChange.event;
1645
1646
private _requests: ChatRequestModel[];
1647
1648
private _contributedChatSession: IChatSessionContext | undefined;
1649
public get contributedChatSession(): IChatSessionContext | undefined {
1650
return this._contributedChatSession;
1651
}
1652
public setContributedChatSession(session: IChatSessionContext | undefined) {
1653
this._contributedChatSession = session;
1654
}
1655
readonly lastRequestObs: IObservable<IChatRequestModel | undefined>;
1656
1657
// TODO to be clear, this is not the same as the id from the session object, which belongs to the provider.
1658
// It's easier to be able to identify this model before its async initialization is complete
1659
private readonly _sessionId: string;
1660
/** @deprecated Use {@link sessionResource} instead */
1661
get sessionId(): string {
1662
return this._sessionId;
1663
}
1664
1665
private readonly _sessionResource: URI;
1666
get sessionResource(): URI {
1667
return this._sessionResource;
1668
}
1669
1670
readonly requestInProgress: IObservable<boolean>;
1671
readonly requestNeedsInput: IObservable<IChatRequestNeedsInputInfo | undefined>;
1672
1673
/** Input model for managing input state */
1674
readonly inputModel: InputModel;
1675
1676
get hasRequests(): boolean {
1677
return this._requests.length > 0;
1678
}
1679
1680
get lastRequest(): ChatRequestModel | undefined {
1681
return this._requests.at(-1);
1682
}
1683
1684
private _timestamp: number;
1685
get timestamp(): number {
1686
return this._timestamp;
1687
}
1688
1689
get timing(): IChatSessionTiming {
1690
const lastResponse = this._requests.at(-1)?.response;
1691
return {
1692
startTime: this._timestamp,
1693
endTime: lastResponse?.completedAt ?? lastResponse?.timestamp
1694
};
1695
}
1696
1697
private _lastMessageDate: number;
1698
get lastMessageDate(): number {
1699
return this._lastMessageDate;
1700
}
1701
1702
private get _defaultAgent() {
1703
return this.chatAgentService.getDefaultAgent(ChatAgentLocation.Chat, ChatModeKind.Ask);
1704
}
1705
1706
private readonly _initialResponderUsername: string | undefined;
1707
get responderUsername(): string {
1708
return this._defaultAgent?.fullName ??
1709
this._initialResponderUsername ?? '';
1710
}
1711
1712
private readonly _initialResponderAvatarIconUri: ThemeIcon | URI | undefined;
1713
get responderAvatarIcon(): ThemeIcon | URI | undefined {
1714
return this._defaultAgent?.metadata.themeIcon ??
1715
this._initialResponderAvatarIconUri;
1716
}
1717
1718
private _isImported = false;
1719
get isImported(): boolean {
1720
return this._isImported;
1721
}
1722
1723
private _customTitle: string | undefined;
1724
get customTitle(): string | undefined {
1725
return this._customTitle;
1726
}
1727
1728
get title(): string {
1729
return this._customTitle || ChatModel.getDefaultTitle(this._requests);
1730
}
1731
1732
get hasCustomTitle(): boolean {
1733
return this._customTitle !== undefined;
1734
}
1735
1736
private _editingSession: IChatEditingSession | undefined;
1737
1738
get editingSession(): IChatEditingSession | undefined {
1739
return this._editingSession;
1740
}
1741
1742
private readonly _initialLocation: ChatAgentLocation;
1743
get initialLocation(): ChatAgentLocation {
1744
return this._initialLocation;
1745
}
1746
1747
private readonly _canUseTools: boolean = true;
1748
get canUseTools(): boolean {
1749
return this._canUseTools;
1750
}
1751
1752
private _disableBackgroundKeepAlive: boolean;
1753
get willKeepAlive(): boolean {
1754
return !this._disableBackgroundKeepAlive;
1755
}
1756
1757
constructor(
1758
initialData: ISerializableChatData | IExportableChatData | undefined,
1759
initialModelProps: { initialLocation: ChatAgentLocation; canUseTools: boolean; inputState?: ISerializableChatModelInputState; resource?: URI; sessionId?: string; disableBackgroundKeepAlive?: boolean },
1760
@IConfigurationService private readonly configurationService: IConfigurationService,
1761
@ILogService private readonly logService: ILogService,
1762
@IChatAgentService private readonly chatAgentService: IChatAgentService,
1763
@IChatEditingService private readonly chatEditingService: IChatEditingService,
1764
@IChatService private readonly chatService: IChatService,
1765
) {
1766
super();
1767
1768
const isValidExportedData = isExportableSessionData(initialData);
1769
const isValidFullData = isValidExportedData && isSerializableSessionData(initialData);
1770
if (initialData && !isValidExportedData) {
1771
this.logService.warn(`ChatModel#constructor: Loaded malformed session data: ${JSON.stringify(initialData)}`);
1772
}
1773
1774
this._isImported = !!initialData && isValidExportedData && !isValidFullData;
1775
this._sessionId = (isValidFullData && initialData.sessionId) || initialModelProps.sessionId || generateUuid();
1776
this._sessionResource = initialModelProps.resource ?? LocalChatSessionUri.forSession(this._sessionId);
1777
this._disableBackgroundKeepAlive = initialModelProps.disableBackgroundKeepAlive ?? false;
1778
1779
this._requests = initialData ? this._deserialize(initialData) : [];
1780
this._timestamp = (isValidFullData && initialData.creationDate) || Date.now();
1781
this._lastMessageDate = (isValidFullData && initialData.lastMessageDate) || this._timestamp;
1782
this._customTitle = isValidFullData ? initialData.customTitle : undefined;
1783
1784
// Initialize input model from serialized data (undefined for new chats)
1785
const serializedInputState = initialModelProps.inputState || (isValidFullData && initialData.inputState ? initialData.inputState : undefined);
1786
this.inputModel = new InputModel(serializedInputState && {
1787
attachments: serializedInputState.attachments,
1788
mode: serializedInputState.mode,
1789
selectedModel: serializedInputState.selectedModel && {
1790
identifier: serializedInputState.selectedModel.identifier,
1791
metadata: serializedInputState.selectedModel.metadata
1792
},
1793
contrib: serializedInputState.contrib,
1794
inputText: serializedInputState.inputText,
1795
selections: serializedInputState.selections
1796
});
1797
1798
this._initialResponderUsername = initialData?.responderUsername;
1799
this._initialResponderAvatarIconUri = isUriComponents(initialData?.responderAvatarIconUri) ? URI.revive(initialData.responderAvatarIconUri) : initialData?.responderAvatarIconUri;
1800
1801
this._initialLocation = initialData?.initialLocation ?? initialModelProps.initialLocation;
1802
this._canUseTools = initialModelProps.canUseTools;
1803
1804
this.lastRequestObs = observableFromEvent(this, this.onDidChange, () => this._requests.at(-1));
1805
1806
this._register(autorun(reader => {
1807
const request = this.lastRequestObs.read(reader);
1808
if (!request?.response) {
1809
return;
1810
}
1811
1812
reader.store.add(request.response.onDidChange(async ev => {
1813
if (!this._editingSession || ev.reason !== 'completedRequest') {
1814
return;
1815
}
1816
1817
if (
1818
request === this._requests.at(-1) &&
1819
request.session.sessionResource.scheme !== Schemas.vscodeLocalChatSession &&
1820
this.configurationService.getValue<boolean>('chat.checkpoints.showFileChanges') === true &&
1821
this._editingSession.hasEditsInRequest(request.id)
1822
) {
1823
const diffs = this._editingSession.getDiffsForFilesInRequest(request.id);
1824
request.response?.updateContent(editEntriesToMultiDiffData(diffs), true);
1825
}
1826
1827
this._onDidChange.fire({ kind: 'completedRequest', request });
1828
}));
1829
}));
1830
1831
this.requestInProgress = this.lastRequestObs.map((request, r) => {
1832
return request?.response?.isInProgress.read(r) ?? false;
1833
});
1834
1835
this.requestNeedsInput = this.lastRequestObs.map((request, r) => {
1836
const pendingInfo = request?.response?.isPendingConfirmation.read(r);
1837
if (!pendingInfo) {
1838
return undefined;
1839
}
1840
return {
1841
title: this.title,
1842
detail: pendingInfo.detail,
1843
};
1844
});
1845
1846
// Retain a reference to itself when a request is in progress, so the ChatModel stays alive in the background
1847
// only while running a request. TODO also keep it alive for 5min or so so we don't have to dispose/restore too often?
1848
if (this.initialLocation === ChatAgentLocation.Chat && !initialModelProps.disableBackgroundKeepAlive) {
1849
const selfRef = this._register(new MutableDisposable<IChatModelReference>());
1850
this._register(autorun(r => {
1851
const inProgress = this.requestInProgress.read(r);
1852
const needsInput = this.requestNeedsInput.read(r);
1853
const shouldStayAlive = inProgress || !!needsInput;
1854
if (shouldStayAlive && !selfRef.value) {
1855
selfRef.value = chatService.getActiveSessionReference(this._sessionResource);
1856
} else if (!shouldStayAlive && selfRef.value) {
1857
selfRef.clear();
1858
}
1859
}));
1860
}
1861
}
1862
1863
startEditingSession(isGlobalEditingSession?: boolean, transferFromSession?: IChatEditingSession): void {
1864
const session = this._editingSession ??= this._register(
1865
transferFromSession
1866
? this.chatEditingService.transferEditingSession(this, transferFromSession)
1867
: isGlobalEditingSession
1868
? this.chatEditingService.startOrContinueGlobalEditingSession(this)
1869
: this.chatEditingService.createEditingSession(this)
1870
);
1871
1872
if (!this._disableBackgroundKeepAlive) {
1873
// todo@connor4312: hold onto a reference so background sessions don't
1874
// trigger early disposal. This will be cleaned up with the globalization of edits.
1875
const selfRef = this._register(new MutableDisposable<IChatModelReference>());
1876
this._register(autorun(r => {
1877
const hasModified = session.entries.read(r).some(e => e.state.read(r) === ModifiedFileEntryState.Modified);
1878
if (hasModified && !selfRef.value) {
1879
selfRef.value = this.chatService.getActiveSessionReference(this._sessionResource);
1880
} else if (!hasModified && selfRef.value) {
1881
selfRef.clear();
1882
}
1883
}));
1884
}
1885
1886
this._register(autorun(reader => {
1887
this._setDisabledRequests(session.requestDisablement.read(reader));
1888
}));
1889
}
1890
1891
private currentEditedFileEvents = new ResourceMap<IChatAgentEditedFileEvent>();
1892
notifyEditingAction(action: IChatEditingSessionAction): void {
1893
const state = action.outcome === 'accepted' ? ChatRequestEditedFileEventKind.Keep :
1894
action.outcome === 'rejected' ? ChatRequestEditedFileEventKind.Undo :
1895
action.outcome === 'userModified' ? ChatRequestEditedFileEventKind.UserModification : null;
1896
if (state === null) {
1897
return;
1898
}
1899
1900
if (!this.currentEditedFileEvents.has(action.uri) || this.currentEditedFileEvents.get(action.uri)?.eventKind === ChatRequestEditedFileEventKind.Keep) {
1901
this.currentEditedFileEvents.set(action.uri, { eventKind: state, uri: action.uri });
1902
}
1903
}
1904
1905
private _deserialize(obj: IExportableChatData | ISerializableChatData): ChatRequestModel[] {
1906
const requests = obj.requests;
1907
if (!Array.isArray(requests)) {
1908
this.logService.error(`Ignoring malformed session data: ${JSON.stringify(obj)}`);
1909
return [];
1910
}
1911
1912
try {
1913
return requests.map((raw: ISerializableChatRequestData) => {
1914
const parsedRequest =
1915
typeof raw.message === 'string'
1916
? this.getParsedRequestFromString(raw.message)
1917
: reviveParsedChatRequest(raw.message);
1918
1919
// Old messages don't have variableData, or have it in the wrong (non-array) shape
1920
const variableData: IChatRequestVariableData = this.reviveVariableData(raw.variableData);
1921
const request = new ChatRequestModel({
1922
session: this,
1923
message: parsedRequest,
1924
variableData,
1925
timestamp: raw.timestamp ?? -1,
1926
restoredId: raw.requestId,
1927
confirmation: raw.confirmation,
1928
editedFileEvents: raw.editedFileEvents,
1929
modelId: raw.modelId,
1930
});
1931
request.shouldBeRemovedOnSend = raw.isHidden ? { requestId: raw.requestId } : raw.shouldBeRemovedOnSend;
1932
// eslint-disable-next-line @typescript-eslint/no-explicit-any, local/code-no-any-casts
1933
if (raw.response || raw.result || (raw as any).responseErrorDetails) {
1934
const agent = (raw.agent && 'metadata' in raw.agent) ? // Check for the new format, ignore entries in the old format
1935
reviveSerializedAgent(raw.agent) : undefined;
1936
1937
// Port entries from old format
1938
const result = 'responseErrorDetails' in raw ?
1939
// eslint-disable-next-line local/code-no-dangerous-type-assertions
1940
{ errorDetails: raw.responseErrorDetails } as IChatAgentResult : raw.result;
1941
request.response = new ChatResponseModel({
1942
responseContent: raw.response ?? [new MarkdownString(raw.response)],
1943
session: this,
1944
agent,
1945
slashCommand: raw.slashCommand,
1946
requestId: request.id,
1947
modelState: raw.modelState || { value: raw.isCanceled ? ResponseModelState.Cancelled : ResponseModelState.Complete, completedAt: 'lastMessageDate' in obj ? obj.lastMessageDate : Date.now() },
1948
vote: raw.vote,
1949
timestamp: raw.timestamp,
1950
voteDownReason: raw.voteDownReason,
1951
result,
1952
followups: raw.followups,
1953
restoredId: raw.responseId,
1954
timeSpentWaiting: raw.timeSpentWaiting,
1955
shouldBeBlocked: request.shouldBeBlocked.get(),
1956
codeBlockInfos: raw.responseMarkdownInfo?.map<ICodeBlockInfo>(info => ({ suggestionId: info.suggestionId })),
1957
});
1958
request.response.shouldBeRemovedOnSend = raw.isHidden ? { requestId: raw.requestId } : raw.shouldBeRemovedOnSend;
1959
if (raw.usedContext) { // @ulugbekna: if this's a new vscode sessions, doc versions are incorrect anyway?
1960
request.response.applyReference(revive(raw.usedContext));
1961
}
1962
1963
raw.contentReferences?.forEach(r => request.response!.applyReference(revive(r)));
1964
raw.codeCitations?.forEach(c => request.response!.applyCodeCitation(revive(c)));
1965
}
1966
return request;
1967
});
1968
} catch (error) {
1969
this.logService.error('Failed to parse chat data', error);
1970
return [];
1971
}
1972
}
1973
1974
private reviveVariableData(raw: IChatRequestVariableData): IChatRequestVariableData {
1975
const variableData = raw && Array.isArray(raw.variables)
1976
? raw :
1977
{ variables: [] };
1978
1979
variableData.variables = variableData.variables.map<IChatRequestVariableEntry>(IChatRequestVariableEntry.fromExport);
1980
1981
return variableData;
1982
}
1983
1984
private getParsedRequestFromString(message: string): IParsedChatRequest {
1985
// TODO These offsets won't be used, but chat replies need to go through the parser as well
1986
const parts = [new ChatRequestTextPart(new OffsetRange(0, message.length), { startColumn: 1, startLineNumber: 1, endColumn: 1, endLineNumber: 1 }, message)];
1987
return {
1988
text: message,
1989
parts
1990
};
1991
}
1992
1993
1994
1995
getRequests(): ChatRequestModel[] {
1996
return this._requests;
1997
}
1998
1999
resetCheckpoint(): void {
2000
for (const request of this._requests) {
2001
request.setShouldBeBlocked(false);
2002
}
2003
}
2004
2005
setCheckpoint(requestId: string | undefined) {
2006
let checkpoint: ChatRequestModel | undefined;
2007
let checkpointIndex = -1;
2008
if (requestId !== undefined) {
2009
this._requests.forEach((request, index) => {
2010
if (request.id === requestId) {
2011
checkpointIndex = index;
2012
checkpoint = request;
2013
request.setShouldBeBlocked(true);
2014
}
2015
});
2016
2017
if (!checkpoint) {
2018
return; // Invalid request ID
2019
}
2020
}
2021
2022
const disabledRequestIds = new Set<string>();
2023
const disabledResponseIds = new Set<string>();
2024
for (let i = this._requests.length - 1; i >= 0; i -= 1) {
2025
const request = this._requests[i];
2026
if (this._checkpoint && !checkpoint) {
2027
request.setShouldBeBlocked(false);
2028
} else if (checkpoint && i >= checkpointIndex) {
2029
request.setShouldBeBlocked(true);
2030
disabledRequestIds.add(request.id);
2031
if (request.response) {
2032
disabledResponseIds.add(request.response.id);
2033
}
2034
} else if (checkpoint && i < checkpointIndex) {
2035
request.setShouldBeBlocked(false);
2036
}
2037
}
2038
2039
this._checkpoint = checkpoint;
2040
this._onDidChange.fire({
2041
kind: 'setCheckpoint',
2042
disabledRequestIds,
2043
disabledResponseIds
2044
});
2045
}
2046
2047
private _checkpoint: ChatRequestModel | undefined = undefined;
2048
public get checkpoint() {
2049
return this._checkpoint;
2050
}
2051
2052
private _setDisabledRequests(requestIds: IChatRequestDisablement[]) {
2053
this._requests.forEach((request) => {
2054
const shouldBeRemovedOnSend = requestIds.find(r => r.requestId === request.id);
2055
request.shouldBeRemovedOnSend = shouldBeRemovedOnSend;
2056
if (request.response) {
2057
request.response.shouldBeRemovedOnSend = shouldBeRemovedOnSend;
2058
}
2059
});
2060
2061
this._onDidChange.fire({ kind: 'setHidden' });
2062
}
2063
2064
addRequest(message: IParsedChatRequest, variableData: IChatRequestVariableData, attempt: number, modeInfo?: IChatRequestModeInfo, chatAgent?: IChatAgentData, slashCommand?: IChatAgentCommand, confirmation?: string, locationData?: IChatLocationData, attachments?: IChatRequestVariableEntry[], isCompleteAddedRequest?: boolean, modelId?: string, userSelectedTools?: UserSelectedTools, id?: string): ChatRequestModel {
2065
const editedFileEvents = [...this.currentEditedFileEvents.values()];
2066
this.currentEditedFileEvents.clear();
2067
const request = new ChatRequestModel({
2068
restoredId: id,
2069
session: this,
2070
message,
2071
variableData,
2072
timestamp: Date.now(),
2073
attempt,
2074
modeInfo,
2075
confirmation,
2076
locationData,
2077
attachedContext: attachments,
2078
isCompleteAddedRequest,
2079
modelId,
2080
editedFileEvents: editedFileEvents.length ? editedFileEvents : undefined,
2081
userSelectedTools,
2082
});
2083
request.response = new ChatResponseModel({
2084
responseContent: [],
2085
session: this,
2086
agent: chatAgent,
2087
slashCommand,
2088
requestId: request.id,
2089
isCompleteAddedRequest,
2090
codeBlockInfos: undefined,
2091
});
2092
2093
this._requests.push(request);
2094
this._lastMessageDate = Date.now();
2095
this._onDidChange.fire({ kind: 'addRequest', request });
2096
return request;
2097
}
2098
2099
public setCustomTitle(title: string): void {
2100
this._customTitle = title;
2101
this._onDidChange.fire({ kind: 'setCustomTitle', title });
2102
}
2103
2104
updateRequest(request: ChatRequestModel, variableData: IChatRequestVariableData) {
2105
request.variableData = variableData;
2106
this._onDidChange.fire({ kind: 'changedRequest', request });
2107
}
2108
2109
adoptRequest(request: ChatRequestModel): void {
2110
// this doesn't use `removeRequest` because it must not dispose the request object
2111
const oldOwner = request.session;
2112
const index = oldOwner._requests.findIndex((candidate: ChatRequestModel) => candidate.id === request.id);
2113
2114
if (index === -1) {
2115
return;
2116
}
2117
2118
oldOwner._requests.splice(index, 1);
2119
2120
request.adoptTo(this);
2121
request.response?.adoptTo(this);
2122
this._requests.push(request);
2123
2124
oldOwner._onDidChange.fire({ kind: 'removeRequest', requestId: request.id, responseId: request.response?.id, reason: ChatRequestRemovalReason.Adoption });
2125
this._onDidChange.fire({ kind: 'addRequest', request });
2126
}
2127
2128
acceptResponseProgress(request: ChatRequestModel, progress: IChatProgress, quiet?: boolean): void {
2129
if (!request.response) {
2130
request.response = new ChatResponseModel({
2131
responseContent: [],
2132
session: this,
2133
requestId: request.id,
2134
codeBlockInfos: undefined,
2135
});
2136
}
2137
2138
if (request.response.isComplete) {
2139
throw new Error('acceptResponseProgress: Adding progress to a completed response');
2140
}
2141
2142
if (progress.kind === 'usedContext' || progress.kind === 'reference') {
2143
request.response.applyReference(progress);
2144
} else if (progress.kind === 'codeCitation') {
2145
request.response.applyCodeCitation(progress);
2146
} else if (progress.kind === 'move') {
2147
this._onDidChange.fire({ kind: 'move', target: progress.uri, range: progress.range });
2148
} else if (progress.kind === 'codeblockUri' && progress.isEdit) {
2149
request.response.addUndoStop({ id: progress.undoStopId ?? generateUuid(), kind: 'undoStop' });
2150
request.response.updateContent(progress, quiet);
2151
} else if (progress.kind === 'progressTaskResult') {
2152
// Should have been handled upstream, not sent to model
2153
this.logService.error(`Couldn't handle progress: ${JSON.stringify(progress)}`);
2154
} else {
2155
request.response.updateContent(progress, quiet);
2156
}
2157
}
2158
2159
removeRequest(id: string, reason: ChatRequestRemovalReason = ChatRequestRemovalReason.Removal): void {
2160
const index = this._requests.findIndex(request => request.id === id);
2161
const request = this._requests[index];
2162
2163
if (index !== -1) {
2164
this._onDidChange.fire({ kind: 'removeRequest', requestId: request.id, responseId: request.response?.id, reason });
2165
this._requests.splice(index, 1);
2166
request.response?.dispose();
2167
}
2168
}
2169
2170
cancelRequest(request: ChatRequestModel): void {
2171
if (request.response) {
2172
request.response.cancel();
2173
}
2174
}
2175
2176
setResponse(request: ChatRequestModel, result: IChatAgentResult): void {
2177
if (!request.response) {
2178
request.response = new ChatResponseModel({
2179
responseContent: [],
2180
session: this,
2181
requestId: request.id,
2182
codeBlockInfos: undefined,
2183
});
2184
}
2185
2186
request.response.setResult(result);
2187
}
2188
2189
setFollowups(request: ChatRequestModel, followups: IChatFollowup[] | undefined): void {
2190
if (!request.response) {
2191
// Maybe something went wrong?
2192
return;
2193
}
2194
request.response.setFollowups(followups);
2195
}
2196
2197
setResponseModel(request: ChatRequestModel, response: ChatResponseModel): void {
2198
request.response = response;
2199
this._onDidChange.fire({ kind: 'addResponse', response });
2200
}
2201
2202
toExport(): IExportableChatData {
2203
return {
2204
responderUsername: this.responderUsername,
2205
responderAvatarIconUri: this.responderAvatarIcon,
2206
initialLocation: this.initialLocation,
2207
requests: this._requests.map((r): ISerializableChatRequestData => {
2208
const message = {
2209
...r.message,
2210
// eslint-disable-next-line @typescript-eslint/no-explicit-any
2211
parts: r.message.parts.map((p: any) => p && 'toJSON' in p ? (p.toJSON as Function)() : p)
2212
};
2213
const agent = r.response?.agent;
2214
const agentJson = agent && 'toJSON' in agent ? (agent.toJSON as Function)() :
2215
agent ? { ...agent } : undefined;
2216
return {
2217
requestId: r.id,
2218
message,
2219
variableData: IChatRequestVariableData.toExport(r.variableData),
2220
response: r.response ?
2221
r.response.entireResponse.value.map(item => {
2222
// Keeping the shape of the persisted data the same for back compat
2223
if (item.kind === 'treeData') {
2224
return item.treeData;
2225
} else if (item.kind === 'markdownContent') {
2226
return item.content;
2227
} else {
2228
// eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any
2229
return item as any; // TODO
2230
}
2231
})
2232
: undefined,
2233
shouldBeRemovedOnSend: r.shouldBeRemovedOnSend,
2234
agent: agentJson,
2235
timestamp: r.timestamp,
2236
confirmation: r.confirmation,
2237
editedFileEvents: r.editedFileEvents,
2238
modelId: r.modelId,
2239
...r.response?.toJSON(),
2240
};
2241
}),
2242
};
2243
}
2244
2245
toJSON(): ISerializableChatData {
2246
return {
2247
version: 3,
2248
...this.toExport(),
2249
sessionId: this.sessionId,
2250
creationDate: this._timestamp,
2251
lastMessageDate: this._lastMessageDate,
2252
customTitle: this._customTitle,
2253
hasPendingEdits: !!(this._editingSession?.entries.get().some(e => e.state.get() === ModifiedFileEntryState.Modified)),
2254
inputState: this.inputModel.toJSON(),
2255
};
2256
}
2257
2258
override dispose() {
2259
this._requests.forEach(r => r.response?.dispose());
2260
this._onDidDispose.fire();
2261
2262
super.dispose();
2263
}
2264
}
2265
2266
export function updateRanges(variableData: IChatRequestVariableData, diff: number): IChatRequestVariableData {
2267
return {
2268
variables: variableData.variables.map(v => ({
2269
...v,
2270
range: v.range && {
2271
start: v.range.start - diff,
2272
endExclusive: v.range.endExclusive - diff
2273
}
2274
}))
2275
};
2276
}
2277
2278
export function canMergeMarkdownStrings(md1: IMarkdownString, md2: IMarkdownString): boolean {
2279
if (md1.baseUri && md2.baseUri) {
2280
const baseUriEquals = md1.baseUri.scheme === md2.baseUri.scheme
2281
&& md1.baseUri.authority === md2.baseUri.authority
2282
&& md1.baseUri.path === md2.baseUri.path
2283
&& md1.baseUri.query === md2.baseUri.query
2284
&& md1.baseUri.fragment === md2.baseUri.fragment;
2285
if (!baseUriEquals) {
2286
return false;
2287
}
2288
} else if (md1.baseUri || md2.baseUri) {
2289
return false;
2290
}
2291
2292
return equals(md1.isTrusted, md2.isTrusted) &&
2293
md1.supportHtml === md2.supportHtml &&
2294
md1.supportThemeIcons === md2.supportThemeIcons;
2295
}
2296
2297
export function appendMarkdownString(md1: IMarkdownString, md2: IMarkdownString | string): IMarkdownString {
2298
const appendedValue = typeof md2 === 'string' ? md2 : md2.value;
2299
return {
2300
value: md1.value + appendedValue,
2301
isTrusted: md1.isTrusted,
2302
supportThemeIcons: md1.supportThemeIcons,
2303
supportHtml: md1.supportHtml,
2304
baseUri: md1.baseUri
2305
};
2306
}
2307
2308
export function getCodeCitationsMessage(citations: ReadonlyArray<IChatCodeCitation>): string {
2309
if (citations.length === 0) {
2310
return '';
2311
}
2312
2313
const licenseTypes = citations.reduce((set, c) => set.add(c.license), new Set<string>());
2314
const label = licenseTypes.size === 1 ?
2315
localize('codeCitation', "Similar code found with 1 license type", licenseTypes.size) :
2316
localize('codeCitations', "Similar code found with {0} license types", licenseTypes.size);
2317
return label;
2318
}
2319
2320
export enum ChatRequestEditedFileEventKind {
2321
Keep = 1,
2322
Undo = 2,
2323
UserModification = 3,
2324
}
2325
2326
export interface IChatAgentEditedFileEvent {
2327
readonly uri: URI;
2328
readonly eventKind: ChatRequestEditedFileEventKind;
2329
}
2330
2331
/** URI for a resource embedded in a chat request/response */
2332
export namespace ChatResponseResource {
2333
export const scheme = 'vscode-chat-response-resource';
2334
2335
export function createUri(sessionResource: URI, toolCallId: string, index: number, basename?: string): URI {
2336
return URI.from({
2337
scheme: ChatResponseResource.scheme,
2338
authority: encodeHex(VSBuffer.fromString(sessionResource.toString())),
2339
path: `/tool/${toolCallId}/${index}` + (basename ? `/${basename}` : ''),
2340
});
2341
}
2342
2343
export function parseUri(uri: URI): undefined | { sessionResource: URI; toolCallId: string; index: number } {
2344
if (uri.scheme !== ChatResponseResource.scheme) {
2345
return undefined;
2346
}
2347
2348
const parts = uri.path.split('/');
2349
if (parts.length < 5) {
2350
return undefined;
2351
}
2352
2353
const [, kind, toolCallId, index] = parts;
2354
if (kind !== 'tool') {
2355
return undefined;
2356
}
2357
2358
let sessionResource: URI;
2359
try {
2360
sessionResource = URI.parse(decodeHex(uri.authority).toString());
2361
} catch (e) {
2362
if (e instanceof SyntaxError) { // pre-1.108 local session ID
2363
sessionResource = LocalChatSessionUri.forSession(uri.authority);
2364
} else {
2365
throw e;
2366
}
2367
}
2368
2369
return {
2370
sessionResource,
2371
toolCallId: toolCallId,
2372
index: Number(index),
2373
};
2374
}
2375
}
2376
2377