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