Path: blob/main/src/vs/workbench/contrib/chat/common/chatModel.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { asArray } from '../../../../base/common/arrays.js';6import { Emitter, Event } from '../../../../base/common/event.js';7import { IMarkdownString, MarkdownString, isMarkdownString } from '../../../../base/common/htmlContent.js';8import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js';9import { ResourceMap } from '../../../../base/common/map.js';10import { revive } from '../../../../base/common/marshalling.js';11import { Schemas } from '../../../../base/common/network.js';12import { equals } from '../../../../base/common/objects.js';13import { IObservable, ObservablePromise, observableFromEvent, observableSignalFromEvent } from '../../../../base/common/observable.js';14import { basename, isEqual } from '../../../../base/common/resources.js';15import { ThemeIcon } from '../../../../base/common/themables.js';16import { URI, UriComponents, UriDto, isUriComponents } from '../../../../base/common/uri.js';17import { generateUuid } from '../../../../base/common/uuid.js';18import { IRange } from '../../../../editor/common/core/range.js';19import { OffsetRange } from '../../../../editor/common/core/ranges/offsetRange.js';20import { TextEdit } from '../../../../editor/common/languages.js';21import { localize } from '../../../../nls.js';22import { ILogService } from '../../../../platform/log/common/log.js';23import { CellUri, ICellEditOperation } from '../../notebook/common/notebookCommon.js';24import { IChatAgentCommand, IChatAgentData, IChatAgentResult, IChatAgentService, reviveSerializedAgent } from './chatAgents.js';25import { migrateLegacyTerminalToolSpecificData } from './chat.js';26import { IChatEditingService, IChatEditingSession } from './chatEditingService.js';27import { ChatRequestTextPart, IParsedChatRequest, reviveParsedChatRequest } from './chatParserTypes.js';28import { 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';29import { IChatRequestVariableEntry, ChatRequestToolReferenceEntry } from './chatVariableEntries.js';30import { ChatAgentLocation, ChatModeKind } from './constants.js';31import { EditSuggestionId } from '../../../../editor/common/textModelEditSource.js';32import { BugIndicatingError } from '../../../../base/common/errors.js';333435export const CHAT_ATTACHABLE_IMAGE_MIME_TYPES: Record<string, string> = {36png: 'image/png',37jpg: 'image/jpeg',38jpeg: 'image/jpeg',39gif: 'image/gif',40webp: 'image/webp',41};4243export function getAttachableImageExtension(mimeType: string): string | undefined {44return Object.entries(CHAT_ATTACHABLE_IMAGE_MIME_TYPES).find(([_, value]) => value === mimeType)?.[0];45}4647export interface IChatRequestVariableData {48variables: IChatRequestVariableEntry[];49}5051export interface IChatRequestModel {52readonly id: string;53readonly timestamp: number;54readonly username: string;55readonly modeInfo?: IChatRequestModeInfo;56readonly avatarIconUri?: URI;57readonly session: IChatModel;58readonly message: IParsedChatRequest;59readonly attempt: number;60readonly variableData: IChatRequestVariableData;61readonly confirmation?: string;62readonly locationData?: IChatLocationData;63readonly attachedContext?: IChatRequestVariableEntry[];64readonly isCompleteAddedRequest: boolean;65readonly response?: IChatResponseModel;66readonly editedFileEvents?: IChatAgentEditedFileEvent[];67shouldBeRemovedOnSend: IChatRequestDisablement | undefined;68shouldBeBlocked: boolean;69readonly modelId?: string;70}7172export interface ICodeBlockInfo {73readonly suggestionId: EditSuggestionId;74}7576export interface IChatTextEditGroupState {77sha1: string;78applied: number;79}8081export interface IChatTextEditGroup {82uri: URI;83edits: TextEdit[][];84state?: IChatTextEditGroupState;85kind: 'textEditGroup';86done: boolean | undefined;87}8889export function isCellTextEditOperation(value: unknown): value is ICellTextEditOperation {90const candidate = value as ICellTextEditOperation;91return !!candidate && !!candidate.edit && !!candidate.uri && URI.isUri(candidate.uri);92}9394export interface ICellTextEditOperation {95edit: TextEdit;96uri: URI;97}9899export interface IChatNotebookEditGroup {100uri: URI;101edits: (ICellTextEditOperation | ICellEditOperation)[];102state?: IChatTextEditGroupState;103kind: 'notebookEditGroup';104done: boolean | undefined;105}106107/**108* Progress kinds that are included in the history of a response.109* Excludes "internal" types that are included in history.110*/111export type IChatProgressHistoryResponseContent =112| IChatMarkdownContent113| IChatAgentMarkdownContentWithVulnerability114| IChatResponseCodeblockUriPart115| IChatTreeData116| IChatMultiDiffData117| IChatContentInlineReference118| IChatProgressMessage119| IChatCommandButton120| IChatWarningMessage121| IChatTask122| IChatTaskSerialized123| IChatTextEditGroup124| IChatNotebookEditGroup125| IChatConfirmation126| IChatExtensionsContent127| IChatThinkingPart128| IChatPullRequestContent;129130/**131* "Normal" progress kinds that are rendered as parts of the stream of content.132*/133export type IChatProgressResponseContent =134| IChatProgressHistoryResponseContent135| IChatToolInvocation136| IChatToolInvocationSerialized137| IChatUndoStop138| IChatPrepareToolInvocationPart139| IChatElicitationRequest140| IChatClearToPreviousToolInvocation;141142const nonHistoryKinds = new Set(['toolInvocation', 'toolInvocationSerialized', 'undoStop', 'prepareToolInvocation']);143function isChatProgressHistoryResponseContent(content: IChatProgressResponseContent): content is IChatProgressHistoryResponseContent {144return !nonHistoryKinds.has(content.kind);145}146147export function toChatHistoryContent(content: ReadonlyArray<IChatProgressResponseContent>): IChatProgressHistoryResponseContent[] {148return content.filter(isChatProgressHistoryResponseContent);149}150151export type IChatProgressRenderableResponseContent = Exclude<IChatProgressResponseContent, IChatContentInlineReference | IChatAgentMarkdownContentWithVulnerability | IChatResponseCodeblockUriPart>;152153export interface IResponse {154readonly value: ReadonlyArray<IChatProgressResponseContent>;155getMarkdown(): string;156toString(): string;157}158159export interface IChatResponseModel {160readonly onDidChange: Event<ChatResponseModelChangeReason>;161readonly id: string;162readonly requestId: string;163readonly request: IChatRequestModel | undefined;164readonly username: string;165readonly avatarIcon?: ThemeIcon | URI;166readonly session: IChatModel;167readonly agent?: IChatAgentData;168readonly usedContext: IChatUsedContext | undefined;169readonly contentReferences: ReadonlyArray<IChatContentReference>;170readonly codeCitations: ReadonlyArray<IChatCodeCitation>;171readonly progressMessages: ReadonlyArray<IChatProgressMessage>;172readonly slashCommand?: IChatAgentCommand;173readonly agentOrSlashCommandDetected: boolean;174/** View of the response shown to the user, may have parts omitted from undo stops. */175readonly response: IResponse;176/** Entire response from the model. */177readonly entireResponse: IResponse;178readonly isComplete: boolean;179readonly isCanceled: boolean;180readonly isPendingConfirmation: IObservable<boolean>;181readonly isInProgress: IObservable<boolean>;182readonly shouldBeRemovedOnSend: IChatRequestDisablement | undefined;183shouldBeBlocked: boolean;184readonly isCompleteAddedRequest: boolean;185/** 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. */186readonly isStale: boolean;187readonly vote: ChatAgentVoteDirection | undefined;188readonly voteDownReason: ChatAgentVoteDownReason | undefined;189readonly followups?: IChatFollowup[] | undefined;190readonly result?: IChatAgentResult;191readonly codeBlockInfos: ICodeBlockInfo[] | undefined;192193initializeCodeBlockInfos(codeBlockInfo: ICodeBlockInfo[]): void;194addUndoStop(undoStop: IChatUndoStop): void;195setVote(vote: ChatAgentVoteDirection): void;196setVoteDownReason(reason: ChatAgentVoteDownReason | undefined): void;197setEditApplied(edit: IChatTextEditGroup, editCount: number): boolean;198/**199* Adopts any partially-undo {@link response} as the {@link entireResponse}.200* Only valid when {@link isComplete}. This is needed because otherwise an201* undone and then diverged state would start showing old data because the202* undo stops would no longer exist in the model.203*/204finalizeUndoState(): void;205}206207export type ChatResponseModelChangeReason =208| { reason: 'other' }209| { reason: 'undoStop'; id: string };210211const defaultChatResponseModelChangeReason: ChatResponseModelChangeReason = { reason: 'other' };212213export interface IChatRequestModeInfo {214kind: ChatModeKind | undefined; // is undefined in case of modeId == 'apply'215isBuiltin: boolean;216instructions: IChatRequestModeInstructions | undefined;217modeId: 'ask' | 'agent' | 'edit' | 'custom' | 'applyCodeBlock' | undefined;218applyCodeBlockSuggestionId: EditSuggestionId | undefined;219}220221export interface IChatRequestModeInstructions {222readonly content: string | undefined;223readonly toolReferences: readonly ChatRequestToolReferenceEntry[] | undefined;224}225226export interface IChatRequestModelParameters {227session: ChatModel;228message: IParsedChatRequest;229variableData: IChatRequestVariableData;230timestamp: number;231attempt?: number;232modeInfo?: IChatRequestModeInfo;233confirmation?: string;234locationData?: IChatLocationData;235attachedContext?: IChatRequestVariableEntry[];236isCompleteAddedRequest?: boolean;237modelId?: string;238restoredId?: string;239editedFileEvents?: IChatAgentEditedFileEvent[];240}241242export class ChatRequestModel implements IChatRequestModel {243public readonly id: string;244public response: ChatResponseModel | undefined;245public shouldBeRemovedOnSend: IChatRequestDisablement | undefined;246public readonly timestamp: number;247public readonly message: IParsedChatRequest;248public readonly isCompleteAddedRequest: boolean;249public readonly modelId?: string;250public readonly modeInfo?: IChatRequestModeInfo;251252public shouldBeBlocked: boolean = false;253254private _session: ChatModel;255private readonly _attempt: number;256private _variableData: IChatRequestVariableData;257private readonly _confirmation?: string;258private readonly _locationData?: IChatLocationData;259private readonly _attachedContext?: IChatRequestVariableEntry[];260private readonly _editedFileEvents?: IChatAgentEditedFileEvent[];261262public get session(): ChatModel {263return this._session;264}265266public get username(): string {267return this.session.requesterUsername;268}269270public get avatarIconUri(): URI | undefined {271return this.session.requesterAvatarIconUri;272}273274public get attempt(): number {275return this._attempt;276}277278public get variableData(): IChatRequestVariableData {279return this._variableData;280}281282public set variableData(v: IChatRequestVariableData) {283this._variableData = v;284}285286public get confirmation(): string | undefined {287return this._confirmation;288}289290public get locationData(): IChatLocationData | undefined {291return this._locationData;292}293294public get attachedContext(): IChatRequestVariableEntry[] | undefined {295return this._attachedContext;296}297298public get editedFileEvents(): IChatAgentEditedFileEvent[] | undefined {299return this._editedFileEvents;300}301302constructor(params: IChatRequestModelParameters) {303this._session = params.session;304this.message = params.message;305this._variableData = params.variableData;306this.timestamp = params.timestamp;307this._attempt = params.attempt ?? 0;308this.modeInfo = params.modeInfo;309this._confirmation = params.confirmation;310this._locationData = params.locationData;311this._attachedContext = params.attachedContext;312this.isCompleteAddedRequest = params.isCompleteAddedRequest ?? false;313this.modelId = params.modelId;314this.id = params.restoredId ?? 'request_' + generateUuid();315this._editedFileEvents = params.editedFileEvents;316}317318adoptTo(session: ChatModel) {319this._session = session;320}321}322323class AbstractResponse implements IResponse {324protected _responseParts: IChatProgressResponseContent[];325326/**327* A stringified representation of response data which might be presented to a screenreader or used when copying a response.328*/329protected _responseRepr = '';330331/**332* Just the markdown content of the response, used for determining the rendering rate of markdown333*/334protected _markdownContent = '';335336get value(): IChatProgressResponseContent[] {337return this._responseParts;338}339340constructor(value: IChatProgressResponseContent[]) {341this._responseParts = value;342this._updateRepr();343}344345toString(): string {346return this._responseRepr;347}348349/**350* _Just_ the content of markdown parts in the response351*/352getMarkdown(): string {353return this._markdownContent;354}355356protected _updateRepr() {357this._responseRepr = this.partsToRepr(this._responseParts);358359this._markdownContent = this._responseParts.map(part => {360if (part.kind === 'inlineReference') {361return this.inlineRefToRepr(part);362} else if (part.kind === 'markdownContent' || part.kind === 'markdownVuln') {363return part.content.value;364} else {365return '';366}367})368.filter(s => s.length > 0)369.join('');370}371372private partsToRepr(parts: readonly IChatProgressResponseContent[]): string {373const blocks: string[] = [];374let currentBlockSegments: string[] = [];375let hasEditGroupsAfterLastClear = false;376377for (const part of parts) {378let segment: { text: string; isBlock?: boolean } | undefined;379switch (part.kind) {380case 'clearToPreviousToolInvocation':381currentBlockSegments = [];382blocks.length = 0;383hasEditGroupsAfterLastClear = false; // Reset edit groups flag when clearing384continue;385case 'treeData':386case 'progressMessage':387case 'codeblockUri':388case 'extensions':389case 'pullRequest':390case 'undoStop':391case 'prepareToolInvocation':392case 'elicitation':393case 'thinking':394case 'multiDiffData':395// Ignore396continue;397case 'toolInvocation':398case 'toolInvocationSerialized':399// Include tool invocations in the copy text400segment = this.getToolInvocationText(part);401break;402case 'inlineReference':403segment = { text: this.inlineRefToRepr(part) };404break;405case 'command':406segment = { text: part.command.title, isBlock: true };407break;408case 'textEditGroup':409case 'notebookEditGroup':410// Mark that we have edit groups after the last clear411hasEditGroupsAfterLastClear = true;412// Skip individual edit groups to avoid duplication413continue;414case 'confirmation':415if (part.message instanceof MarkdownString) {416segment = { text: `${part.title}\n${part.message.value}`, isBlock: true };417break;418}419segment = { text: `${part.title}\n${part.message}`, isBlock: true };420break;421default:422segment = { text: part.content.value };423break;424}425426if (segment.isBlock) {427if (currentBlockSegments.length) {428blocks.push(currentBlockSegments.join(''));429currentBlockSegments = [];430}431blocks.push(segment.text);432} else {433currentBlockSegments.push(segment.text);434}435}436437if (currentBlockSegments.length) {438blocks.push(currentBlockSegments.join(''));439}440441// Add consolidated edit summary at the end if there were any edit groups after the last clear442if (hasEditGroupsAfterLastClear) {443blocks.push(localize('editsSummary', "Made changes."));444}445446return blocks.join('\n\n');447}448449private inlineRefToRepr(part: IChatContentInlineReference) {450if ('uri' in part.inlineReference) {451return this.uriToRepr(part.inlineReference.uri);452}453454return 'name' in part.inlineReference455? '`' + part.inlineReference.name + '`'456: this.uriToRepr(part.inlineReference);457}458459private getToolInvocationText(toolInvocation: IChatToolInvocation | IChatToolInvocationSerialized): { text: string; isBlock?: boolean } {460// Extract the message and input details461let message = '';462let input = '';463464if (toolInvocation.pastTenseMessage) {465message = typeof toolInvocation.pastTenseMessage === 'string'466? toolInvocation.pastTenseMessage467: toolInvocation.pastTenseMessage.value;468} else {469message = typeof toolInvocation.invocationMessage === 'string'470? toolInvocation.invocationMessage471: toolInvocation.invocationMessage.value;472}473474// Handle different types of tool invocations475if (toolInvocation.toolSpecificData) {476if (toolInvocation.toolSpecificData.kind === 'terminal') {477message = 'Ran terminal command';478const terminalData = migrateLegacyTerminalToolSpecificData(toolInvocation.toolSpecificData);479input = terminalData.commandLine.userEdited ?? terminalData.commandLine.toolEdited ?? terminalData.commandLine.original;480}481}482483// Format the tool invocation text484let text = message;485if (input) {486text += `: ${input}`;487}488489// For completed tool invocations, also include the result details if available490if (toolInvocation.kind === 'toolInvocationSerialized' || (toolInvocation.kind === 'toolInvocation' && toolInvocation.isComplete)) {491if (toolInvocation.resultDetails && 'input' in toolInvocation.resultDetails) {492const resultPrefix = toolInvocation.kind === 'toolInvocationSerialized' || toolInvocation.isComplete ? 'Completed' : 'Errored';493text += `\n${resultPrefix} with input: ${toolInvocation.resultDetails.input}`;494}495}496497return { text, isBlock: true };498}499500private uriToRepr(uri: URI): string {501if (uri.scheme === Schemas.http || uri.scheme === Schemas.https) {502return uri.toString(false);503}504505return basename(uri);506}507}508509/** A view of a subset of a response */510class ResponseView extends AbstractResponse {511constructor(512_response: IResponse,513public readonly undoStop: string,514) {515let idx = _response.value.findIndex(v => v.kind === 'undoStop' && v.id === undoStop);516// Undo stops are inserted before `codeblockUri`'s, which are preceeded by a517// markdownContent containing the opening code fence. Adjust the index518// backwards to avoid a buggy response if it looked like this happened.519if (_response.value[idx + 1]?.kind === 'codeblockUri' && _response.value[idx - 1]?.kind === 'markdownContent') {520idx--;521}522523super(idx === -1 ? _response.value.slice() : _response.value.slice(0, idx));524}525}526527export class Response extends AbstractResponse implements IDisposable {528private _onDidChangeValue = new Emitter<void>();529public get onDidChangeValue() {530return this._onDidChangeValue.event;531}532533private _citations: IChatCodeCitation[] = [];534535536constructor(value: IMarkdownString | ReadonlyArray<IMarkdownString | IChatResponseProgressFileTreeData | IChatContentInlineReference | IChatAgentMarkdownContentWithVulnerability | IChatResponseCodeblockUriPart | IChatThinkingPart>) {537super(asArray(value).map((v) => (538'kind' in v ? v :539isMarkdownString(v) ? { content: v, kind: 'markdownContent' } satisfies IChatMarkdownContent :540{ kind: 'treeData', treeData: v }541)));542}543544dispose(): void {545this._onDidChangeValue.dispose();546}547548549clear(): void {550this._responseParts = [];551this._updateRepr(true);552}553554clearToPreviousToolInvocation(message?: string): void {555// look through the response parts and find the last tool invocation, then slice the response parts to that point556let lastToolInvocationIndex = -1;557for (let i = this._responseParts.length - 1; i >= 0; i--) {558const part = this._responseParts[i];559if (part.kind === 'toolInvocation' || part.kind === 'toolInvocationSerialized') {560lastToolInvocationIndex = i;561break;562}563}564if (lastToolInvocationIndex !== -1) {565this._responseParts = this._responseParts.slice(0, lastToolInvocationIndex + 1);566} else {567this._responseParts = [];568}569if (message) {570this._responseParts.push({ kind: 'warning', content: new MarkdownString(message) });571}572this._updateRepr(true);573}574575updateContent(progress: IChatProgressResponseContent | IChatTextEdit | IChatNotebookEdit | IChatTask, quiet?: boolean): void {576if (progress.kind === 'clearToPreviousToolInvocation') {577if (progress.reason === ChatResponseClearToPreviousToolInvocationReason.CopyrightContentRetry) {578this.clearToPreviousToolInvocation(localize('copyrightContentRetry', "Response cleared due to possible match to public code, retrying with modified prompt."));579} else if (progress.reason === ChatResponseClearToPreviousToolInvocationReason.FilteredContentRetry) {580this.clearToPreviousToolInvocation(localize('filteredContentRetry', "Response cleared due to content safety filters, retrying with modified prompt."));581} else {582this.clearToPreviousToolInvocation();583}584return;585} else if (progress.kind === 'markdownContent') {586587// last response which is NOT a text edit group because we do want to support heterogenous streaming but not have588// the MD be chopped up by text edit groups (and likely other non-renderable parts)589const lastResponsePart = this._responseParts590.filter(p => p.kind !== 'textEditGroup')591.at(-1);592593if (!lastResponsePart || lastResponsePart.kind !== 'markdownContent' || !canMergeMarkdownStrings(lastResponsePart.content, progress.content)) {594// The last part can't be merged with- not markdown, or markdown with different permissions595this._responseParts.push(progress);596} else {597// Don't modify the current object, since it's being diffed by the renderer598const idx = this._responseParts.indexOf(lastResponsePart);599this._responseParts[idx] = { ...lastResponsePart, content: appendMarkdownString(lastResponsePart.content, progress.content) };600}601this._updateRepr(quiet);602} else if (progress.kind === 'thinking') {603604// tries to split thinking chunks if it is an array. only while certain models give us array chunks.605const lastResponsePart = this._responseParts606.filter(p => p.kind !== 'textEditGroup')607.at(-1);608609const lastText = lastResponsePart && lastResponsePart.kind === 'thinking'610? (Array.isArray(lastResponsePart.value) ? lastResponsePart.value.join('') : (lastResponsePart.value || ''))611: '';612const currText = Array.isArray(progress.value) ? progress.value.join('') : (progress.value || '');613const isEmpty = (s: string) => s.trim().length === 0;614615// Do not merge if either the current or last thinking chunk is empty; empty chunks separate thinking616if (!lastResponsePart617|| lastResponsePart.kind !== 'thinking'618|| isEmpty(currText)619|| isEmpty(lastText)620|| !canMergeMarkdownStrings(new MarkdownString(lastText), new MarkdownString(currText))) {621this._responseParts.push(progress);622} else {623const idx = this._responseParts.indexOf(lastResponsePart);624this._responseParts[idx] = {625...lastResponsePart,626value: appendMarkdownString(new MarkdownString(lastText), new MarkdownString(currText)).value627};628}629this._updateRepr(quiet);630} else if (progress.kind === 'textEdit' || progress.kind === 'notebookEdit') {631// If the progress.uri is a cell Uri, its possible its part of the inline chat.632// Old approach of notebook inline chat would not start and end with notebook Uri, so we need to check for old approach.633const useOldApproachForInlineNotebook = progress.uri.scheme === Schemas.vscodeNotebookCell && !this._responseParts.find(part => part.kind === 'notebookEditGroup');634// merge edits for the same file no matter when they come in635const notebookUri = useOldApproachForInlineNotebook ? undefined : CellUri.parse(progress.uri)?.notebook;636const uri = notebookUri ?? progress.uri;637let found = false;638const groupKind = progress.kind === 'textEdit' && !notebookUri ? 'textEditGroup' : 'notebookEditGroup';639const edits: any = groupKind === 'textEditGroup' ? progress.edits : progress.edits.map(edit => TextEdit.isTextEdit(edit) ? { uri: progress.uri, edit } : edit);640for (let i = 0; !found && i < this._responseParts.length; i++) {641const candidate = this._responseParts[i];642if (candidate.kind === groupKind && !candidate.done && isEqual(candidate.uri, uri)) {643candidate.edits.push(edits);644candidate.done = progress.done;645found = true;646}647}648if (!found) {649this._responseParts.push({650kind: groupKind,651uri,652edits: groupKind === 'textEditGroup' ? [edits] : edits,653done: progress.done654});655}656this._updateRepr(quiet);657} else if (progress.kind === 'progressTask') {658// Add a new resolving part659const responsePosition = this._responseParts.push(progress) - 1;660this._updateRepr(quiet);661662const disp = progress.onDidAddProgress(() => {663this._updateRepr(false);664});665666progress.task?.().then((content) => {667// Stop listening for progress updates once the task settles668disp.dispose();669670// Replace the resolving part's content with the resolved response671if (typeof content === 'string') {672(this._responseParts[responsePosition] as IChatTask).content = new MarkdownString(content);673}674this._updateRepr(false);675});676677} else if (progress.kind === 'toolInvocation') {678if (progress.confirmationMessages) {679progress.confirmed.p.then(() => {680this._updateRepr(false);681});682}683progress.isCompletePromise.then(() => {684this._updateRepr(false);685});686this._responseParts.push(progress);687this._updateRepr(quiet);688} else {689this._responseParts.push(progress);690this._updateRepr(quiet);691}692}693694public addCitation(citation: IChatCodeCitation) {695this._citations.push(citation);696this._updateRepr();697}698699protected override _updateRepr(quiet?: boolean) {700super._updateRepr();701if (!this._onDidChangeValue) {702return; // called from parent constructor703}704705this._responseRepr += this._citations.length ? '\n\n' + getCodeCitationsMessage(this._citations) : '';706707if (!quiet) {708this._onDidChangeValue.fire();709}710}711}712713export interface IChatResponseModelParameters {714responseContent: IMarkdownString | ReadonlyArray<IMarkdownString | IChatResponseProgressFileTreeData | IChatContentInlineReference | IChatAgentMarkdownContentWithVulnerability | IChatResponseCodeblockUriPart | IChatThinkingPart>;715session: ChatModel;716agent?: IChatAgentData;717slashCommand?: IChatAgentCommand;718requestId: string;719isComplete?: boolean;720isCanceled?: boolean;721vote?: ChatAgentVoteDirection;722voteDownReason?: ChatAgentVoteDownReason;723result?: IChatAgentResult;724followups?: ReadonlyArray<IChatFollowup>;725isCompleteAddedRequest?: boolean;726shouldBeRemovedOnSend?: IChatRequestDisablement;727shouldBeBlocked?: boolean;728restoredId?: string;729/**730* undefined means it will be set later.731*/732codeBlockInfos: ICodeBlockInfo[] | undefined;733}734735export class ChatResponseModel extends Disposable implements IChatResponseModel {736private readonly _onDidChange = this._register(new Emitter<ChatResponseModelChangeReason>());737readonly onDidChange = this._onDidChange.event;738739public readonly id: string;740public readonly requestId: string;741private _session: ChatModel;742private _agent: IChatAgentData | undefined;743private _slashCommand: IChatAgentCommand | undefined;744private _isComplete: boolean;745private _isCanceled: boolean;746private _vote?: ChatAgentVoteDirection;747private _voteDownReason?: ChatAgentVoteDownReason;748private _result?: IChatAgentResult;749private _shouldBeRemovedOnSend: IChatRequestDisablement | undefined;750public readonly isCompleteAddedRequest: boolean;751private _shouldBeBlocked: boolean = false;752753public get shouldBeBlocked() {754return this._shouldBeBlocked;755}756757public get request(): IChatRequestModel | undefined {758return this.session.getRequests().find(r => r.id === this.requestId);759}760761public get session() {762return this._session;763}764765public get shouldBeRemovedOnSend() {766return this._shouldBeRemovedOnSend;767}768769public get isComplete(): boolean {770return this._isComplete;771}772773public set shouldBeRemovedOnSend(disablement: IChatRequestDisablement | undefined) {774this._shouldBeRemovedOnSend = disablement;775this._onDidChange.fire(defaultChatResponseModelChangeReason);776}777778public get isCanceled(): boolean {779return this._isCanceled;780}781782public get vote(): ChatAgentVoteDirection | undefined {783return this._vote;784}785786public get voteDownReason(): ChatAgentVoteDownReason | undefined {787return this._voteDownReason;788}789790public get followups(): IChatFollowup[] | undefined {791return this._followups;792}793794private _response: Response;795private _finalizedResponse?: IResponse;796public get entireResponse(): IResponse {797return this._finalizedResponse || this._response;798}799800public get result(): IChatAgentResult | undefined {801return this._result;802}803804public get username(): string {805return this.session.responderUsername;806}807808public get avatarIcon(): ThemeIcon | URI | undefined {809return this.session.responderAvatarIcon;810}811812private _followups?: IChatFollowup[];813814public get agent(): IChatAgentData | undefined {815return this._agent;816}817818public get slashCommand(): IChatAgentCommand | undefined {819return this._slashCommand;820}821822private _agentOrSlashCommandDetected: boolean | undefined;823public get agentOrSlashCommandDetected(): boolean {824return this._agentOrSlashCommandDetected ?? false;825}826827private _usedContext: IChatUsedContext | undefined;828public get usedContext(): IChatUsedContext | undefined {829return this._usedContext;830}831832private readonly _contentReferences: IChatContentReference[] = [];833public get contentReferences(): ReadonlyArray<IChatContentReference> {834return Array.from(this._contentReferences);835}836837private readonly _codeCitations: IChatCodeCitation[] = [];838public get codeCitations(): ReadonlyArray<IChatCodeCitation> {839return this._codeCitations;840}841842private readonly _progressMessages: IChatProgressMessage[] = [];843public get progressMessages(): ReadonlyArray<IChatProgressMessage> {844return this._progressMessages;845}846847private _isStale: boolean = false;848public get isStale(): boolean {849return this._isStale;850}851852853readonly isPendingConfirmation: IObservable<boolean>;854855readonly isInProgress: IObservable<boolean>;856857private _responseView?: ResponseView;858public get response(): IResponse {859const undoStop = this._shouldBeRemovedOnSend?.afterUndoStop;860if (!undoStop) {861return this._finalizedResponse || this._response;862}863864if (this._responseView?.undoStop !== undoStop) {865this._responseView = new ResponseView(this._response, undoStop);866}867868return this._responseView;869}870871private _codeBlockInfos: ICodeBlockInfo[] | undefined;872public get codeBlockInfos(): ICodeBlockInfo[] | undefined {873return this._codeBlockInfos;874}875876constructor(params: IChatResponseModelParameters) {877super();878879this._session = params.session;880this._agent = params.agent;881this._slashCommand = params.slashCommand;882this.requestId = params.requestId;883this._isComplete = params.isComplete ?? false;884this._isCanceled = params.isCanceled ?? false;885this._vote = params.vote;886this._voteDownReason = params.voteDownReason;887this._result = params.result;888this._followups = params.followups ? [...params.followups] : undefined;889this.isCompleteAddedRequest = params.isCompleteAddedRequest ?? false;890this._shouldBeRemovedOnSend = params.shouldBeRemovedOnSend;891this._shouldBeBlocked = params.shouldBeBlocked ?? false;892893// If we are creating a response with some existing content, consider it stale894this._isStale = Array.isArray(params.responseContent) && (params.responseContent.length !== 0 || isMarkdownString(params.responseContent) && params.responseContent.value.length !== 0);895896this._response = this._register(new Response(params.responseContent));897this._codeBlockInfos = params.codeBlockInfos ? [...params.codeBlockInfos] : undefined;898899const signal = observableSignalFromEvent(this, this.onDidChange);900901this.isPendingConfirmation = signal.map((_value, r) => {902903signal.read(r);904905return this._response.value.some(part =>906part.kind === 'toolInvocation' && part.isConfirmed === undefined907|| part.kind === 'confirmation' && part.isUsed === false908);909});910911this.isInProgress = signal.map((_value, r) => {912913signal.read(r);914915return !this.isPendingConfirmation.read(r)916&& !this.shouldBeRemovedOnSend917&& !this._isComplete;918});919920this._register(this._response.onDidChangeValue(() => this._onDidChange.fire(defaultChatResponseModelChangeReason)));921this.id = params.restoredId ?? 'response_' + generateUuid();922923this._register(this._session.onDidChange((e) => {924if (e.kind === 'setCheckpoint') {925const isDisabled = e.disabledResponseIds.has(this.id);926const didChange = this._shouldBeBlocked === isDisabled;927this._shouldBeBlocked = isDisabled;928if (didChange) {929this._onDidChange.fire(defaultChatResponseModelChangeReason);930}931}932}));933}934935initializeCodeBlockInfos(codeBlockInfo: ICodeBlockInfo[]): void {936if (this._codeBlockInfos) {937throw new BugIndicatingError('Code block infos have already been initialized');938}939this._codeBlockInfos = [...codeBlockInfo];940}941942/**943* Apply a progress update to the actual response content.944*/945updateContent(responsePart: IChatProgressResponseContent | IChatTextEdit | IChatNotebookEdit, quiet?: boolean) {946this._response.updateContent(responsePart, quiet);947}948949/**950* Adds an undo stop at the current position in the stream.951*/952addUndoStop(undoStop: IChatUndoStop) {953this._onDidChange.fire({ reason: 'undoStop', id: undoStop.id });954this._response.updateContent(undoStop, true);955}956957/**958* Apply one of the progress updates that are not part of the actual response content.959*/960applyReference(progress: IChatUsedContext | IChatContentReference) {961if (progress.kind === 'usedContext') {962this._usedContext = progress;963} else if (progress.kind === 'reference') {964this._contentReferences.push(progress);965this._onDidChange.fire(defaultChatResponseModelChangeReason);966}967}968969applyCodeCitation(progress: IChatCodeCitation) {970this._codeCitations.push(progress);971this._response.addCitation(progress);972this._onDidChange.fire(defaultChatResponseModelChangeReason);973}974975setAgent(agent: IChatAgentData, slashCommand?: IChatAgentCommand) {976this._agent = agent;977this._slashCommand = slashCommand;978this._agentOrSlashCommandDetected = !agent.isDefault || !!slashCommand;979this._onDidChange.fire(defaultChatResponseModelChangeReason);980}981982setResult(result: IChatAgentResult): void {983this._result = result;984this._onDidChange.fire(defaultChatResponseModelChangeReason);985}986987complete(): void {988if (this._result?.errorDetails?.responseIsRedacted) {989this._response.clear();990}991992this._isComplete = true;993this._onDidChange.fire(defaultChatResponseModelChangeReason);994}995996cancel(): void {997this._isComplete = true;998this._isCanceled = true;999this._onDidChange.fire(defaultChatResponseModelChangeReason);1000}10011002setFollowups(followups: IChatFollowup[] | undefined): void {1003this._followups = followups;1004this._onDidChange.fire(defaultChatResponseModelChangeReason); // Fire so that command followups get rendered on the row1005}10061007setVote(vote: ChatAgentVoteDirection): void {1008this._vote = vote;1009this._onDidChange.fire(defaultChatResponseModelChangeReason);1010}10111012setVoteDownReason(reason: ChatAgentVoteDownReason | undefined): void {1013this._voteDownReason = reason;1014this._onDidChange.fire(defaultChatResponseModelChangeReason);1015}10161017setEditApplied(edit: IChatTextEditGroup, editCount: number): boolean {1018if (!this.response.value.includes(edit)) {1019return false;1020}1021if (!edit.state) {1022return false;1023}1024edit.state.applied = editCount; // must not be edit.edits.length1025this._onDidChange.fire(defaultChatResponseModelChangeReason);1026return true;1027}10281029adoptTo(session: ChatModel) {1030this._session = session;1031this._onDidChange.fire(defaultChatResponseModelChangeReason);1032}103310341035finalizeUndoState(): void {1036this._finalizedResponse = this.response;1037this._responseView = undefined;1038this._shouldBeRemovedOnSend = undefined;1039}10401041}104210431044export interface IChatRequestDisablement {1045requestId: string;1046afterUndoStop?: string;1047}10481049export interface IChatModel extends IDisposable {1050readonly onDidDispose: Event<void>;1051readonly onDidChange: Event<IChatChangeEvent>;1052readonly sessionId: string;1053readonly initialLocation: ChatAgentLocation;1054readonly title: string;1055readonly requestInProgress: boolean;1056readonly requestInProgressObs: IObservable<boolean>;1057readonly inputPlaceholder?: string;1058readonly editingSessionObs?: ObservablePromise<IChatEditingSession> | undefined;1059readonly editingSession?: IChatEditingSession | undefined;1060/**1061* Sets requests as 'disabled', removing them from the UI. If a request ID1062* is given without undo stops, it's removed entirely. If an undo stop1063* is given, all content after that stop is removed.1064*/1065setDisabledRequests(requestIds: IChatRequestDisablement[]): void;1066getRequests(): IChatRequestModel[];1067setCheckpoint(requestId: string | undefined): void;1068readonly checkpoint: IChatRequestModel | undefined;1069addRequest(message: IParsedChatRequest, variableData: IChatRequestVariableData, attempt: number, modeInfo?: IChatRequestModeInfo, chatAgent?: IChatAgentData, slashCommand?: IChatAgentCommand, confirmation?: string, locationData?: IChatLocationData, attachments?: IChatRequestVariableEntry[], isCompleteAddedRequest?: boolean, modelId?: string): IChatRequestModel;1070acceptResponseProgress(request: IChatRequestModel, progress: IChatProgress, quiet?: boolean): void;1071setResponse(request: IChatRequestModel, result: IChatAgentResult): void;1072completeResponse(request: IChatRequestModel): void;1073setCustomTitle(title: string): void;1074toExport(): IExportableChatData;1075toJSON(): ISerializableChatData;1076}10771078export interface ISerializableChatsData {1079[sessionId: string]: ISerializableChatData;1080}10811082export type ISerializableChatAgentData = UriDto<IChatAgentData>;10831084export interface ISerializableChatRequestData {1085requestId: string;1086message: string | IParsedChatRequest; // string => old format1087/** Is really like "prompt data". This is the message in the format in which the agent gets it + variable values. */1088variableData: IChatRequestVariableData;1089response: ReadonlyArray<IMarkdownString | IChatResponseProgressFileTreeData | IChatContentInlineReference | IChatAgentMarkdownContentWithVulnerability | IChatThinkingPart> | undefined;10901091/**Old, persisted name for shouldBeRemovedOnSend */1092isHidden?: boolean;1093shouldBeRemovedOnSend?: IChatRequestDisablement;1094responseId?: string;1095agent?: ISerializableChatAgentData;1096workingSet?: UriComponents[];1097slashCommand?: IChatAgentCommand;1098// responseErrorDetails: IChatResponseErrorDetails | undefined;1099result?: IChatAgentResult; // Optional for backcompat1100followups: ReadonlyArray<IChatFollowup> | undefined;1101isCanceled: boolean | undefined;1102vote: ChatAgentVoteDirection | undefined;1103voteDownReason?: ChatAgentVoteDownReason;1104/** For backward compat: should be optional */1105usedContext?: IChatUsedContext;1106contentReferences?: ReadonlyArray<IChatContentReference>;1107codeCitations?: ReadonlyArray<IChatCodeCitation>;1108timestamp?: number;1109confirmation?: string;1110editedFileEvents?: IChatAgentEditedFileEvent[];1111modelId?: string;11121113responseMarkdownInfo: ISerializableMarkdownInfo[] | undefined;1114}11151116export interface ISerializableMarkdownInfo {1117readonly suggestionId: EditSuggestionId;1118}11191120export interface IExportableChatData {1121initialLocation: ChatAgentLocation | undefined;1122requests: ISerializableChatRequestData[];1123requesterUsername: string;1124responderUsername: string;1125requesterAvatarIconUri: UriComponents | undefined;1126responderAvatarIconUri: ThemeIcon | UriComponents | undefined; // Keeping Uri name for backcompat1127}11281129/*1130NOTE: 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.1131*/11321133export interface ISerializableChatData1 extends IExportableChatData {1134sessionId: string;1135creationDate: number;1136isImported: boolean;11371138/** 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. */1139isNew?: boolean;1140}11411142export interface ISerializableChatData2 extends ISerializableChatData1 {1143version: 2;1144lastMessageDate: number;1145computedTitle: string | undefined;1146}11471148export interface ISerializableChatData3 extends Omit<ISerializableChatData2, 'version' | 'computedTitle'> {1149version: 3;1150customTitle: string | undefined;1151}11521153/**1154* Chat data that has been parsed and normalized to the current format.1155*/1156export type ISerializableChatData = ISerializableChatData3;11571158/**1159* Chat data that has been loaded but not normalized, and could be any format1160*/1161export type ISerializableChatDataIn = ISerializableChatData1 | ISerializableChatData2 | ISerializableChatData3;11621163/**1164* Normalize chat data from storage to the current format.1165* TODO- ChatModel#_deserialize and reviveSerializedAgent also still do some normalization and maybe that should be done in here too.1166*/1167export function normalizeSerializableChatData(raw: ISerializableChatDataIn): ISerializableChatData {1168normalizeOldFields(raw);11691170if (!('version' in raw)) {1171return {1172version: 3,1173...raw,1174lastMessageDate: raw.creationDate,1175customTitle: undefined,1176};1177}11781179if (raw.version === 2) {1180return {1181...raw,1182version: 3,1183customTitle: raw.computedTitle1184};1185}11861187return raw;1188}11891190function normalizeOldFields(raw: ISerializableChatDataIn): void {1191// Fill in fields that very old chat data may be missing1192if (!raw.sessionId) {1193raw.sessionId = generateUuid();1194}11951196if (!raw.creationDate) {1197raw.creationDate = getLastYearDate();1198}11991200if ('version' in raw && (raw.version === 2 || raw.version === 3)) {1201if (!raw.lastMessageDate) {1202// A bug led to not porting creationDate properly, and that was copied to lastMessageDate, so fix that up if missing.1203raw.lastMessageDate = getLastYearDate();1204}1205}12061207if ((raw.initialLocation as any) === 'editing-session') {1208raw.initialLocation = ChatAgentLocation.Panel;1209}1210}12111212function getLastYearDate(): number {1213const lastYearDate = new Date();1214lastYearDate.setFullYear(lastYearDate.getFullYear() - 1);1215return lastYearDate.getTime();1216}12171218export function isExportableSessionData(obj: unknown): obj is IExportableChatData {1219const data = obj as IExportableChatData;1220return typeof data === 'object' &&1221typeof data.requesterUsername === 'string';1222}12231224export function isSerializableSessionData(obj: unknown): obj is ISerializableChatData {1225const data = obj as ISerializableChatData;1226return isExportableSessionData(obj) &&1227typeof data.creationDate === 'number' &&1228typeof data.sessionId === 'string' &&1229obj.requests.every((request: ISerializableChatRequestData) =>1230!request.usedContext /* for backward compat allow missing usedContext */ || isIUsedContext(request.usedContext)1231);1232}12331234export type IChatChangeEvent =1235| IChatInitEvent1236| IChatAddRequestEvent | IChatChangedRequestEvent | IChatRemoveRequestEvent1237| IChatAddResponseEvent1238| IChatSetAgentEvent1239| IChatMoveEvent1240| IChatSetHiddenEvent1241| IChatCompletedRequestEvent1242| IChatSetCheckpointEvent1243| IChatSetCustomTitleEvent1244;12451246export interface IChatAddRequestEvent {1247kind: 'addRequest';1248request: IChatRequestModel;1249}12501251export interface IChatSetCheckpointEvent {1252kind: 'setCheckpoint';1253disabledRequestIds: Set<string>;1254disabledResponseIds: Set<string>;1255}12561257export interface IChatChangedRequestEvent {1258kind: 'changedRequest';1259request: IChatRequestModel;1260}12611262export interface IChatCompletedRequestEvent {1263kind: 'completedRequest';1264request: IChatRequestModel;1265}12661267export interface IChatAddResponseEvent {1268kind: 'addResponse';1269response: IChatResponseModel;1270}12711272export const enum ChatRequestRemovalReason {1273/**1274* "Normal" remove1275*/1276Removal,12771278/**1279* Removed because the request will be resent1280*/1281Resend,12821283/**1284* Remove because the request is moving to another model1285*/1286Adoption1287}12881289export interface IChatRemoveRequestEvent {1290kind: 'removeRequest';1291requestId: string;1292responseId?: string;1293reason: ChatRequestRemovalReason;1294}12951296export interface IChatSetHiddenEvent {1297kind: 'setHidden';1298hiddenRequestIds: readonly IChatRequestDisablement[];1299}13001301export interface IChatMoveEvent {1302kind: 'move';1303target: URI;1304range: IRange;1305}13061307export interface IChatSetAgentEvent {1308kind: 'setAgent';1309agent: IChatAgentData;1310command?: IChatAgentCommand;1311}13121313export interface IChatSetCustomTitleEvent {1314kind: 'setCustomTitle';1315title: string;1316}13171318export interface IChatInitEvent {1319kind: 'initialize';1320}13211322export class ChatModel extends Disposable implements IChatModel {1323static getDefaultTitle(requests: (ISerializableChatRequestData | IChatRequestModel)[]): string {1324const firstRequestMessage = requests.at(0)?.message ?? '';1325const message = typeof firstRequestMessage === 'string' ?1326firstRequestMessage :1327firstRequestMessage.text;1328return message.split('\n')[0].substring(0, 200);1329}13301331private readonly _onDidDispose = this._register(new Emitter<void>());1332readonly onDidDispose = this._onDidDispose.event;13331334private readonly _onDidChange = this._register(new Emitter<IChatChangeEvent>());1335readonly onDidChange = this._onDidChange.event;13361337private _requests: ChatRequestModel[];13381339// TODO to be clear, this is not the same as the id from the session object, which belongs to the provider.1340// It's easier to be able to identify this model before its async initialization is complete1341private _sessionId: string;1342get sessionId(): string {1343return this._sessionId;1344}13451346get requestInProgress(): boolean {1347return this.requestInProgressObs.get();1348}13491350readonly requestInProgressObs: IObservable<boolean>;135113521353get hasRequests(): boolean {1354return this._requests.length > 0;1355}13561357get lastRequest(): ChatRequestModel | undefined {1358return this._requests.at(-1);1359}13601361private _creationDate: number;1362get creationDate(): number {1363return this._creationDate;1364}13651366private _lastMessageDate: number;1367get lastMessageDate(): number {1368return this._lastMessageDate;1369}13701371private get _defaultAgent() {1372return this.chatAgentService.getDefaultAgent(ChatAgentLocation.Panel, ChatModeKind.Ask);1373}13741375get requesterUsername(): string {1376return this._defaultAgent?.metadata.requester?.name ??1377this.initialData?.requesterUsername ?? '';1378}13791380get responderUsername(): string {1381return this._defaultAgent?.fullName ??1382this.initialData?.responderUsername ?? '';1383}13841385private readonly _initialRequesterAvatarIconUri: URI | undefined;1386get requesterAvatarIconUri(): URI | undefined {1387return this._defaultAgent?.metadata.requester?.icon ??1388this._initialRequesterAvatarIconUri;1389}13901391private readonly _initialResponderAvatarIconUri: ThemeIcon | URI | undefined;1392get responderAvatarIcon(): ThemeIcon | URI | undefined {1393return this._defaultAgent?.metadata.themeIcon ??1394this._initialResponderAvatarIconUri;1395}13961397private _isImported = false;1398get isImported(): boolean {1399return this._isImported;1400}14011402private _customTitle: string | undefined;1403get customTitle(): string | undefined {1404return this._customTitle;1405}14061407get title(): string {1408return this._customTitle || ChatModel.getDefaultTitle(this._requests);1409}14101411get initialLocation() {1412return this._initialLocation;1413}14141415private _editingSession: ObservablePromise<IChatEditingSession> | undefined;1416get editingSessionObs(): ObservablePromise<IChatEditingSession> | undefined {1417return this._editingSession;1418}14191420get editingSession(): IChatEditingSession | undefined {1421return this._editingSession?.promiseResult.get()?.data;1422}14231424constructor(1425private readonly initialData: ISerializableChatData | IExportableChatData | undefined,1426private readonly _initialLocation: ChatAgentLocation,1427@ILogService private readonly logService: ILogService,1428@IChatAgentService private readonly chatAgentService: IChatAgentService,1429@IChatEditingService private readonly chatEditingService: IChatEditingService,1430) {1431super();14321433const isValid = isSerializableSessionData(initialData);1434if (initialData && !isValid) {1435this.logService.warn(`ChatModel#constructor: Loaded malformed session data: ${JSON.stringify(initialData)}`);1436}14371438this._isImported = (!!initialData && !isValid) || (initialData?.isImported ?? false);1439this._sessionId = (isValid && initialData.sessionId) || generateUuid();1440this._requests = initialData ? this._deserialize(initialData) : [];1441this._creationDate = (isValid && initialData.creationDate) || Date.now();1442this._lastMessageDate = (isValid && initialData.lastMessageDate) || this._creationDate;1443this._customTitle = isValid ? initialData.customTitle : undefined;14441445this._initialRequesterAvatarIconUri = initialData?.requesterAvatarIconUri && URI.revive(initialData.requesterAvatarIconUri);1446this._initialResponderAvatarIconUri = isUriComponents(initialData?.responderAvatarIconUri) ? URI.revive(initialData.responderAvatarIconUri) : initialData?.responderAvatarIconUri;144714481449const lastResponse = observableFromEvent(this, this.onDidChange, () => this._requests.at(-1)?.response);14501451this.requestInProgressObs = lastResponse.map((response, r) => {1452return response?.isInProgress.read(r) ?? false;1453});1454}14551456startEditingSession(isGlobalEditingSession?: boolean): void {1457const editingSessionPromise = isGlobalEditingSession ?1458this.chatEditingService.startOrContinueGlobalEditingSession(this) :1459this.chatEditingService.createEditingSession(this);1460this._editingSession = new ObservablePromise(editingSessionPromise);1461this._editingSession.promise.then(editingSession => {1462this._store.isDisposed ? editingSession.dispose() : this._register(editingSession);1463});1464}14651466private currentEditedFileEvents = new ResourceMap<IChatAgentEditedFileEvent>();1467notifyEditingAction(action: IChatEditingSessionAction): void {1468const state = action.outcome === 'accepted' ? ChatRequestEditedFileEventKind.Keep :1469action.outcome === 'rejected' ? ChatRequestEditedFileEventKind.Undo :1470action.outcome === 'userModified' ? ChatRequestEditedFileEventKind.UserModification : null;1471if (state === null) {1472return;1473}14741475if (!this.currentEditedFileEvents.has(action.uri) || this.currentEditedFileEvents.get(action.uri)?.eventKind === ChatRequestEditedFileEventKind.Keep) {1476this.currentEditedFileEvents.set(action.uri, { eventKind: state, uri: action.uri });1477}1478}14791480private _deserialize(obj: IExportableChatData): ChatRequestModel[] {1481const requests = obj.requests;1482if (!Array.isArray(requests)) {1483this.logService.error(`Ignoring malformed session data: ${JSON.stringify(obj)}`);1484return [];1485}14861487try {1488return requests.map((raw: ISerializableChatRequestData) => {1489const parsedRequest =1490typeof raw.message === 'string'1491? this.getParsedRequestFromString(raw.message)1492: reviveParsedChatRequest(raw.message);14931494// Old messages don't have variableData, or have it in the wrong (non-array) shape1495const variableData: IChatRequestVariableData = this.reviveVariableData(raw.variableData);1496const request = new ChatRequestModel({1497session: this,1498message: parsedRequest,1499variableData,1500timestamp: raw.timestamp ?? -1,1501restoredId: raw.requestId,1502confirmation: raw.confirmation,1503editedFileEvents: raw.editedFileEvents,1504modelId: raw.modelId,1505});1506request.shouldBeRemovedOnSend = raw.isHidden ? { requestId: raw.requestId } : raw.shouldBeRemovedOnSend;1507if (raw.response || raw.result || (raw as any).responseErrorDetails) {1508const agent = (raw.agent && 'metadata' in raw.agent) ? // Check for the new format, ignore entries in the old format1509reviveSerializedAgent(raw.agent) : undefined;15101511// Port entries from old format1512const result = 'responseErrorDetails' in raw ?1513// eslint-disable-next-line local/code-no-dangerous-type-assertions1514{ errorDetails: raw.responseErrorDetails } as IChatAgentResult : raw.result;1515request.response = new ChatResponseModel({1516responseContent: raw.response ?? [new MarkdownString(raw.response)],1517session: this,1518agent,1519slashCommand: raw.slashCommand,1520requestId: request.id,1521isComplete: true,1522isCanceled: raw.isCanceled,1523vote: raw.vote,1524voteDownReason: raw.voteDownReason,1525result,1526followups: raw.followups,1527restoredId: raw.responseId,1528shouldBeBlocked: request.shouldBeBlocked,1529codeBlockInfos: raw.responseMarkdownInfo?.map<ICodeBlockInfo>(info => ({ suggestionId: info.suggestionId })),1530});1531request.response.shouldBeRemovedOnSend = raw.isHidden ? { requestId: raw.requestId } : raw.shouldBeRemovedOnSend;1532if (raw.usedContext) { // @ulugbekna: if this's a new vscode sessions, doc versions are incorrect anyway?1533request.response.applyReference(revive(raw.usedContext));1534}15351536raw.contentReferences?.forEach(r => request.response!.applyReference(revive(r)));1537raw.codeCitations?.forEach(c => request.response!.applyCodeCitation(revive(c)));1538}1539return request;1540});1541} catch (error) {1542this.logService.error('Failed to parse chat data', error);1543return [];1544}1545}15461547private reviveVariableData(raw: IChatRequestVariableData): IChatRequestVariableData {1548const variableData = raw && Array.isArray(raw.variables)1549? raw :1550{ variables: [] };15511552variableData.variables = variableData.variables.map<IChatRequestVariableEntry>((v): IChatRequestVariableEntry => {1553// Old variables format1554if (v && 'values' in v && Array.isArray(v.values)) {1555return {1556kind: 'generic',1557id: v.id ?? '',1558name: v.name,1559value: v.values[0]?.value,1560range: v.range,1561modelDescription: v.modelDescription,1562references: v.references1563};1564} else {1565return v;1566}1567});15681569return variableData;1570}15711572private getParsedRequestFromString(message: string): IParsedChatRequest {1573// TODO These offsets won't be used, but chat replies need to go through the parser as well1574const parts = [new ChatRequestTextPart(new OffsetRange(0, message.length), { startColumn: 1, startLineNumber: 1, endColumn: 1, endLineNumber: 1 }, message)];1575return {1576text: message,1577parts1578};1579}1580158115821583getRequests(): ChatRequestModel[] {1584return this._requests;1585}15861587resetCheckpoint(): void {1588for (const request of this._requests) {1589request.shouldBeBlocked = false;1590}1591}15921593setCheckpoint(requestId: string | undefined) {1594let checkpoint: ChatRequestModel | undefined;1595let checkpointIndex = -1;1596if (requestId !== undefined) {1597this._requests.forEach((request, index) => {1598if (request.id === requestId) {1599checkpointIndex = index;1600checkpoint = request;1601request.shouldBeBlocked = true;1602}1603});16041605if (!checkpoint) {1606return; // Invalid request ID1607}1608}16091610const disabledRequestIds = new Set<string>();1611const disabledResponseIds = new Set<string>();1612for (let i = this._requests.length - 1; i >= 0; i -= 1) {1613const request = this._requests[i];1614if (this._checkpoint && !checkpoint) {1615request.shouldBeBlocked = false;1616} else if (checkpoint && i >= checkpointIndex) {1617request.shouldBeBlocked = true;1618disabledRequestIds.add(request.id);1619if (request.response) {1620disabledResponseIds.add(request.response.id);1621}1622} else if (checkpoint && i < checkpointIndex) {1623request.shouldBeBlocked = false;1624}1625}16261627this._checkpoint = checkpoint;1628this._onDidChange.fire({1629kind: 'setCheckpoint',1630disabledRequestIds,1631disabledResponseIds1632});1633}16341635private _checkpoint: ChatRequestModel | undefined = undefined;1636public get checkpoint() {1637return this._checkpoint;1638}16391640setDisabledRequests(requestIds: IChatRequestDisablement[]) {1641this._requests.forEach((request) => {1642const shouldBeRemovedOnSend = requestIds.find(r => r.requestId === request.id);1643request.shouldBeRemovedOnSend = shouldBeRemovedOnSend;1644if (request.response) {1645request.response.shouldBeRemovedOnSend = shouldBeRemovedOnSend;1646}1647});16481649this._onDidChange.fire({1650kind: 'setHidden',1651hiddenRequestIds: requestIds,1652});1653}16541655addRequest(message: IParsedChatRequest, variableData: IChatRequestVariableData, attempt: number, modeInfo?: IChatRequestModeInfo, chatAgent?: IChatAgentData, slashCommand?: IChatAgentCommand, confirmation?: string, locationData?: IChatLocationData, attachments?: IChatRequestVariableEntry[], isCompleteAddedRequest?: boolean, modelId?: string): ChatRequestModel {1656const editedFileEvents = [...this.currentEditedFileEvents.values()];1657this.currentEditedFileEvents.clear();1658const request = new ChatRequestModel({1659session: this,1660message,1661variableData,1662timestamp: Date.now(),1663attempt,1664modeInfo,1665confirmation,1666locationData,1667attachedContext: attachments,1668isCompleteAddedRequest,1669modelId,1670editedFileEvents: editedFileEvents.length ? editedFileEvents : undefined,1671});1672request.response = new ChatResponseModel({1673responseContent: [],1674session: this,1675agent: chatAgent,1676slashCommand,1677requestId: request.id,1678isCompleteAddedRequest,1679codeBlockInfos: undefined,1680});16811682this._requests.push(request);1683this._lastMessageDate = Date.now();1684this._onDidChange.fire({ kind: 'addRequest', request });1685return request;1686}16871688public setCustomTitle(title: string): void {1689this._customTitle = title;1690this._onDidChange.fire({ kind: 'setCustomTitle', title });1691}16921693updateRequest(request: ChatRequestModel, variableData: IChatRequestVariableData) {1694request.variableData = variableData;1695this._onDidChange.fire({ kind: 'changedRequest', request });1696}16971698adoptRequest(request: ChatRequestModel): void {1699// this doesn't use `removeRequest` because it must not dispose the request object1700const oldOwner = request.session;1701const index = oldOwner._requests.findIndex((candidate: ChatRequestModel) => candidate.id === request.id);17021703if (index === -1) {1704return;1705}17061707oldOwner._requests.splice(index, 1);17081709request.adoptTo(this);1710request.response?.adoptTo(this);1711this._requests.push(request);17121713oldOwner._onDidChange.fire({ kind: 'removeRequest', requestId: request.id, responseId: request.response?.id, reason: ChatRequestRemovalReason.Adoption });1714this._onDidChange.fire({ kind: 'addRequest', request });1715}17161717acceptResponseProgress(request: ChatRequestModel, progress: IChatProgress, quiet?: boolean): void {1718if (!request.response) {1719request.response = new ChatResponseModel({1720responseContent: [],1721session: this,1722requestId: request.id,1723codeBlockInfos: undefined,1724});1725}17261727if (request.response.isComplete) {1728throw new Error('acceptResponseProgress: Adding progress to a completed response');1729}173017311732if (progress.kind === 'usedContext' || progress.kind === 'reference') {1733request.response.applyReference(progress);1734} else if (progress.kind === 'codeCitation') {1735request.response.applyCodeCitation(progress);1736} else if (progress.kind === 'move') {1737this._onDidChange.fire({ kind: 'move', target: progress.uri, range: progress.range });1738} else if (progress.kind === 'codeblockUri' && progress.isEdit) {1739request.response.addUndoStop({ id: generateUuid(), kind: 'undoStop' });1740request.response.updateContent(progress, quiet);1741} else if (progress.kind === 'progressTaskResult') {1742// Should have been handled upstream, not sent to model1743this.logService.error(`Couldn't handle progress: ${JSON.stringify(progress)}`);1744} else {1745request.response.updateContent(progress, quiet);1746}1747}17481749removeRequest(id: string, reason: ChatRequestRemovalReason = ChatRequestRemovalReason.Removal): void {1750const index = this._requests.findIndex(request => request.id === id);1751const request = this._requests[index];17521753if (index !== -1) {1754this._onDidChange.fire({ kind: 'removeRequest', requestId: request.id, responseId: request.response?.id, reason });1755this._requests.splice(index, 1);1756request.response?.dispose();1757}1758}17591760cancelRequest(request: ChatRequestModel): void {1761if (request.response) {1762request.response.cancel();1763}1764}17651766setResponse(request: ChatRequestModel, result: IChatAgentResult): void {1767if (!request.response) {1768request.response = new ChatResponseModel({1769responseContent: [],1770session: this,1771requestId: request.id,1772codeBlockInfos: undefined,1773});1774}17751776request.response.setResult(result);1777}17781779completeResponse(request: ChatRequestModel): void {1780if (!request.response) {1781throw new Error('Call setResponse before completeResponse');1782}17831784request.response.complete();1785this._onDidChange.fire({ kind: 'completedRequest', request });1786}17871788setFollowups(request: ChatRequestModel, followups: IChatFollowup[] | undefined): void {1789if (!request.response) {1790// Maybe something went wrong?1791return;1792}17931794request.response.setFollowups(followups);1795}17961797setResponseModel(request: ChatRequestModel, response: ChatResponseModel): void {1798request.response = response;1799this._onDidChange.fire({ kind: 'addResponse', response });1800}18011802toExport(): IExportableChatData {1803return {1804requesterUsername: this.requesterUsername,1805requesterAvatarIconUri: this.requesterAvatarIconUri,1806responderUsername: this.responderUsername,1807responderAvatarIconUri: this.responderAvatarIcon,1808initialLocation: this.initialLocation,1809requests: this._requests.map((r): ISerializableChatRequestData => {1810const message = {1811...r.message,1812parts: r.message.parts.map((p: any) => p && 'toJSON' in p ? (p.toJSON as Function)() : p)1813};1814const agent = r.response?.agent;1815const agentJson = agent && 'toJSON' in agent ? (agent.toJSON as Function)() :1816agent ? { ...agent } : undefined;1817return {1818requestId: r.id,1819message,1820variableData: r.variableData,1821response: r.response ?1822r.response.entireResponse.value.map(item => {1823// Keeping the shape of the persisted data the same for back compat1824if (item.kind === 'treeData') {1825return item.treeData;1826} else if (item.kind === 'markdownContent') {1827return item.content;1828} else if (item.kind === 'thinking') {1829return {1830kind: 'thinking',1831value: item.value,1832id: item.id,1833metadata: item.metadata1834};1835} else {1836return item as any; // TODO1837}1838})1839: undefined,1840responseId: r.response?.id,1841shouldBeRemovedOnSend: r.shouldBeRemovedOnSend,1842result: r.response?.result,1843responseMarkdownInfo: r.response?.codeBlockInfos?.map<ISerializableMarkdownInfo>(info => ({ suggestionId: info.suggestionId })),1844followups: r.response?.followups,1845isCanceled: r.response?.isCanceled,1846vote: r.response?.vote,1847voteDownReason: r.response?.voteDownReason,1848agent: agentJson,1849slashCommand: r.response?.slashCommand,1850usedContext: r.response?.usedContext,1851contentReferences: r.response?.contentReferences,1852codeCitations: r.response?.codeCitations,1853timestamp: r.timestamp,1854confirmation: r.confirmation,1855editedFileEvents: r.editedFileEvents,1856modelId: r.modelId,1857};1858}),1859};1860}18611862toJSON(): ISerializableChatData {1863return {1864version: 3,1865...this.toExport(),1866sessionId: this.sessionId,1867creationDate: this._creationDate,1868isImported: this._isImported,1869lastMessageDate: this._lastMessageDate,1870customTitle: this._customTitle1871};1872}18731874override dispose() {1875this._requests.forEach(r => r.response?.dispose());1876this._onDidDispose.fire();18771878super.dispose();1879}1880}18811882export function updateRanges(variableData: IChatRequestVariableData, diff: number): IChatRequestVariableData {1883return {1884variables: variableData.variables.map(v => ({1885...v,1886range: v.range && {1887start: v.range.start - diff,1888endExclusive: v.range.endExclusive - diff1889}1890}))1891};1892}18931894export function canMergeMarkdownStrings(md1: IMarkdownString, md2: IMarkdownString): boolean {1895if (md1.baseUri && md2.baseUri) {1896const baseUriEquals = md1.baseUri.scheme === md2.baseUri.scheme1897&& md1.baseUri.authority === md2.baseUri.authority1898&& md1.baseUri.path === md2.baseUri.path1899&& md1.baseUri.query === md2.baseUri.query1900&& md1.baseUri.fragment === md2.baseUri.fragment;1901if (!baseUriEquals) {1902return false;1903}1904} else if (md1.baseUri || md2.baseUri) {1905return false;1906}19071908return equals(md1.isTrusted, md2.isTrusted) &&1909md1.supportHtml === md2.supportHtml &&1910md1.supportThemeIcons === md2.supportThemeIcons;1911}19121913export function appendMarkdownString(md1: IMarkdownString, md2: IMarkdownString | string): IMarkdownString {1914const appendedValue = typeof md2 === 'string' ? md2 : md2.value;1915return {1916value: md1.value + appendedValue,1917isTrusted: md1.isTrusted,1918supportThemeIcons: md1.supportThemeIcons,1919supportHtml: md1.supportHtml,1920baseUri: md1.baseUri1921};1922}19231924export function getCodeCitationsMessage(citations: ReadonlyArray<IChatCodeCitation>): string {1925if (citations.length === 0) {1926return '';1927}19281929const licenseTypes = citations.reduce((set, c) => set.add(c.license), new Set<string>());1930const label = licenseTypes.size === 1 ?1931localize('codeCitation', "Similar code found with 1 license type", licenseTypes.size) :1932localize('codeCitations', "Similar code found with {0} license types", licenseTypes.size);1933return label;1934}19351936export enum ChatRequestEditedFileEventKind {1937Keep = 1,1938Undo = 2,1939UserModification = 3,1940}19411942export interface IChatAgentEditedFileEvent {1943readonly uri: URI;1944readonly eventKind: ChatRequestEditedFileEventKind;1945}19461947/** URI for a resource embedded in a chat request/response */1948export namespace ChatResponseResource {1949export const scheme = 'vscode-chat-response-resource';19501951export function createUri(sessionId: string, requestId: string, toolCallId: string, index: number, basename?: string): URI {1952return URI.from({1953scheme: ChatResponseResource.scheme,1954authority: sessionId,1955path: `/tool/${requestId}/${toolCallId}/${index}` + (basename ? `/${basename}` : ''),1956});1957}19581959export function parseUri(uri: URI): undefined | { sessionId: string; requestId: string; toolCallId: string; index: number } {1960if (uri.scheme !== ChatResponseResource.scheme) {1961return undefined;1962}19631964const parts = uri.path.split('/');1965if (parts.length < 5) {1966return undefined;1967}19681969const [, kind, requestId, toolCallId, index] = parts;1970if (kind !== 'tool') {1971return undefined;1972}19731974return {1975sessionId: uri.authority,1976requestId: requestId,1977toolCallId: toolCallId,1978index: Number(index),1979};1980}1981}198219831984