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