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
5252 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 === 'questionCarousel' && !part.isUsed) {
1154
return localize('waitingAnswer', "Answer questions to continue...");
1155
}
1156
if (part.kind === 'elicitation2' && part.state.read(r) === ElicitationState.Pending) {
1157
const title = part.title;
1158
return isMarkdownString(title) ? title.value : title;
1159
}
1160
}
1161
1162
return undefined;
1163
});
1164
1165
const _startedWaitingAt = _pendingInfo.map(p => !!p).map(p => p ? Date.now() : undefined);
1166
this.isPendingConfirmation = _startedWaitingAt.map((waiting, r) => waiting ? { startedWaitingAt: waiting, detail: _pendingInfo.read(r) } : undefined);
1167
1168
this.isInProgress = signal.map((_value, r) => {
1169
1170
signal.read(r);
1171
1172
return !_pendingInfo.read(r)
1173
&& !this.shouldBeRemovedOnSend
1174
&& (this._modelState.read(r).value === ResponseModelState.Pending || this._modelState.read(r).value === ResponseModelState.NeedsInput);
1175
});
1176
1177
this._register(this._response.onDidChangeValue(() => this._onDidChange.fire(defaultChatResponseModelChangeReason)));
1178
this.id = params.restoredId ?? 'response_' + generateUuid();
1179
1180
let lastStartedWaitingAt: number | undefined = undefined;
1181
this.confirmationAdjustedTimestamp = derived(reader => {
1182
const pending = this.isPendingConfirmation.read(reader);
1183
if (pending) {
1184
this._modelState.set({ value: ResponseModelState.NeedsInput }, undefined);
1185
if (!lastStartedWaitingAt) {
1186
lastStartedWaitingAt = pending.startedWaitingAt;
1187
}
1188
} else if (lastStartedWaitingAt) {
1189
// Restore state to Pending if it was set to NeedsInput by this observable
1190
if (this._modelState.read(reader).value === ResponseModelState.NeedsInput) {
1191
this._modelState.set({ value: ResponseModelState.Pending }, undefined);
1192
}
1193
this._timeSpentWaitingAccumulator += Date.now() - lastStartedWaitingAt;
1194
lastStartedWaitingAt = undefined;
1195
}
1196
1197
return this._timestamp + this._timeSpentWaitingAccumulator;
1198
}).recomputeInitiallyAndOnChange(this._store);
1199
}
1200
1201
initializeCodeBlockInfos(codeBlockInfo: ICodeBlockInfo[]): void {
1202
if (this._codeBlockInfos) {
1203
throw new BugIndicatingError('Code block infos have already been initialized');
1204
}
1205
this._codeBlockInfos = [...codeBlockInfo];
1206
}
1207
1208
setBlockedState(isBlocked: boolean): void {
1209
this._shouldBeBlocked.set(isBlocked, undefined);
1210
}
1211
1212
/**
1213
* Apply a progress update to the actual response content.
1214
*/
1215
updateContent(responsePart: IChatProgressResponseContent | IChatTextEdit | IChatNotebookEdit | IChatExternalToolInvocationUpdate, quiet?: boolean) {
1216
this._response.updateContent(responsePart, quiet);
1217
}
1218
1219
/**
1220
* Adds an undo stop at the current position in the stream.
1221
*/
1222
addUndoStop(undoStop: IChatUndoStop) {
1223
this._onDidChange.fire({ reason: 'undoStop', id: undoStop.id });
1224
this._response.updateContent(undoStop, true);
1225
}
1226
1227
/**
1228
* Apply one of the progress updates that are not part of the actual response content.
1229
*/
1230
applyReference(progress: IChatUsedContext | IChatContentReference) {
1231
if (progress.kind === 'usedContext') {
1232
this._usedContext = progress;
1233
} else if (progress.kind === 'reference') {
1234
this._contentReferences.push(progress);
1235
this._onDidChange.fire(defaultChatResponseModelChangeReason);
1236
}
1237
}
1238
1239
applyCodeCitation(progress: IChatCodeCitation) {
1240
this._codeCitations.push(progress);
1241
this._response.addCitation(progress);
1242
this._onDidChange.fire(defaultChatResponseModelChangeReason);
1243
}
1244
1245
setAgent(agent: IChatAgentData, slashCommand?: IChatAgentCommand) {
1246
this._agent = agent;
1247
this._slashCommand = slashCommand;
1248
this._agentOrSlashCommandDetected = !agent.isDefault || !!slashCommand;
1249
this._onDidChange.fire(defaultChatResponseModelChangeReason);
1250
}
1251
1252
setResult(result: IChatAgentResult): void {
1253
this._result = result;
1254
this._onDidChange.fire(defaultChatResponseModelChangeReason);
1255
}
1256
1257
setUsage(usage: IChatUsage): void {
1258
this._usage = usage;
1259
this._onDidChange.fire(defaultChatResponseModelChangeReason);
1260
}
1261
1262
complete(): void {
1263
// No-op if it's already complete
1264
if (this.isComplete) {
1265
return;
1266
}
1267
if (this._result?.errorDetails?.responseIsRedacted) {
1268
this._response.clear();
1269
}
1270
1271
// Canceled sessions can be considered 'Complete'
1272
const state = !!this._result?.errorDetails && this._result.errorDetails.code !== 'canceled' ? ResponseModelState.Failed : ResponseModelState.Complete;
1273
this._modelState.set({ value: state, completedAt: Date.now() }, undefined);
1274
this._onDidChange.fire({ reason: 'completedRequest' });
1275
}
1276
1277
cancel(): void {
1278
this._modelState.set({ value: ResponseModelState.Cancelled, completedAt: Date.now() }, undefined);
1279
this._onDidChange.fire({ reason: 'completedRequest' });
1280
}
1281
1282
setFollowups(followups: IChatFollowup[] | undefined): void {
1283
this._followups = followups;
1284
this._onDidChange.fire(defaultChatResponseModelChangeReason); // Fire so that command followups get rendered on the row
1285
}
1286
1287
setVote(vote: ChatAgentVoteDirection): void {
1288
this._vote = vote;
1289
this._onDidChange.fire(defaultChatResponseModelChangeReason);
1290
}
1291
1292
setVoteDownReason(reason: ChatAgentVoteDownReason | undefined): void {
1293
this._voteDownReason = reason;
1294
this._onDidChange.fire(defaultChatResponseModelChangeReason);
1295
}
1296
1297
setEditApplied(edit: IChatTextEditGroup, editCount: number): boolean {
1298
if (!this.response.value.includes(edit)) {
1299
return false;
1300
}
1301
if (!edit.state) {
1302
return false;
1303
}
1304
edit.state.applied = editCount; // must not be edit.edits.length
1305
this._onDidChange.fire(defaultChatResponseModelChangeReason);
1306
return true;
1307
}
1308
1309
adoptTo(session: ChatModel) {
1310
this._session = session;
1311
this._onDidChange.fire(defaultChatResponseModelChangeReason);
1312
}
1313
1314
1315
finalizeUndoState(): void {
1316
this._finalizedResponse = this.response;
1317
this._responseView = undefined;
1318
this._shouldBeRemovedOnSend = undefined;
1319
}
1320
1321
toJSON(): ISerializableChatResponseData {
1322
const modelState = this._modelState.get();
1323
const pendingConfirmation = this.isPendingConfirmation.get();
1324
1325
return {
1326
responseId: this.id,
1327
result: this.result,
1328
responseMarkdownInfo: this.codeBlockInfos?.map<ISerializableMarkdownInfo>(info => ({ suggestionId: info.suggestionId })),
1329
followups: this.followups,
1330
modelState: modelState.value === ResponseModelState.Pending || modelState.value === ResponseModelState.NeedsInput ? { value: ResponseModelState.Cancelled, completedAt: Date.now() } : modelState,
1331
vote: this.vote,
1332
voteDownReason: this.voteDownReason,
1333
slashCommand: this.slashCommand,
1334
usedContext: this.usedContext,
1335
contentReferences: this.contentReferences,
1336
codeCitations: this.codeCitations,
1337
timestamp: this._timestamp,
1338
timeSpentWaiting: (pendingConfirmation ? Date.now() - pendingConfirmation.startedWaitingAt : 0) + this._timeSpentWaitingAccumulator,
1339
} satisfies WithDefinedProps<ISerializableChatResponseData>;
1340
}
1341
}
1342
1343
1344
export interface IChatRequestDisablement {
1345
requestId: string;
1346
afterUndoStop?: string;
1347
}
1348
1349
/**
1350
* Information about a chat request that needs user input to continue.
1351
*/
1352
export interface IChatRequestNeedsInputInfo {
1353
/** The chat session title */
1354
readonly title: string;
1355
/** Optional detail message, e.g., "<toolname> needs approval to run." */
1356
readonly detail?: string;
1357
}
1358
1359
export interface IChatModel extends IDisposable {
1360
readonly onDidDispose: Event<void>;
1361
readonly onDidChange: Event<IChatChangeEvent>;
1362
/** @deprecated Use {@link sessionResource} instead */
1363
readonly sessionId: string;
1364
/** Milliseconds timestamp this chat model was created. */
1365
readonly timestamp: number;
1366
readonly timing: IChatSessionTiming;
1367
readonly sessionResource: URI;
1368
readonly initialLocation: ChatAgentLocation;
1369
readonly title: string;
1370
readonly hasCustomTitle: boolean;
1371
readonly responderUsername: string;
1372
/** True whenever a request is currently running */
1373
readonly requestInProgress: IObservable<boolean>;
1374
/** Provides session information when a request needs user interaction to continue */
1375
readonly requestNeedsInput: IObservable<IChatRequestNeedsInputInfo | undefined>;
1376
readonly inputPlaceholder?: string;
1377
readonly editingSession?: IChatEditingSession | undefined;
1378
readonly checkpoint: IChatRequestModel | undefined;
1379
startEditingSession(isGlobalEditingSession?: boolean, transferFromSession?: IChatEditingSession): void;
1380
/** Input model for managing input state */
1381
readonly inputModel: IInputModel;
1382
readonly hasRequests: boolean;
1383
readonly lastRequest: IChatRequestModel | undefined;
1384
/** Whether this model will be kept alive while it is running or has edits */
1385
readonly willKeepAlive: boolean;
1386
readonly lastRequestObs: IObservable<IChatRequestModel | undefined>;
1387
getRequests(): IChatRequestModel[];
1388
setCheckpoint(requestId: string | undefined): void;
1389
1390
toExport(): IExportableChatData;
1391
toJSON(): ISerializableChatData;
1392
readonly contributedChatSession: IChatSessionContext | undefined;
1393
1394
readonly repoData: IExportableRepoData | undefined;
1395
setRepoData(data: IExportableRepoData | undefined): void;
1396
1397
readonly onDidChangePendingRequests: Event<void>;
1398
getPendingRequests(): readonly IChatPendingRequest[];
1399
}
1400
1401
export interface ISerializableChatsData {
1402
[sessionId: string]: ISerializableChatData;
1403
}
1404
1405
export type ISerializableChatAgentData = UriDto<IChatAgentData>;
1406
1407
interface ISerializableChatResponseData {
1408
responseId?: string;
1409
result?: IChatAgentResult; // Optional for backcompat
1410
responseMarkdownInfo?: ISerializableMarkdownInfo[];
1411
followups?: ReadonlyArray<IChatFollowup>;
1412
modelState?: ResponseModelStateT;
1413
vote?: ChatAgentVoteDirection;
1414
voteDownReason?: ChatAgentVoteDownReason;
1415
timestamp?: number;
1416
slashCommand?: IChatAgentCommand;
1417
/** For backward compat: should be optional */
1418
usedContext?: IChatUsedContext;
1419
contentReferences?: ReadonlyArray<IChatContentReference>;
1420
codeCitations?: ReadonlyArray<IChatCodeCitation>;
1421
timeSpentWaiting?: number;
1422
}
1423
1424
export type SerializedChatResponsePart = IMarkdownString | IChatResponseProgressFileTreeData | IChatContentInlineReference | IChatAgentMarkdownContentWithVulnerability | IChatThinkingPart | IChatProgressResponseContentSerialized | IChatQuestionCarousel;
1425
1426
export interface ISerializableChatRequestData extends ISerializableChatResponseData {
1427
requestId: string;
1428
message: string | IParsedChatRequest; // string => old format
1429
/** Is really like "prompt data". This is the message in the format in which the agent gets it + variable values. */
1430
variableData: IChatRequestVariableData;
1431
response: ReadonlyArray<SerializedChatResponsePart> | undefined;
1432
1433
/**Old, persisted name for shouldBeRemovedOnSend */
1434
isHidden?: boolean;
1435
shouldBeRemovedOnSend?: IChatRequestDisablement;
1436
agent?: ISerializableChatAgentData;
1437
// responseErrorDetails: IChatResponseErrorDetails | undefined;
1438
/** @deprecated modelState is used instead now */
1439
isCanceled?: boolean;
1440
timestamp?: number;
1441
confirmation?: string;
1442
editedFileEvents?: IChatAgentEditedFileEvent[];
1443
modelId?: string;
1444
}
1445
1446
export interface ISerializableMarkdownInfo {
1447
readonly suggestionId: EditSuggestionId;
1448
}
1449
1450
/**
1451
* Repository state captured for chat session export.
1452
* Enables reproducing the workspace state by cloning, checking out the commit, and applying diffs.
1453
*/
1454
export interface IExportableRepoData {
1455
/**
1456
* Classification of the workspace's version control state.
1457
* - `remote-git`: Git repo with a configured remote URL
1458
* - `local-git`: Git repo without any remote (local only)
1459
* - `plain-folder`: Not a git repository
1460
*/
1461
workspaceType: 'remote-git' | 'local-git' | 'plain-folder';
1462
1463
/**
1464
* Sync status between local and remote.
1465
* - `synced`: Local HEAD matches remote tracking branch (fully pushed)
1466
* - `unpushed`: Local has commits not pushed to the remote tracking branch
1467
* - `unpublished`: Local branch has no remote tracking branch configured
1468
* - `local-only`: No remote configured (local git repo only)
1469
* - `no-git`: Not a git repository
1470
*/
1471
syncStatus: 'synced' | 'unpushed' | 'unpublished' | 'local-only' | 'no-git';
1472
1473
/**
1474
* Remote URL of the repository (e.g., https://github.com/org/repo.git).
1475
* Undefined if no remote is configured.
1476
*/
1477
remoteUrl?: string;
1478
1479
/**
1480
* Vendor/host of the remote repository.
1481
* Undefined if no remote is configured.
1482
*/
1483
remoteVendor?: 'github' | 'ado' | 'other';
1484
1485
/**
1486
* Remote tracking branch for the current branch (e.g., "origin/feature/my-work").
1487
* Undefined if branch is unpublished or no remote.
1488
*/
1489
remoteTrackingBranch?: string;
1490
1491
/**
1492
* Default remote branch used as base for unpublished branches (e.g., "origin/main").
1493
* Helpful for computing merge-base when branch has no tracking.
1494
*/
1495
remoteBaseBranch?: string;
1496
1497
/**
1498
* Commit hash of the remote tracking branch HEAD.
1499
* Undefined if branch has no remote tracking branch.
1500
*/
1501
remoteHeadCommit?: string;
1502
1503
/**
1504
* Name of the current local branch (e.g., "feature/my-work").
1505
*/
1506
localBranch?: string;
1507
1508
/**
1509
* Commit hash of the local HEAD when captured.
1510
*/
1511
localHeadCommit?: string;
1512
1513
/**
1514
* Working tree diffs (uncommitted changes).
1515
*/
1516
diffs?: IExportableRepoDiff[];
1517
1518
/**
1519
* Status of the diffs collection.
1520
* - `included`: Diffs were successfully captured and included
1521
* - `tooManyChanges`: Diffs skipped because >100 files changed (degenerate case like mass renames)
1522
* - `tooLarge`: Diffs skipped because total size exceeded 900KB
1523
* - `trimmedForStorage`: Diffs were trimmed to save storage (older session)
1524
* - `noChanges`: No working tree changes detected
1525
* - `notCaptured`: Diffs not captured (default/undefined case)
1526
*/
1527
diffsStatus?: 'included' | 'tooManyChanges' | 'tooLarge' | 'trimmedForStorage' | 'noChanges' | 'notCaptured';
1528
1529
/**
1530
* Number of changed files detected, even if diffs were not included.
1531
*/
1532
changedFileCount?: number;
1533
}
1534
1535
/**
1536
* A file change exported as a unified diff patch compatible with `git apply`.
1537
*/
1538
export interface IExportableRepoDiff {
1539
relativePath: string;
1540
changeType: 'added' | 'modified' | 'deleted' | 'renamed';
1541
oldRelativePath?: string;
1542
unifiedDiff?: string;
1543
status: string;
1544
}
1545
1546
export interface IExportableChatData {
1547
initialLocation: ChatAgentLocation | undefined;
1548
requests: ISerializableChatRequestData[];
1549
responderUsername: string;
1550
}
1551
1552
/*
1553
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.
1554
*/
1555
1556
export interface ISerializableChatData1 extends IExportableChatData {
1557
sessionId: string;
1558
creationDate: number;
1559
}
1560
1561
export interface ISerializableChatData2 extends ISerializableChatData1 {
1562
version: 2;
1563
computedTitle: string | undefined;
1564
}
1565
1566
export interface ISerializableChatData3 extends Omit<ISerializableChatData2, 'version' | 'computedTitle'> {
1567
version: 3;
1568
customTitle: string | undefined;
1569
/**
1570
* Whether the session had pending edits when it was stored.
1571
* todo@connor4312 This will be cleaned up with the globalization of edits.
1572
*/
1573
hasPendingEdits?: boolean;
1574
/** Current draft input state (added later, fully backwards compatible) */
1575
inputState?: ISerializableChatModelInputState;
1576
repoData?: IExportableRepoData;
1577
/** Pending requests that were queued but not yet processed */
1578
pendingRequests?: ISerializablePendingRequestData[];
1579
}
1580
1581
/**
1582
* Input model for managing chat input state independently from the chat model.
1583
* This keeps display logic separated from the core chat model.
1584
*
1585
* The input model:
1586
* - Manages the current draft state (text, attachments, mode, model selection, cursor/selection)
1587
* - Provides an observable interface for reactive UI updates
1588
* - Automatically persists through the chat model's serialization
1589
* - Enables bidirectional sync between the UI (ChatInputPart) and the model
1590
* - Uses `undefined` state to indicate no persisted state (new/empty chat)
1591
*
1592
* This architecture ensures that:
1593
* - Input state is preserved when moving chats between editor/sidebar/window
1594
* - No manual state transfer is needed when switching contexts
1595
* - The UI stays in sync with the persisted state
1596
* - New chats use UI defaults (persisted preferences) instead of hardcoded values
1597
*/
1598
export interface IInputModel {
1599
/** Observable for current input state (undefined for new/uninitialized chats) */
1600
readonly state: IObservable<IChatModelInputState | undefined>;
1601
1602
/** Update the input state (partial update) */
1603
setState(state: Partial<IChatModelInputState>): void;
1604
1605
/** Clear input state (after sending or clearing) */
1606
clearState(): void;
1607
1608
/** Serializes the state */
1609
toJSON(): ISerializableChatModelInputState | undefined;
1610
}
1611
1612
/**
1613
* Represents the current state of the chat input that hasn't been sent yet.
1614
* This is the "draft" state that should be preserved across sessions.
1615
*/
1616
export interface IChatModelInputState {
1617
/** Current attachments in the input */
1618
attachments: readonly IChatRequestVariableEntry[];
1619
1620
/** Currently selected chat mode */
1621
mode: {
1622
/** Mode ID (e.g., 'ask', 'edit', 'agent', or custom mode ID) */
1623
id: string;
1624
/** Mode kind for builtin modes */
1625
kind: ChatModeKind | undefined;
1626
};
1627
1628
/** Currently selected language model, if any */
1629
selectedModel: ILanguageModelChatMetadataAndIdentifier | undefined;
1630
1631
/** Current input text */
1632
inputText: string;
1633
1634
/** Current selection ranges */
1635
selections: ISelection[];
1636
1637
/** Contributed stored state */
1638
contrib: Record<string, unknown>;
1639
}
1640
1641
/**
1642
* Serializable version of IChatModelInputState
1643
*/
1644
export interface ISerializableChatModelInputState {
1645
attachments: readonly IChatRequestVariableEntry[];
1646
mode: {
1647
id: string;
1648
kind: ChatModeKind | undefined;
1649
};
1650
selectedModel: {
1651
identifier: string;
1652
metadata: ILanguageModelChatMetadata;
1653
} | undefined;
1654
inputText: string;
1655
selections: ISelection[];
1656
contrib: Record<string, unknown>;
1657
}
1658
1659
/**
1660
* Chat data that has been parsed and normalized to the current format.
1661
*/
1662
export type ISerializableChatData = ISerializableChatData3;
1663
1664
export type IChatDataSerializerLog = ObjectMutationLog<IChatModel, ISerializableChatData>;
1665
1666
export interface ISerializedChatDataReference {
1667
value: ISerializableChatData | IExportableChatData;
1668
serializer: IChatDataSerializerLog;
1669
}
1670
1671
/**
1672
* Chat data that has been loaded but not normalized, and could be any format
1673
*/
1674
export type ISerializableChatDataIn = ISerializableChatData1 | ISerializableChatData2 | ISerializableChatData3;
1675
1676
/**
1677
* Normalize chat data from storage to the current format.
1678
* TODO- ChatModel#_deserialize and reviveSerializedAgent also still do some normalization and maybe that should be done in here too.
1679
*/
1680
export function normalizeSerializableChatData(raw: ISerializableChatDataIn): ISerializableChatData {
1681
normalizeOldFields(raw);
1682
1683
if (!('version' in raw)) {
1684
return {
1685
version: 3,
1686
...raw,
1687
customTitle: undefined,
1688
};
1689
}
1690
1691
if (raw.version === 2) {
1692
return {
1693
...raw,
1694
version: 3,
1695
customTitle: raw.computedTitle
1696
};
1697
}
1698
1699
return raw;
1700
}
1701
1702
function normalizeOldFields(raw: ISerializableChatDataIn): void {
1703
// Fill in fields that very old chat data may be missing
1704
if (!raw.sessionId) {
1705
raw.sessionId = generateUuid();
1706
}
1707
1708
if (!raw.creationDate) {
1709
raw.creationDate = getLastYearDate();
1710
}
1711
1712
// eslint-disable-next-line @typescript-eslint/no-explicit-any, local/code-no-any-casts
1713
if ((raw.initialLocation as any) === 'editing-session') {
1714
raw.initialLocation = ChatAgentLocation.Chat;
1715
}
1716
}
1717
1718
function getLastYearDate(): number {
1719
const lastYearDate = new Date();
1720
lastYearDate.setFullYear(lastYearDate.getFullYear() - 1);
1721
return lastYearDate.getTime();
1722
}
1723
1724
export function isExportableSessionData(obj: unknown): obj is IExportableChatData {
1725
return !!obj &&
1726
Array.isArray((obj as IExportableChatData).requests) &&
1727
typeof (obj as IExportableChatData).responderUsername === 'string';
1728
}
1729
1730
export function isSerializableSessionData(obj: unknown): obj is ISerializableChatData {
1731
const data = obj as ISerializableChatData;
1732
return isExportableSessionData(obj) &&
1733
typeof data.creationDate === 'number' &&
1734
typeof data.sessionId === 'string' &&
1735
obj.requests.every((request: ISerializableChatRequestData) =>
1736
!request.usedContext /* for backward compat allow missing usedContext */ || isIUsedContext(request.usedContext)
1737
);
1738
}
1739
1740
export type IChatChangeEvent =
1741
| IChatInitEvent
1742
| IChatAddRequestEvent | IChatChangedRequestEvent | IChatRemoveRequestEvent
1743
| IChatAddResponseEvent
1744
| IChatSetAgentEvent
1745
| IChatMoveEvent
1746
| IChatSetHiddenEvent
1747
| IChatCompletedRequestEvent
1748
| IChatSetCustomTitleEvent
1749
;
1750
1751
export interface IChatAddRequestEvent {
1752
kind: 'addRequest';
1753
request: IChatRequestModel;
1754
}
1755
1756
export interface IChatChangedRequestEvent {
1757
kind: 'changedRequest';
1758
request: IChatRequestModel;
1759
}
1760
1761
export interface IChatCompletedRequestEvent {
1762
kind: 'completedRequest';
1763
request: IChatRequestModel;
1764
}
1765
1766
export interface IChatAddResponseEvent {
1767
kind: 'addResponse';
1768
response: IChatResponseModel;
1769
}
1770
1771
export const enum ChatRequestRemovalReason {
1772
/**
1773
* "Normal" remove
1774
*/
1775
Removal,
1776
1777
/**
1778
* Removed because the request will be resent
1779
*/
1780
Resend,
1781
1782
/**
1783
* Remove because the request is moving to another model
1784
*/
1785
Adoption
1786
}
1787
1788
export interface IChatRemoveRequestEvent {
1789
kind: 'removeRequest';
1790
requestId: string;
1791
responseId?: string;
1792
reason: ChatRequestRemovalReason;
1793
}
1794
1795
export interface IChatSetHiddenEvent {
1796
kind: 'setHidden';
1797
}
1798
1799
export interface IChatMoveEvent {
1800
kind: 'move';
1801
target: URI;
1802
range: IRange;
1803
}
1804
1805
export interface IChatSetAgentEvent {
1806
kind: 'setAgent';
1807
agent: IChatAgentData;
1808
command?: IChatAgentCommand;
1809
}
1810
1811
export interface IChatSetCustomTitleEvent {
1812
kind: 'setCustomTitle';
1813
title: string;
1814
}
1815
1816
export interface IChatInitEvent {
1817
kind: 'initialize';
1818
}
1819
1820
/**
1821
* Internal implementation of IInputModel
1822
*/
1823
class InputModel implements IInputModel {
1824
private readonly _state: ReturnType<typeof observableValue<IChatModelInputState | undefined>>;
1825
readonly state: IObservable<IChatModelInputState | undefined>;
1826
1827
constructor(initialState: IChatModelInputState | undefined) {
1828
this._state = observableValueOpts({ debugName: 'inputModelState', equalsFn: equals }, initialState);
1829
this.state = this._state;
1830
}
1831
1832
setState(state: Partial<IChatModelInputState>): void {
1833
const current = this._state.get();
1834
this._state.set({
1835
// If current is undefined, provide defaults for required fields
1836
attachments: [],
1837
mode: { id: 'agent', kind: ChatModeKind.Agent },
1838
selectedModel: undefined,
1839
inputText: '',
1840
selections: [],
1841
contrib: {},
1842
...current,
1843
...state
1844
}, undefined);
1845
}
1846
1847
clearState(): void {
1848
this._state.set(undefined, undefined);
1849
}
1850
1851
toJSON(): ISerializableChatModelInputState | undefined {
1852
const value = this.state.get();
1853
if (!value) {
1854
return undefined;
1855
}
1856
1857
// Filter out extension-contributed context items (kind: 'string' or implicit entries with StringChatContextValue)
1858
// These have handles that become invalid after window reload and cannot be properly restored.
1859
const persistableAttachments = value.attachments.filter(attachment => {
1860
if (isStringVariableEntry(attachment)) {
1861
return false;
1862
}
1863
if (isImplicitVariableEntry(attachment) && isStringImplicitContextValue(attachment.value)) {
1864
return false;
1865
}
1866
return true;
1867
});
1868
1869
return {
1870
contrib: value.contrib,
1871
attachments: persistableAttachments,
1872
mode: value.mode,
1873
selectedModel: value.selectedModel ? {
1874
identifier: value.selectedModel.identifier,
1875
metadata: value.selectedModel.metadata
1876
} : undefined,
1877
inputText: value.inputText,
1878
selections: value.selections
1879
};
1880
}
1881
}
1882
1883
export class ChatModel extends Disposable implements IChatModel {
1884
static getDefaultTitle(requests: (ISerializableChatRequestData | IChatRequestModel)[]): string {
1885
const firstRequestMessage = requests.at(0)?.message ?? '';
1886
const message = typeof firstRequestMessage === 'string' ?
1887
firstRequestMessage :
1888
firstRequestMessage.text;
1889
return message.split('\n')[0].substring(0, 200);
1890
}
1891
1892
private readonly _onDidDispose = this._register(new Emitter<void>());
1893
readonly onDidDispose = this._onDidDispose.event;
1894
1895
private readonly _onDidChange = this._register(new Emitter<IChatChangeEvent>());
1896
readonly onDidChange = this._onDidChange.event;
1897
1898
private readonly _pendingRequests: IChatPendingRequest[] = [];
1899
private readonly _onDidChangePendingRequests = this._register(new Emitter<void>());
1900
readonly onDidChangePendingRequests = this._onDidChangePendingRequests.event;
1901
1902
private _requests: ChatRequestModel[];
1903
1904
private _contributedChatSession: IChatSessionContext | undefined;
1905
public get contributedChatSession(): IChatSessionContext | undefined {
1906
return this._contributedChatSession;
1907
}
1908
public setContributedChatSession(session: IChatSessionContext | undefined) {
1909
this._contributedChatSession = session;
1910
}
1911
1912
private _repoData: IExportableRepoData | undefined;
1913
public get repoData(): IExportableRepoData | undefined {
1914
return this._repoData;
1915
}
1916
public setRepoData(data: IExportableRepoData | undefined): void {
1917
this._repoData = data;
1918
}
1919
1920
getPendingRequests(): readonly IChatPendingRequest[] {
1921
return this._pendingRequests;
1922
}
1923
1924
setPendingRequests(requests: readonly { requestId: string; kind: ChatRequestQueueKind }[]): void {
1925
const existingMap = new Map(this._pendingRequests.map(p => [p.request.id, p]));
1926
const newPending: IChatPendingRequest[] = [];
1927
for (const { requestId, kind } of requests) {
1928
const existing = existingMap.get(requestId);
1929
if (existing) {
1930
// Update kind if changed, keep existing request and sendOptions
1931
newPending.push(existing.kind === kind ? existing : { request: existing.request, kind, sendOptions: existing.sendOptions });
1932
}
1933
}
1934
this._pendingRequests.length = 0;
1935
this._pendingRequests.push(...newPending);
1936
this._onDidChangePendingRequests.fire();
1937
}
1938
1939
/**
1940
* @internal Used by ChatService to add a request to the queue.
1941
* Steering messages are placed before queued messages.
1942
*/
1943
addPendingRequest(request: ChatRequestModel, kind: ChatRequestQueueKind, sendOptions: IChatSendRequestOptions): IChatPendingRequest {
1944
const pendingRequest: IChatPendingRequest = {
1945
request,
1946
kind,
1947
sendOptions,
1948
};
1949
1950
if (kind === ChatRequestQueueKind.Steering) {
1951
// Insert after the last steering message, or at the beginning if there is none
1952
let insertIndex = 0;
1953
for (let i = 0; i < this._pendingRequests.length; i++) {
1954
if (this._pendingRequests[i].kind === ChatRequestQueueKind.Steering) {
1955
insertIndex = i + 1;
1956
} else {
1957
break;
1958
}
1959
}
1960
this._pendingRequests.splice(insertIndex, 0, pendingRequest);
1961
} else {
1962
// Queued messages always go at the end
1963
this._pendingRequests.push(pendingRequest);
1964
}
1965
1966
this._onDidChangePendingRequests.fire();
1967
return pendingRequest;
1968
}
1969
1970
/**
1971
* @internal Used by ChatService to remove a pending request
1972
*/
1973
removePendingRequest(id: string): void {
1974
const index = this._pendingRequests.findIndex(r => r.request.id === id);
1975
if (index !== -1) {
1976
this._pendingRequests.splice(index, 1);
1977
this._onDidChangePendingRequests.fire();
1978
}
1979
}
1980
1981
/**
1982
* @internal Used by ChatService to dequeue the next pending request
1983
*/
1984
dequeuePendingRequest(): IChatPendingRequest | undefined {
1985
const request = this._pendingRequests.shift();
1986
if (request) {
1987
this._onDidChangePendingRequests.fire();
1988
}
1989
return request;
1990
}
1991
1992
/**
1993
* @internal Used by ChatService to clear all pending requests
1994
*/
1995
clearPendingRequests(): void {
1996
if (this._pendingRequests.length > 0) {
1997
this._pendingRequests.length = 0;
1998
this._onDidChangePendingRequests.fire();
1999
}
2000
}
2001
2002
readonly lastRequestObs: IObservable<IChatRequestModel | undefined>;
2003
2004
// TODO to be clear, this is not the same as the id from the session object, which belongs to the provider.
2005
// It's easier to be able to identify this model before its async initialization is complete
2006
private readonly _sessionId: string;
2007
/** @deprecated Use {@link sessionResource} instead */
2008
get sessionId(): string {
2009
return this._sessionId;
2010
}
2011
2012
private readonly _sessionResource: URI;
2013
get sessionResource(): URI {
2014
return this._sessionResource;
2015
}
2016
2017
readonly requestInProgress: IObservable<boolean>;
2018
readonly requestNeedsInput: IObservable<IChatRequestNeedsInputInfo | undefined>;
2019
2020
/** Input model for managing input state */
2021
readonly inputModel: InputModel;
2022
2023
get hasRequests(): boolean {
2024
return this._requests.length > 0;
2025
}
2026
2027
get lastRequest(): ChatRequestModel | undefined {
2028
return this._requests.at(-1);
2029
}
2030
2031
private _timestamp: number;
2032
get timestamp(): number {
2033
return this._timestamp;
2034
}
2035
2036
get timing(): IChatSessionTiming {
2037
const lastRequest = this._requests.at(-1);
2038
const lastResponse = lastRequest?.response;
2039
const lastRequestStarted = lastRequest?.timestamp;
2040
const lastRequestEnded = lastResponse?.completedAt ?? lastResponse?.timestamp;
2041
return {
2042
created: this._timestamp,
2043
lastRequestStarted,
2044
lastRequestEnded,
2045
};
2046
}
2047
2048
get lastMessageDate(): number {
2049
return this._requests.at(-1)?.timestamp ?? this._timestamp;
2050
}
2051
2052
private get _defaultAgent() {
2053
return this.chatAgentService.getDefaultAgent(ChatAgentLocation.Chat, ChatModeKind.Ask);
2054
}
2055
2056
private readonly _initialResponderUsername: string | undefined;
2057
get responderUsername(): string {
2058
return this._defaultAgent?.fullName ??
2059
this._initialResponderUsername ?? '';
2060
}
2061
2062
private _isImported = false;
2063
get isImported(): boolean {
2064
return this._isImported;
2065
}
2066
2067
private _customTitle: string | undefined;
2068
get customTitle(): string | undefined {
2069
return this._customTitle;
2070
}
2071
2072
get title(): string {
2073
return this._customTitle || ChatModel.getDefaultTitle(this._requests);
2074
}
2075
2076
get hasCustomTitle(): boolean {
2077
return this._customTitle !== undefined;
2078
}
2079
2080
private _editingSession: IChatEditingSession | undefined;
2081
2082
get editingSession(): IChatEditingSession | undefined {
2083
return this._editingSession;
2084
}
2085
2086
private readonly _initialLocation: ChatAgentLocation;
2087
get initialLocation(): ChatAgentLocation {
2088
return this._initialLocation;
2089
}
2090
2091
private readonly _canUseTools: boolean = true;
2092
get canUseTools(): boolean {
2093
return this._canUseTools;
2094
}
2095
2096
private _disableBackgroundKeepAlive: boolean;
2097
get willKeepAlive(): boolean {
2098
return !this._disableBackgroundKeepAlive;
2099
}
2100
2101
public dataSerializer?: IChatDataSerializerLog;
2102
2103
constructor(
2104
dataRef: ISerializedChatDataReference | undefined,
2105
initialModelProps: { initialLocation: ChatAgentLocation; canUseTools: boolean; inputState?: ISerializableChatModelInputState; resource?: URI; sessionId?: string; disableBackgroundKeepAlive?: boolean },
2106
@ILogService private readonly logService: ILogService,
2107
@IChatAgentService private readonly chatAgentService: IChatAgentService,
2108
@IChatEditingService private readonly chatEditingService: IChatEditingService,
2109
@IChatService private readonly chatService: IChatService,
2110
) {
2111
super();
2112
2113
const initialData = dataRef?.value;
2114
const isValidExportedData = isExportableSessionData(initialData);
2115
const isValidFullData = isValidExportedData && isSerializableSessionData(initialData);
2116
if (initialData && !isValidExportedData) {
2117
this.logService.warn(`ChatModel#constructor: Loaded malformed session data: ${JSON.stringify(initialData)}`);
2118
}
2119
2120
this._isImported = !!initialData && isValidExportedData && !isValidFullData;
2121
this._sessionId = (isValidFullData && initialData.sessionId) || initialModelProps.sessionId || generateUuid();
2122
this._sessionResource = initialModelProps.resource ?? LocalChatSessionUri.forSession(this._sessionId);
2123
this._disableBackgroundKeepAlive = initialModelProps.disableBackgroundKeepAlive ?? false;
2124
2125
this._requests = initialData ? this._deserialize(initialData) : [];
2126
this._timestamp = (isValidFullData && initialData.creationDate) || Date.now();
2127
this._customTitle = isValidFullData ? initialData.customTitle : undefined;
2128
2129
// Initialize input model from serialized data (undefined for new chats)
2130
const serializedInputState = initialModelProps.inputState || (isValidFullData && initialData.inputState ? initialData.inputState : undefined);
2131
this.inputModel = new InputModel(serializedInputState && {
2132
attachments: serializedInputState.attachments,
2133
mode: serializedInputState.mode,
2134
selectedModel: serializedInputState.selectedModel && {
2135
identifier: serializedInputState.selectedModel.identifier,
2136
metadata: serializedInputState.selectedModel.metadata
2137
},
2138
contrib: serializedInputState.contrib,
2139
inputText: serializedInputState.inputText,
2140
selections: serializedInputState.selections
2141
});
2142
2143
this.dataSerializer = dataRef?.serializer;
2144
this._initialResponderUsername = initialData?.responderUsername;
2145
2146
this._repoData = isValidFullData && initialData.repoData ? initialData.repoData : undefined;
2147
2148
// Hydrate pending requests from serialized data
2149
if (isValidFullData && initialData.pendingRequests) {
2150
this._pendingRequests = this._deserializePendingRequests(initialData.pendingRequests);
2151
}
2152
2153
this._initialLocation = initialData?.initialLocation ?? initialModelProps.initialLocation;
2154
2155
this._canUseTools = initialModelProps.canUseTools;
2156
2157
this.lastRequestObs = observableFromEvent(this, this.onDidChange, () => this._requests.at(-1));
2158
2159
this._register(autorun(reader => {
2160
const request = this.lastRequestObs.read(reader);
2161
if (!request?.response) {
2162
return;
2163
}
2164
2165
reader.store.add(request.response.onDidChange(async ev => {
2166
if (!this._editingSession || ev.reason !== 'completedRequest') {
2167
return;
2168
}
2169
2170
this._onDidChange.fire({ kind: 'completedRequest', request });
2171
}));
2172
}));
2173
2174
this.requestInProgress = this.lastRequestObs.map((request, r) => {
2175
return request?.response?.isInProgress.read(r) ?? false;
2176
});
2177
2178
this.requestNeedsInput = this.lastRequestObs.map((request, r) => {
2179
const pendingInfo = request?.response?.isPendingConfirmation.read(r);
2180
if (!pendingInfo) {
2181
return undefined;
2182
}
2183
return {
2184
title: this.title,
2185
detail: pendingInfo.detail,
2186
};
2187
});
2188
2189
// Retain a reference to itself when a request is in progress, so the ChatModel stays alive in the background
2190
// only while running a request. TODO also keep it alive for 5min or so so we don't have to dispose/restore too often?
2191
if (this.initialLocation === ChatAgentLocation.Chat && !initialModelProps.disableBackgroundKeepAlive) {
2192
const selfRef = this._register(new MutableDisposable<IChatModelReference>());
2193
this._register(autorun(r => {
2194
const inProgress = this.requestInProgress.read(r);
2195
const needsInput = this.requestNeedsInput.read(r);
2196
const shouldStayAlive = inProgress || !!needsInput;
2197
if (shouldStayAlive && !selfRef.value) {
2198
selfRef.value = chatService.getActiveSessionReference(this._sessionResource);
2199
} else if (!shouldStayAlive && selfRef.value) {
2200
selfRef.clear();
2201
}
2202
}));
2203
}
2204
}
2205
2206
startEditingSession(isGlobalEditingSession?: boolean, transferFromSession?: IChatEditingSession): void {
2207
const session = this._editingSession ??= this._register(
2208
transferFromSession
2209
? this.chatEditingService.transferEditingSession(this, transferFromSession)
2210
: isGlobalEditingSession
2211
? this.chatEditingService.startOrContinueGlobalEditingSession(this)
2212
: this.chatEditingService.createEditingSession(this)
2213
);
2214
2215
if (!this._disableBackgroundKeepAlive) {
2216
// todo@connor4312: hold onto a reference so background sessions don't
2217
// trigger early disposal. This will be cleaned up with the globalization of edits.
2218
const selfRef = this._register(new MutableDisposable<IChatModelReference>());
2219
this._register(autorun(r => {
2220
const hasModified = session.entries.read(r).some(e => e.state.read(r) === ModifiedFileEntryState.Modified);
2221
if (hasModified && !selfRef.value) {
2222
selfRef.value = this.chatService.getActiveSessionReference(this._sessionResource);
2223
} else if (!hasModified && selfRef.value) {
2224
selfRef.clear();
2225
}
2226
}));
2227
}
2228
2229
this._register(autorun(reader => {
2230
this._setDisabledRequests(session.requestDisablement.read(reader));
2231
}));
2232
}
2233
2234
private currentEditedFileEvents = new ResourceMap<IChatAgentEditedFileEvent>();
2235
notifyEditingAction(action: IChatEditingSessionAction): void {
2236
const state = action.outcome === 'accepted' ? ChatRequestEditedFileEventKind.Keep :
2237
action.outcome === 'rejected' ? ChatRequestEditedFileEventKind.Undo :
2238
action.outcome === 'userModified' ? ChatRequestEditedFileEventKind.UserModification : null;
2239
if (state === null) {
2240
return;
2241
}
2242
2243
if (!this.currentEditedFileEvents.has(action.uri) || this.currentEditedFileEvents.get(action.uri)?.eventKind === ChatRequestEditedFileEventKind.Keep) {
2244
this.currentEditedFileEvents.set(action.uri, { eventKind: state, uri: action.uri });
2245
}
2246
}
2247
2248
private _deserialize(obj: IExportableChatData | ISerializedChatDataReference): ChatRequestModel[] {
2249
const requests = hasKey(obj, { serializer: true }) ? obj.value.requests : obj.requests;
2250
if (!Array.isArray(requests)) {
2251
this.logService.error(`Ignoring malformed session data: ${JSON.stringify(obj)}`);
2252
return [];
2253
}
2254
2255
try {
2256
return requests.map(r => this._deserializeRequest(r));
2257
} catch (error) {
2258
this.logService.error('Failed to parse chat data', error);
2259
return [];
2260
}
2261
}
2262
2263
private _deserializeRequest(raw: ISerializableChatRequestData): ChatRequestModel {
2264
const parsedRequest =
2265
typeof raw.message === 'string'
2266
? this.getParsedRequestFromString(raw.message)
2267
: reviveParsedChatRequest(raw.message);
2268
2269
// Old messages don't have variableData, or have it in the wrong (non-array) shape
2270
const variableData: IChatRequestVariableData = this.reviveVariableData(raw.variableData);
2271
const request = new ChatRequestModel({
2272
session: this,
2273
message: parsedRequest,
2274
variableData,
2275
timestamp: raw.timestamp ?? -1,
2276
restoredId: raw.requestId,
2277
confirmation: raw.confirmation,
2278
editedFileEvents: raw.editedFileEvents,
2279
modelId: raw.modelId,
2280
});
2281
request.shouldBeRemovedOnSend = raw.isHidden ? { requestId: raw.requestId } : raw.shouldBeRemovedOnSend;
2282
// eslint-disable-next-line @typescript-eslint/no-explicit-any, local/code-no-any-casts
2283
if (raw.response || raw.result || (raw as any).responseErrorDetails) {
2284
const agent = (raw.agent && 'metadata' in raw.agent) ? // Check for the new format, ignore entries in the old format
2285
reviveSerializedAgent(raw.agent) : undefined;
2286
2287
// Port entries from old format
2288
const result = 'responseErrorDetails' in raw ?
2289
// eslint-disable-next-line local/code-no-dangerous-type-assertions
2290
{ errorDetails: raw.responseErrorDetails } as IChatAgentResult : raw.result;
2291
let modelState = raw.modelState || { value: raw.isCanceled ? ResponseModelState.Cancelled : ResponseModelState.Complete, completedAt: Date.now() };
2292
if (modelState.value === ResponseModelState.Pending || modelState.value === ResponseModelState.NeedsInput) {
2293
modelState = { value: ResponseModelState.Cancelled, completedAt: Date.now() };
2294
}
2295
2296
request.response = new ChatResponseModel({
2297
responseContent: raw.response ?? [new MarkdownString(raw.response)],
2298
session: this,
2299
agent,
2300
slashCommand: raw.slashCommand,
2301
requestId: request.id,
2302
modelState,
2303
vote: raw.vote,
2304
timestamp: raw.timestamp,
2305
voteDownReason: raw.voteDownReason,
2306
result,
2307
followups: raw.followups,
2308
restoredId: raw.responseId,
2309
timeSpentWaiting: raw.timeSpentWaiting,
2310
shouldBeBlocked: request.shouldBeBlocked.get(),
2311
codeBlockInfos: raw.responseMarkdownInfo?.map<ICodeBlockInfo>(info => ({ suggestionId: info.suggestionId })),
2312
});
2313
request.response.shouldBeRemovedOnSend = raw.isHidden ? { requestId: raw.requestId } : raw.shouldBeRemovedOnSend;
2314
if (raw.usedContext) { // @ulugbekna: if this's a new vscode sessions, doc versions are incorrect anyway?
2315
request.response.applyReference(revive(raw.usedContext));
2316
}
2317
2318
raw.contentReferences?.forEach(r => request.response!.applyReference(revive(r)));
2319
raw.codeCitations?.forEach(c => request.response!.applyCodeCitation(revive(c)));
2320
}
2321
return request;
2322
}
2323
2324
private reviveVariableData(raw: IChatRequestVariableData): IChatRequestVariableData {
2325
const variableData = raw && Array.isArray(raw.variables)
2326
? raw :
2327
{ variables: [] };
2328
2329
variableData.variables = variableData.variables.map<IChatRequestVariableEntry>(IChatRequestVariableEntry.fromExport);
2330
2331
return variableData;
2332
}
2333
2334
private getParsedRequestFromString(message: string): IParsedChatRequest {
2335
// TODO These offsets won't be used, but chat replies need to go through the parser as well
2336
const parts = [new ChatRequestTextPart(new OffsetRange(0, message.length), { startColumn: 1, startLineNumber: 1, endColumn: 1, endLineNumber: 1 }, message)];
2337
return {
2338
text: message,
2339
parts
2340
};
2341
}
2342
2343
/**
2344
* Hydrates pending requests from serialized data.
2345
* For each serialized pending request, finds the matching request model and adds it to the pending queue.
2346
*/
2347
private _deserializePendingRequests(pendingRequests: ISerializablePendingRequestData[]): IChatPendingRequest[] {
2348
try {
2349
return pendingRequests.map(pending => ({
2350
id: pending.id,
2351
request: this._deserializeRequest(pending.request),
2352
kind: pending.kind,
2353
sendOptions: {
2354
...pending.sendOptions,
2355
userSelectedTools: pending.sendOptions.userSelectedTools
2356
? constObservable(pending.sendOptions.userSelectedTools)
2357
: undefined,
2358
}
2359
}));
2360
} catch (e) {
2361
this.logService.error('Failed to parse pending chat requests', e);
2362
return [];
2363
}
2364
}
2365
2366
2367
2368
getRequests(): ChatRequestModel[] {
2369
return this._requests;
2370
}
2371
2372
resetCheckpoint(): void {
2373
for (const request of this._requests) {
2374
request.setShouldBeBlocked(false);
2375
if (request.response) {
2376
request.response.setBlockedState(false);
2377
}
2378
}
2379
}
2380
2381
setCheckpoint(requestId: string | undefined) {
2382
let checkpoint: ChatRequestModel | undefined;
2383
let checkpointIndex = -1;
2384
if (requestId !== undefined) {
2385
this._requests.forEach((request, index) => {
2386
if (request.id === requestId) {
2387
checkpointIndex = index;
2388
checkpoint = request;
2389
request.setShouldBeBlocked(true);
2390
}
2391
});
2392
2393
if (!checkpoint) {
2394
return; // Invalid request ID
2395
}
2396
}
2397
2398
for (let i = this._requests.length - 1; i >= 0; i -= 1) {
2399
const request = this._requests[i];
2400
if (this._checkpoint && !checkpoint) {
2401
request.setShouldBeBlocked(false);
2402
if (request.response) {
2403
request.response.setBlockedState(false);
2404
}
2405
} else if (checkpoint && i >= checkpointIndex) {
2406
request.setShouldBeBlocked(true);
2407
if (request.response) {
2408
request.response.setBlockedState(true);
2409
}
2410
} else if (checkpoint && i < checkpointIndex) {
2411
request.setShouldBeBlocked(false);
2412
if (request.response) {
2413
request.response.setBlockedState(false);
2414
}
2415
}
2416
}
2417
2418
this._checkpoint = checkpoint;
2419
}
2420
2421
private _checkpoint: ChatRequestModel | undefined = undefined;
2422
public get checkpoint() {
2423
return this._checkpoint;
2424
}
2425
2426
private _setDisabledRequests(requestIds: IChatRequestDisablement[]) {
2427
this._requests.forEach((request) => {
2428
const shouldBeRemovedOnSend = requestIds.find(r => r.requestId === request.id);
2429
request.shouldBeRemovedOnSend = shouldBeRemovedOnSend;
2430
if (request.response) {
2431
request.response.shouldBeRemovedOnSend = shouldBeRemovedOnSend;
2432
}
2433
});
2434
2435
this._onDidChange.fire({ kind: 'setHidden' });
2436
}
2437
2438
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 {
2439
const editedFileEvents = [...this.currentEditedFileEvents.values()];
2440
this.currentEditedFileEvents.clear();
2441
const request = new ChatRequestModel({
2442
restoredId: id,
2443
session: this,
2444
message,
2445
variableData,
2446
timestamp: Date.now(),
2447
attempt,
2448
modeInfo,
2449
confirmation,
2450
locationData,
2451
attachedContext: attachments,
2452
isCompleteAddedRequest,
2453
modelId,
2454
editedFileEvents: editedFileEvents.length ? editedFileEvents : undefined,
2455
userSelectedTools,
2456
});
2457
request.response = new ChatResponseModel({
2458
responseContent: [],
2459
session: this,
2460
agent: chatAgent,
2461
slashCommand,
2462
requestId: request.id,
2463
isCompleteAddedRequest,
2464
codeBlockInfos: undefined,
2465
});
2466
2467
this._requests.push(request);
2468
this._onDidChange.fire({ kind: 'addRequest', request });
2469
return request;
2470
}
2471
2472
public setCustomTitle(title: string): void {
2473
this._customTitle = title;
2474
this._onDidChange.fire({ kind: 'setCustomTitle', title });
2475
}
2476
2477
updateRequest(request: ChatRequestModel, variableData: IChatRequestVariableData) {
2478
request.variableData = variableData;
2479
this._onDidChange.fire({ kind: 'changedRequest', request });
2480
}
2481
2482
adoptRequest(request: ChatRequestModel): void {
2483
// this doesn't use `removeRequest` because it must not dispose the request object
2484
const oldOwner = request.session;
2485
const index = oldOwner._requests.findIndex((candidate: ChatRequestModel) => candidate.id === request.id);
2486
2487
if (index === -1) {
2488
return;
2489
}
2490
2491
oldOwner._requests.splice(index, 1);
2492
2493
request.adoptTo(this);
2494
request.response?.adoptTo(this);
2495
this._requests.push(request);
2496
2497
oldOwner._onDidChange.fire({ kind: 'removeRequest', requestId: request.id, responseId: request.response?.id, reason: ChatRequestRemovalReason.Adoption });
2498
this._onDidChange.fire({ kind: 'addRequest', request });
2499
}
2500
2501
acceptResponseProgress(request: ChatRequestModel, progress: IChatProgress, quiet?: boolean): void {
2502
if (!request.response) {
2503
request.response = new ChatResponseModel({
2504
responseContent: [],
2505
session: this,
2506
requestId: request.id,
2507
codeBlockInfos: undefined,
2508
});
2509
}
2510
2511
if (request.response.isComplete) {
2512
throw new Error('acceptResponseProgress: Adding progress to a completed response');
2513
}
2514
2515
if (progress.kind === 'usedContext' || progress.kind === 'reference') {
2516
request.response.applyReference(progress);
2517
} else if (progress.kind === 'codeCitation') {
2518
request.response.applyCodeCitation(progress);
2519
} else if (progress.kind === 'move') {
2520
this._onDidChange.fire({ kind: 'move', target: progress.uri, range: progress.range });
2521
} else if (progress.kind === 'codeblockUri' && progress.isEdit) {
2522
request.response.addUndoStop({ id: progress.undoStopId ?? generateUuid(), kind: 'undoStop' });
2523
request.response.updateContent(progress, quiet);
2524
} else if (progress.kind === 'progressTaskResult') {
2525
// Should have been handled upstream, not sent to model
2526
this.logService.error(`Couldn't handle progress: ${JSON.stringify(progress)}`);
2527
} else {
2528
request.response.updateContent(progress, quiet);
2529
}
2530
}
2531
2532
removeRequest(id: string, reason: ChatRequestRemovalReason = ChatRequestRemovalReason.Removal): void {
2533
const index = this._requests.findIndex(request => request.id === id);
2534
const request = this._requests[index];
2535
2536
if (index !== -1) {
2537
this._onDidChange.fire({ kind: 'removeRequest', requestId: request.id, responseId: request.response?.id, reason });
2538
this._requests.splice(index, 1);
2539
request.response?.dispose();
2540
}
2541
}
2542
2543
cancelRequest(request: ChatRequestModel): void {
2544
if (request.response) {
2545
request.response.cancel();
2546
}
2547
}
2548
2549
setResponse(request: ChatRequestModel, result: IChatAgentResult): void {
2550
if (!request.response) {
2551
request.response = new ChatResponseModel({
2552
responseContent: [],
2553
session: this,
2554
requestId: request.id,
2555
codeBlockInfos: undefined,
2556
});
2557
}
2558
2559
request.response.setResult(result);
2560
}
2561
2562
setFollowups(request: ChatRequestModel, followups: IChatFollowup[] | undefined): void {
2563
if (!request.response) {
2564
// Maybe something went wrong?
2565
return;
2566
}
2567
request.response.setFollowups(followups);
2568
}
2569
2570
setResponseModel(request: ChatRequestModel, response: ChatResponseModel): void {
2571
request.response = response;
2572
this._onDidChange.fire({ kind: 'addResponse', response });
2573
}
2574
2575
toExport(): IExportableChatData {
2576
return {
2577
responderUsername: this.responderUsername,
2578
initialLocation: this.initialLocation,
2579
requests: this._requests.map((r): ISerializableChatRequestData => {
2580
const message = {
2581
...r.message,
2582
// eslint-disable-next-line @typescript-eslint/no-explicit-any
2583
parts: r.message.parts.map((p: any) => p && 'toJSON' in p ? (p.toJSON as Function)() : p)
2584
};
2585
const agent = r.response?.agent;
2586
const agentJson = agent && 'toJSON' in agent ? (agent.toJSON as Function)() :
2587
agent ? { ...agent } : undefined;
2588
return {
2589
requestId: r.id,
2590
message,
2591
variableData: IChatRequestVariableData.toExport(r.variableData),
2592
response: r.response ?
2593
r.response.entireResponse.value.map(item => {
2594
// Keeping the shape of the persisted data the same for back compat
2595
if (item.kind === 'treeData') {
2596
return item.treeData;
2597
} else if (item.kind === 'markdownContent') {
2598
return item.content;
2599
} else {
2600
// eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any
2601
return item as any; // TODO
2602
}
2603
})
2604
: undefined,
2605
shouldBeRemovedOnSend: r.shouldBeRemovedOnSend,
2606
agent: agentJson,
2607
timestamp: r.timestamp,
2608
confirmation: r.confirmation,
2609
editedFileEvents: r.editedFileEvents,
2610
modelId: r.modelId,
2611
...r.response?.toJSON(),
2612
};
2613
}),
2614
};
2615
}
2616
2617
toJSON(): ISerializableChatData {
2618
return {
2619
version: 3,
2620
...this.toExport(),
2621
sessionId: this.sessionId,
2622
creationDate: this._timestamp,
2623
customTitle: this._customTitle,
2624
inputState: this.inputModel.toJSON(),
2625
repoData: this._repoData,
2626
};
2627
}
2628
2629
override dispose() {
2630
this._requests.forEach(r => r.response?.dispose());
2631
this._onDidDispose.fire();
2632
2633
super.dispose();
2634
}
2635
}
2636
2637
export function updateRanges(variableData: IChatRequestVariableData, diff: number): IChatRequestVariableData {
2638
return {
2639
variables: variableData.variables.map(v => ({
2640
...v,
2641
range: v.range && {
2642
start: v.range.start - diff,
2643
endExclusive: v.range.endExclusive - diff
2644
}
2645
}))
2646
};
2647
}
2648
2649
export function canMergeMarkdownStrings(md1: IMarkdownString, md2: IMarkdownString): boolean {
2650
if (md1.baseUri && md2.baseUri) {
2651
const baseUriEquals = md1.baseUri.scheme === md2.baseUri.scheme
2652
&& md1.baseUri.authority === md2.baseUri.authority
2653
&& md1.baseUri.path === md2.baseUri.path
2654
&& md1.baseUri.query === md2.baseUri.query
2655
&& md1.baseUri.fragment === md2.baseUri.fragment;
2656
if (!baseUriEquals) {
2657
return false;
2658
}
2659
} else if (md1.baseUri || md2.baseUri) {
2660
return false;
2661
}
2662
2663
return equals(md1.isTrusted, md2.isTrusted) &&
2664
md1.supportHtml === md2.supportHtml &&
2665
md1.supportThemeIcons === md2.supportThemeIcons;
2666
}
2667
2668
export function appendMarkdownString(md1: IMarkdownString, md2: IMarkdownString | string): IMarkdownString {
2669
const appendedValue = typeof md2 === 'string' ? md2 : md2.value;
2670
return {
2671
value: md1.value + appendedValue,
2672
isTrusted: md1.isTrusted,
2673
supportThemeIcons: md1.supportThemeIcons,
2674
supportHtml: md1.supportHtml,
2675
baseUri: md1.baseUri
2676
};
2677
}
2678
2679
export function getCodeCitationsMessage(citations: ReadonlyArray<IChatCodeCitation>): string {
2680
if (citations.length === 0) {
2681
return '';
2682
}
2683
2684
const licenseTypes = citations.reduce((set, c) => set.add(c.license), new Set<string>());
2685
const label = licenseTypes.size === 1 ?
2686
localize('codeCitation', "Similar code found with 1 license type", licenseTypes.size) :
2687
localize('codeCitations', "Similar code found with {0} license types", licenseTypes.size);
2688
return label;
2689
}
2690
2691
/**
2692
* Converts IChatSendRequestOptions to a serializable format by extracting only
2693
* serializable fields and converting observables to static values.
2694
*/
2695
export function serializeSendOptions(options: IChatSendRequestOptions): ISerializableSendOptions {
2696
return {
2697
modeInfo: options.modeInfo,
2698
userSelectedModelId: options.userSelectedModelId,
2699
userSelectedTools: options.userSelectedTools?.get(),
2700
location: options.location,
2701
locationData: options.locationData,
2702
attempt: options.attempt,
2703
noCommandDetection: options.noCommandDetection,
2704
agentId: options.agentId,
2705
agentIdSilent: options.agentIdSilent,
2706
slashCommand: options.slashCommand,
2707
confirmation: options.confirmation,
2708
};
2709
}
2710
2711
export enum ChatRequestEditedFileEventKind {
2712
Keep = 1,
2713
Undo = 2,
2714
UserModification = 3,
2715
}
2716
2717
export interface IChatAgentEditedFileEvent {
2718
readonly uri: URI;
2719
readonly eventKind: ChatRequestEditedFileEventKind;
2720
}
2721
2722
/** URI for a resource embedded in a chat request/response */
2723
export namespace ChatResponseResource {
2724
export const scheme = 'vscode-chat-response-resource';
2725
2726
export function createUri(sessionResource: URI, toolCallId: string, index: number, basename?: string): URI {
2727
return URI.from({
2728
scheme: ChatResponseResource.scheme,
2729
authority: encodeHex(VSBuffer.fromString(sessionResource.toString())),
2730
path: `/tool/${toolCallId}/${index}` + (basename ? `/${basename}` : ''),
2731
});
2732
}
2733
2734
export function parseUri(uri: URI): undefined | { sessionResource: URI; toolCallId: string; index: number } {
2735
if (uri.scheme !== ChatResponseResource.scheme) {
2736
return undefined;
2737
}
2738
2739
const parts = uri.path.split('/');
2740
if (parts.length < 5) {
2741
return undefined;
2742
}
2743
2744
const [, kind, toolCallId, index] = parts;
2745
if (kind !== 'tool') {
2746
return undefined;
2747
}
2748
2749
let sessionResource: URI;
2750
try {
2751
sessionResource = URI.parse(decodeHex(uri.authority).toString());
2752
} catch (e) {
2753
if (e instanceof SyntaxError) { // pre-1.108 local session ID
2754
sessionResource = LocalChatSessionUri.forSession(uri.authority);
2755
} else {
2756
throw e;
2757
}
2758
}
2759
2760
return {
2761
sessionResource,
2762
toolCallId: toolCallId,
2763
index: Number(index),
2764
};
2765
}
2766
}
2767
2768