Path: blob/main/src/vs/workbench/contrib/chat/common/model/chatModel.ts
4780 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 { softAssertNever } from '../../../../../base/common/assert.js';7import { VSBuffer, decodeHex, encodeHex } from '../../../../../base/common/buffer.js';8import { BugIndicatingError } from '../../../../../base/common/errors.js';9import { Emitter, Event } from '../../../../../base/common/event.js';10import { IMarkdownString, MarkdownString, isMarkdownString } from '../../../../../base/common/htmlContent.js';11import { Disposable, IDisposable, MutableDisposable } from '../../../../../base/common/lifecycle.js';12import { ResourceMap } from '../../../../../base/common/map.js';13import { revive } from '../../../../../base/common/marshalling.js';14import { Schemas } from '../../../../../base/common/network.js';15import { equals } from '../../../../../base/common/objects.js';16import { IObservable, autorun, autorunSelfDisposable, derived, observableFromEvent, observableSignalFromEvent, observableValue, observableValueOpts } from '../../../../../base/common/observable.js';17import { basename, isEqual } from '../../../../../base/common/resources.js';18import { ThemeIcon } from '../../../../../base/common/themables.js';19import { WithDefinedProps } from '../../../../../base/common/types.js';20import { URI, UriComponents, UriDto, isUriComponents } from '../../../../../base/common/uri.js';21import { generateUuid } from '../../../../../base/common/uuid.js';22import { IRange } from '../../../../../editor/common/core/range.js';23import { OffsetRange } from '../../../../../editor/common/core/ranges/offsetRange.js';24import { ISelection } from '../../../../../editor/common/core/selection.js';25import { TextEdit } from '../../../../../editor/common/languages.js';26import { EditSuggestionId } from '../../../../../editor/common/textModelEditSource.js';27import { localize } from '../../../../../nls.js';28import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';29import { ILogService } from '../../../../../platform/log/common/log.js';30import { CellUri, ICellEditOperation } from '../../../notebook/common/notebookCommon.js';31import { ChatRequestToolReferenceEntry, IChatRequestVariableEntry } from '../attachments/chatVariableEntries.js';32import { migrateLegacyTerminalToolSpecificData } from '../chat.js';33import { ChatAgentVoteDirection, ChatAgentVoteDownReason, ChatResponseClearToPreviousToolInvocationReason, ElicitationState, IChatAgentMarkdownContentWithVulnerability, IChatClearToPreviousToolInvocation, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatEditingSessionAction, IChatElicitationRequest, IChatElicitationRequestSerialized, IChatExtensionsContent, IChatFollowup, IChatLocationData, IChatMarkdownContent, IChatMcpServersStarting, IChatModelReference, IChatMultiDiffData, IChatMultiDiffDataSerialized, IChatNotebookEdit, IChatPrepareToolInvocationPart, IChatProgress, IChatProgressMessage, IChatPullRequestContent, IChatResponseCodeblockUriPart, IChatResponseProgressFileTreeData, IChatService, IChatSessionContext, IChatSessionTiming, IChatTask, IChatTaskSerialized, IChatTextEdit, IChatThinkingPart, IChatToolInvocation, IChatToolInvocationSerialized, IChatTreeData, IChatUndoStop, IChatUsedContext, IChatWarningMessage, ResponseModelState, isIUsedContext } from '../chatService/chatService.js';34import { ChatAgentLocation, ChatModeKind } from '../constants.js';35import { IChatEditingService, IChatEditingSession, ModifiedFileEntryState, editEntriesToMultiDiffData } from '../editing/chatEditingService.js';36import { ILanguageModelChatMetadata, ILanguageModelChatMetadataAndIdentifier } from '../languageModels.js';37import { IChatAgentCommand, IChatAgentData, IChatAgentResult, IChatAgentService, UserSelectedTools, reviveSerializedAgent } from '../participants/chatAgents.js';38import { ChatRequestTextPart, IParsedChatRequest, reviveParsedChatRequest } from '../requestParser/chatParserTypes.js';39import { LocalChatSessionUri } from './chatUri.js';404142export const CHAT_ATTACHABLE_IMAGE_MIME_TYPES: Record<string, string> = {43png: 'image/png',44jpg: 'image/jpeg',45jpeg: 'image/jpeg',46gif: 'image/gif',47webp: 'image/webp',48};4950export function getAttachableImageExtension(mimeType: string): string | undefined {51return Object.entries(CHAT_ATTACHABLE_IMAGE_MIME_TYPES).find(([_, value]) => value === mimeType)?.[0];52}5354export interface IChatRequestVariableData {55variables: IChatRequestVariableEntry[];56}5758export namespace IChatRequestVariableData {59export function toExport(data: IChatRequestVariableData): IChatRequestVariableData {60return { variables: data.variables.map(IChatRequestVariableEntry.toExport) };61}62}6364export interface IChatRequestModel {65readonly id: string;66readonly timestamp: number;67readonly modeInfo?: IChatRequestModeInfo;68readonly session: IChatModel;69readonly message: IParsedChatRequest;70readonly attempt: number;71readonly variableData: IChatRequestVariableData;72readonly confirmation?: string;73readonly locationData?: IChatLocationData;74readonly attachedContext?: IChatRequestVariableEntry[];75readonly isCompleteAddedRequest: boolean;76readonly response?: IChatResponseModel;77readonly editedFileEvents?: IChatAgentEditedFileEvent[];78shouldBeRemovedOnSend: IChatRequestDisablement | undefined;79readonly shouldBeBlocked: IObservable<boolean>;80setShouldBeBlocked(value: boolean): void;81readonly modelId?: string;82readonly userSelectedTools?: UserSelectedTools;83}8485export interface ICodeBlockInfo {86readonly suggestionId: EditSuggestionId;87}8889export interface IChatTextEditGroupState {90sha1: string;91applied: number;92}9394export interface IChatTextEditGroup {95uri: URI;96edits: TextEdit[][];97state?: IChatTextEditGroupState;98kind: 'textEditGroup';99done: boolean | undefined;100isExternalEdit?: boolean;101}102103export function isCellTextEditOperation(value: unknown): value is ICellTextEditOperation {104const candidate = value as ICellTextEditOperation;105return !!candidate && !!candidate.edit && !!candidate.uri && URI.isUri(candidate.uri);106}107108export function isCellTextEditOperationArray(value: ICellTextEditOperation[] | ICellEditOperation[]): value is ICellTextEditOperation[] {109return value.some(isCellTextEditOperation);110}111112export interface ICellTextEditOperation {113edit: TextEdit;114uri: URI;115}116117export interface IChatNotebookEditGroup {118uri: URI;119edits: (ICellTextEditOperation[] | ICellEditOperation[])[];120state?: IChatTextEditGroupState;121kind: 'notebookEditGroup';122done: boolean | undefined;123isExternalEdit?: boolean;124}125126/**127* Progress kinds that are included in the history of a response.128* Excludes "internal" types that are included in history.129*/130export type IChatProgressHistoryResponseContent =131| IChatMarkdownContent132| IChatAgentMarkdownContentWithVulnerability133| IChatResponseCodeblockUriPart134| IChatTreeData135| IChatMultiDiffDataSerialized136| IChatContentInlineReference137| IChatProgressMessage138| IChatCommandButton139| IChatWarningMessage140| IChatTask141| IChatTaskSerialized142| IChatTextEditGroup143| IChatNotebookEditGroup144| IChatConfirmation145| IChatExtensionsContent146| IChatThinkingPart147| IChatPullRequestContent;148149/**150* "Normal" progress kinds that are rendered as parts of the stream of content.151*/152export type IChatProgressResponseContent =153| IChatProgressHistoryResponseContent154| IChatToolInvocation155| IChatToolInvocationSerialized156| IChatMultiDiffData157| IChatUndoStop158| IChatPrepareToolInvocationPart159| IChatElicitationRequest160| IChatElicitationRequestSerialized161| IChatClearToPreviousToolInvocation162| IChatMcpServersStarting;163164const nonHistoryKinds = new Set(['toolInvocation', 'toolInvocationSerialized', 'undoStop', 'prepareToolInvocation']);165function isChatProgressHistoryResponseContent(content: IChatProgressResponseContent): content is IChatProgressHistoryResponseContent {166return !nonHistoryKinds.has(content.kind);167}168169export function toChatHistoryContent(content: ReadonlyArray<IChatProgressResponseContent>): IChatProgressHistoryResponseContent[] {170return content.filter(isChatProgressHistoryResponseContent);171}172173export type IChatProgressRenderableResponseContent = Exclude<IChatProgressResponseContent, IChatContentInlineReference | IChatAgentMarkdownContentWithVulnerability | IChatResponseCodeblockUriPart>;174175export interface IResponse {176readonly value: ReadonlyArray<IChatProgressResponseContent>;177getMarkdown(): string;178toString(): string;179}180181export interface IChatResponseModel {182readonly onDidChange: Event<ChatResponseModelChangeReason>;183readonly id: string;184readonly requestId: string;185readonly request: IChatRequestModel | undefined;186readonly username: string;187readonly avatarIcon?: ThemeIcon | URI;188readonly session: IChatModel;189readonly agent?: IChatAgentData;190readonly usedContext: IChatUsedContext | undefined;191readonly contentReferences: ReadonlyArray<IChatContentReference>;192readonly codeCitations: ReadonlyArray<IChatCodeCitation>;193readonly progressMessages: ReadonlyArray<IChatProgressMessage>;194readonly slashCommand?: IChatAgentCommand;195readonly agentOrSlashCommandDetected: boolean;196/** View of the response shown to the user, may have parts omitted from undo stops. */197readonly response: IResponse;198/** Entire response from the model. */199readonly entireResponse: IResponse;200/** Milliseconds timestamp when this chat response was created. */201readonly timestamp: number;202/** Milliseconds timestamp when this chat response was completed or cancelled. */203readonly completedAt?: number;204/** The state of this response */205readonly state: ResponseModelState;206/**207* Adjusted millisecond timestamp that excludes the duration during which208* the model was pending user confirmation. `Date.now() - confirmationAdjustedTimestamp`209* will return the amount of time the response was busy generating content.210* This is updated only when `isPendingConfirmation` changes state.211*/212readonly confirmationAdjustedTimestamp: IObservable<number>;213readonly isComplete: boolean;214readonly isCanceled: boolean;215readonly isPendingConfirmation: IObservable<{ startedWaitingAt: number; detail?: string } | undefined>;216readonly isInProgress: IObservable<boolean>;217readonly shouldBeRemovedOnSend: IChatRequestDisablement | undefined;218readonly shouldBeBlocked: IObservable<boolean>;219readonly isCompleteAddedRequest: boolean;220/** 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. */221readonly isStale: boolean;222readonly vote: ChatAgentVoteDirection | undefined;223readonly voteDownReason: ChatAgentVoteDownReason | undefined;224readonly followups?: IChatFollowup[] | undefined;225readonly result?: IChatAgentResult;226readonly codeBlockInfos: ICodeBlockInfo[] | undefined;227228initializeCodeBlockInfos(codeBlockInfo: ICodeBlockInfo[]): void;229addUndoStop(undoStop: IChatUndoStop): void;230setVote(vote: ChatAgentVoteDirection): void;231setVoteDownReason(reason: ChatAgentVoteDownReason | undefined): void;232setEditApplied(edit: IChatTextEditGroup, editCount: number): boolean;233updateContent(progress: IChatProgressResponseContent | IChatTextEdit | IChatNotebookEdit | IChatTask, quiet?: boolean): void;234/**235* Adopts any partially-undo {@link response} as the {@link entireResponse}.236* Only valid when {@link isComplete}. This is needed because otherwise an237* undone and then diverged state would start showing old data because the238* undo stops would no longer exist in the model.239*/240finalizeUndoState(): void;241}242243export type ChatResponseModelChangeReason =244| { reason: 'other' }245| { reason: 'completedRequest' }246| { reason: 'undoStop'; id: string };247248export const defaultChatResponseModelChangeReason: ChatResponseModelChangeReason = { reason: 'other' };249250export interface IChatRequestModeInfo {251kind: ChatModeKind | undefined; // is undefined in case of modeId == 'apply'252isBuiltin: boolean;253modeInstructions: IChatRequestModeInstructions | undefined;254modeId: 'ask' | 'agent' | 'edit' | 'custom' | 'applyCodeBlock' | undefined;255applyCodeBlockSuggestionId: EditSuggestionId | undefined;256}257258export interface IChatRequestModeInstructions {259readonly name: string;260readonly content: string;261readonly toolReferences: readonly ChatRequestToolReferenceEntry[];262readonly metadata?: Record<string, boolean | string | number>;263}264265export interface IChatRequestModelParameters {266session: ChatModel;267message: IParsedChatRequest;268variableData: IChatRequestVariableData;269timestamp: number;270attempt?: number;271modeInfo?: IChatRequestModeInfo;272confirmation?: string;273locationData?: IChatLocationData;274attachedContext?: IChatRequestVariableEntry[];275isCompleteAddedRequest?: boolean;276modelId?: string;277restoredId?: string;278editedFileEvents?: IChatAgentEditedFileEvent[];279userSelectedTools?: UserSelectedTools;280}281282export class ChatRequestModel implements IChatRequestModel {283public readonly id: string;284public response: ChatResponseModel | undefined;285public shouldBeRemovedOnSend: IChatRequestDisablement | undefined;286public readonly timestamp: number;287public readonly message: IParsedChatRequest;288public readonly isCompleteAddedRequest: boolean;289public readonly modelId?: string;290public readonly modeInfo?: IChatRequestModeInfo;291public readonly userSelectedTools?: UserSelectedTools;292293private readonly _shouldBeBlocked = observableValue<boolean>(this, false);294public get shouldBeBlocked(): IObservable<boolean> {295return this._shouldBeBlocked;296}297298public setShouldBeBlocked(value: boolean): void {299this._shouldBeBlocked.set(value, undefined);300}301302private _session: ChatModel;303private readonly _attempt: number;304private _variableData: IChatRequestVariableData;305private readonly _confirmation?: string;306private readonly _locationData?: IChatLocationData;307private readonly _attachedContext?: IChatRequestVariableEntry[];308private readonly _editedFileEvents?: IChatAgentEditedFileEvent[];309310public get session(): ChatModel {311return this._session;312}313314public get attempt(): number {315return this._attempt;316}317318public get variableData(): IChatRequestVariableData {319return this._variableData;320}321322public set variableData(v: IChatRequestVariableData) {323this._variableData = v;324}325326public get confirmation(): string | undefined {327return this._confirmation;328}329330public get locationData(): IChatLocationData | undefined {331return this._locationData;332}333334public get attachedContext(): IChatRequestVariableEntry[] | undefined {335return this._attachedContext;336}337338public get editedFileEvents(): IChatAgentEditedFileEvent[] | undefined {339return this._editedFileEvents;340}341342constructor(params: IChatRequestModelParameters) {343this._session = params.session;344this.message = params.message;345this._variableData = params.variableData;346this.timestamp = params.timestamp;347this._attempt = params.attempt ?? 0;348this.modeInfo = params.modeInfo;349this._confirmation = params.confirmation;350this._locationData = params.locationData;351this._attachedContext = params.attachedContext;352this.isCompleteAddedRequest = params.isCompleteAddedRequest ?? false;353this.modelId = params.modelId;354this.id = params.restoredId ?? 'request_' + generateUuid();355this._editedFileEvents = params.editedFileEvents;356this.userSelectedTools = params.userSelectedTools;357}358359adoptTo(session: ChatModel) {360this._session = session;361}362}363364class AbstractResponse implements IResponse {365protected _responseParts: IChatProgressResponseContent[];366367/**368* A stringified representation of response data which might be presented to a screenreader or used when copying a response.369*/370protected _responseRepr = '';371372/**373* Just the markdown content of the response, used for determining the rendering rate of markdown374*/375protected _markdownContent = '';376377get value(): IChatProgressResponseContent[] {378return this._responseParts;379}380381constructor(value: IChatProgressResponseContent[]) {382this._responseParts = value;383this._updateRepr();384}385386toString(): string {387return this._responseRepr;388}389390/**391* _Just_ the content of markdown parts in the response392*/393getMarkdown(): string {394return this._markdownContent;395}396397protected _updateRepr() {398this._responseRepr = this.partsToRepr(this._responseParts);399400this._markdownContent = this._responseParts.map(part => {401if (part.kind === 'inlineReference') {402return this.inlineRefToRepr(part);403} else if (part.kind === 'markdownContent' || part.kind === 'markdownVuln') {404return part.content.value;405} else {406return '';407}408})409.filter(s => s.length > 0)410.join('');411}412413private partsToRepr(parts: readonly IChatProgressResponseContent[]): string {414const blocks: string[] = [];415let currentBlockSegments: string[] = [];416let hasEditGroupsAfterLastClear = false;417418for (const part of parts) {419let segment: { text: string; isBlock?: boolean } | undefined;420switch (part.kind) {421case 'clearToPreviousToolInvocation':422currentBlockSegments = [];423blocks.length = 0;424hasEditGroupsAfterLastClear = false; // Reset edit groups flag when clearing425continue;426case 'treeData':427case 'progressMessage':428case 'codeblockUri':429case 'extensions':430case 'pullRequest':431case 'undoStop':432case 'prepareToolInvocation':433case 'elicitation2':434case 'elicitationSerialized':435case 'thinking':436case 'multiDiffData':437case 'mcpServersStarting':438// Ignore439continue;440case 'toolInvocation':441case 'toolInvocationSerialized':442// Include tool invocations in the copy text443segment = this.getToolInvocationText(part);444break;445case 'inlineReference':446segment = { text: this.inlineRefToRepr(part) };447break;448case 'command':449segment = { text: part.command.title, isBlock: true };450break;451case 'textEditGroup':452case 'notebookEditGroup':453// Mark that we have edit groups after the last clear454hasEditGroupsAfterLastClear = true;455// Skip individual edit groups to avoid duplication456continue;457case 'confirmation':458if (part.message instanceof MarkdownString) {459segment = { text: `${part.title}\n${part.message.value}`, isBlock: true };460break;461}462segment = { text: `${part.title}\n${part.message}`, isBlock: true };463break;464case 'markdownContent':465case 'markdownVuln':466case 'progressTask':467case 'progressTaskSerialized':468case 'warning':469segment = { text: part.content.value };470break;471default:472// Ignore any unknown/obsolete parts, but assert that all are handled:473softAssertNever(part);474continue;475}476477if (segment.isBlock) {478if (currentBlockSegments.length) {479blocks.push(currentBlockSegments.join(''));480currentBlockSegments = [];481}482blocks.push(segment.text);483} else {484currentBlockSegments.push(segment.text);485}486}487488if (currentBlockSegments.length) {489blocks.push(currentBlockSegments.join(''));490}491492// Add consolidated edit summary at the end if there were any edit groups after the last clear493if (hasEditGroupsAfterLastClear) {494blocks.push(localize('editsSummary', "Made changes."));495}496497return blocks.join('\n\n');498}499500private inlineRefToRepr(part: IChatContentInlineReference) {501if ('uri' in part.inlineReference) {502return this.uriToRepr(part.inlineReference.uri);503}504505return 'name' in part.inlineReference506? '`' + part.inlineReference.name + '`'507: this.uriToRepr(part.inlineReference);508}509510private getToolInvocationText(toolInvocation: IChatToolInvocation | IChatToolInvocationSerialized): { text: string; isBlock?: boolean } {511// Extract the message and input details512let message = '';513let input = '';514515if (toolInvocation.pastTenseMessage) {516message = typeof toolInvocation.pastTenseMessage === 'string'517? toolInvocation.pastTenseMessage518: toolInvocation.pastTenseMessage.value;519} else {520message = typeof toolInvocation.invocationMessage === 'string'521? toolInvocation.invocationMessage522: toolInvocation.invocationMessage.value;523}524525// Handle different types of tool invocations526if (toolInvocation.toolSpecificData) {527if (toolInvocation.toolSpecificData.kind === 'terminal') {528message = 'Ran terminal command';529const terminalData = migrateLegacyTerminalToolSpecificData(toolInvocation.toolSpecificData);530input = terminalData.commandLine.userEdited ?? terminalData.commandLine.toolEdited ?? terminalData.commandLine.original;531}532}533534// Format the tool invocation text535let text = message;536if (input) {537text += `: ${input}`;538}539540// For completed tool invocations, also include the result details if available541if (toolInvocation.kind === 'toolInvocationSerialized' || (toolInvocation.kind === 'toolInvocation' && IChatToolInvocation.isComplete(toolInvocation))) {542const resultDetails = IChatToolInvocation.resultDetails(toolInvocation);543if (resultDetails && 'input' in resultDetails) {544const resultPrefix = toolInvocation.kind === 'toolInvocationSerialized' || IChatToolInvocation.isComplete(toolInvocation) ? 'Completed' : 'Errored';545text += `\n${resultPrefix} with input: ${resultDetails.input}`;546}547}548549return { text, isBlock: true };550}551552private uriToRepr(uri: URI): string {553if (uri.scheme === Schemas.http || uri.scheme === Schemas.https) {554return uri.toString(false);555}556557return basename(uri);558}559}560561/** A view of a subset of a response */562class ResponseView extends AbstractResponse {563constructor(564_response: IResponse,565public readonly undoStop: string,566) {567let idx = _response.value.findIndex(v => v.kind === 'undoStop' && v.id === undoStop);568// Undo stops are inserted before `codeblockUri`'s, which are preceeded by a569// markdownContent containing the opening code fence. Adjust the index570// backwards to avoid a buggy response if it looked like this happened.571if (_response.value[idx + 1]?.kind === 'codeblockUri' && _response.value[idx - 1]?.kind === 'markdownContent') {572idx--;573}574575super(idx === -1 ? _response.value.slice() : _response.value.slice(0, idx));576}577}578579export class Response extends AbstractResponse implements IDisposable {580private _onDidChangeValue = new Emitter<void>();581public get onDidChangeValue() {582return this._onDidChangeValue.event;583}584585private _citations: IChatCodeCitation[] = [];586587588constructor(value: IMarkdownString | ReadonlyArray<IMarkdownString | IChatResponseProgressFileTreeData | IChatContentInlineReference | IChatAgentMarkdownContentWithVulnerability | IChatResponseCodeblockUriPart | IChatThinkingPart>) {589super(asArray(value).map((v) => (590'kind' in v ? v :591isMarkdownString(v) ? { content: v, kind: 'markdownContent' } satisfies IChatMarkdownContent :592{ kind: 'treeData', treeData: v }593)));594}595596dispose(): void {597this._onDidChangeValue.dispose();598}599600601clear(): void {602this._responseParts = [];603this._updateRepr(true);604}605606clearToPreviousToolInvocation(message?: string): void {607// look through the response parts and find the last tool invocation, then slice the response parts to that point608let lastToolInvocationIndex = -1;609for (let i = this._responseParts.length - 1; i >= 0; i--) {610const part = this._responseParts[i];611if (part.kind === 'toolInvocation' || part.kind === 'toolInvocationSerialized') {612lastToolInvocationIndex = i;613break;614}615}616if (lastToolInvocationIndex !== -1) {617this._responseParts = this._responseParts.slice(0, lastToolInvocationIndex + 1);618} else {619this._responseParts = [];620}621if (message) {622this._responseParts.push({ kind: 'warning', content: new MarkdownString(message) });623}624this._updateRepr(true);625}626627updateContent(progress: IChatProgressResponseContent | IChatTextEdit | IChatNotebookEdit | IChatTask, quiet?: boolean): void {628if (progress.kind === 'clearToPreviousToolInvocation') {629if (progress.reason === ChatResponseClearToPreviousToolInvocationReason.CopyrightContentRetry) {630this.clearToPreviousToolInvocation(localize('copyrightContentRetry', "Response cleared due to possible match to public code, retrying with modified prompt."));631} else if (progress.reason === ChatResponseClearToPreviousToolInvocationReason.FilteredContentRetry) {632this.clearToPreviousToolInvocation(localize('filteredContentRetry', "Response cleared due to content safety filters, retrying with modified prompt."));633} else {634this.clearToPreviousToolInvocation();635}636return;637} else if (progress.kind === 'markdownContent') {638639// last response which is NOT a text edit group because we do want to support heterogenous streaming but not have640// the MD be chopped up by text edit groups (and likely other non-renderable parts)641const lastResponsePart = this._responseParts642.filter(p => p.kind !== 'textEditGroup')643.at(-1);644645if (!lastResponsePart || lastResponsePart.kind !== 'markdownContent' || !canMergeMarkdownStrings(lastResponsePart.content, progress.content)) {646// The last part can't be merged with- not markdown, or markdown with different permissions647this._responseParts.push(progress);648} else {649// Don't modify the current object, since it's being diffed by the renderer650const idx = this._responseParts.indexOf(lastResponsePart);651this._responseParts[idx] = { ...lastResponsePart, content: appendMarkdownString(lastResponsePart.content, progress.content) };652}653this._updateRepr(quiet);654} else if (progress.kind === 'thinking') {655656// tries to split thinking chunks if it is an array. only while certain models give us array chunks.657const lastResponsePart = this._responseParts658.filter(p => p.kind !== 'textEditGroup')659.at(-1);660661const lastText = lastResponsePart && lastResponsePart.kind === 'thinking'662? (Array.isArray(lastResponsePart.value) ? lastResponsePart.value.join('') : (lastResponsePart.value || ''))663: '';664const currText = Array.isArray(progress.value) ? progress.value.join('') : (progress.value || '');665const isEmpty = (s: string) => s.length === 0;666667// Do not merge if either the current or last thinking chunk is empty; empty chunks separate thinking668if (!lastResponsePart669|| lastResponsePart.kind !== 'thinking'670|| isEmpty(currText)671|| isEmpty(lastText)672|| !canMergeMarkdownStrings(new MarkdownString(lastText), new MarkdownString(currText))) {673this._responseParts.push(progress);674} else {675const idx = this._responseParts.indexOf(lastResponsePart);676this._responseParts[idx] = {677...lastResponsePart,678value: appendMarkdownString(new MarkdownString(lastText), new MarkdownString(currText)).value679};680}681this._updateRepr(quiet);682} else if (progress.kind === 'textEdit' || progress.kind === 'notebookEdit') {683// merge edits for the same file no matter when they come in684const notebookUri = CellUri.parse(progress.uri)?.notebook;685const uri = notebookUri ?? progress.uri;686const isExternalEdit = progress.isExternalEdit;687688if (progress.kind === 'textEdit' && !notebookUri) {689// Text edits to a regular (non-notebook) file690this._mergeOrPushTextEditGroup(uri, progress.edits, progress.done, isExternalEdit);691} else if (progress.kind === 'textEdit') {692// Text edits to a notebook cell - convert to ICellTextEditOperation693const cellEdits = progress.edits.map(edit => ({ uri: progress.uri, edit }));694this._mergeOrPushNotebookEditGroup(uri, cellEdits, progress.done, isExternalEdit);695} else {696// Notebook cell edits (ICellEditOperation)697this._mergeOrPushNotebookEditGroup(uri, progress.edits, progress.done, isExternalEdit);698}699this._updateRepr(quiet);700} else if (progress.kind === 'progressTask') {701// Add a new resolving part702const responsePosition = this._responseParts.push(progress) - 1;703this._updateRepr(quiet);704705const disp = progress.onDidAddProgress(() => {706this._updateRepr(false);707});708709progress.task?.().then((content) => {710// Stop listening for progress updates once the task settles711disp.dispose();712713// Replace the resolving part's content with the resolved response714if (typeof content === 'string') {715(this._responseParts[responsePosition] as IChatTask).content = new MarkdownString(content);716}717this._updateRepr(false);718});719720} else if (progress.kind === 'toolInvocation') {721autorunSelfDisposable(reader => {722progress.state.read(reader); // update repr when state changes723this._updateRepr(false);724725if (IChatToolInvocation.isComplete(progress, reader)) {726reader.dispose();727}728});729this._responseParts.push(progress);730this._updateRepr(quiet);731} else {732this._responseParts.push(progress);733this._updateRepr(quiet);734}735}736737public addCitation(citation: IChatCodeCitation) {738this._citations.push(citation);739this._updateRepr();740}741742private _mergeOrPushTextEditGroup(uri: URI, edits: TextEdit[], done: boolean | undefined, isExternalEdit: boolean | undefined): void {743for (const candidate of this._responseParts) {744if (candidate.kind === 'textEditGroup' && !candidate.done && isEqual(candidate.uri, uri)) {745candidate.edits.push(edits);746candidate.done = done;747return;748}749}750this._responseParts.push({ kind: 'textEditGroup', uri, edits: [edits], done, isExternalEdit });751}752753private _mergeOrPushNotebookEditGroup(uri: URI, edits: ICellTextEditOperation[] | ICellEditOperation[], done: boolean | undefined, isExternalEdit: boolean | undefined): void {754for (const candidate of this._responseParts) {755if (candidate.kind === 'notebookEditGroup' && !candidate.done && isEqual(candidate.uri, uri)) {756candidate.edits.push(edits);757candidate.done = done;758return;759}760}761this._responseParts.push({ kind: 'notebookEditGroup', uri, edits: [edits], done, isExternalEdit });762}763764protected override _updateRepr(quiet?: boolean) {765super._updateRepr();766if (!this._onDidChangeValue) {767return; // called from parent constructor768}769770this._responseRepr += this._citations.length ? '\n\n' + getCodeCitationsMessage(this._citations) : '';771772if (!quiet) {773this._onDidChangeValue.fire();774}775}776}777778export interface IChatResponseModelParameters {779responseContent: IMarkdownString | ReadonlyArray<IMarkdownString | IChatResponseProgressFileTreeData | IChatContentInlineReference | IChatAgentMarkdownContentWithVulnerability | IChatResponseCodeblockUriPart | IChatThinkingPart>;780session: ChatModel;781agent?: IChatAgentData;782slashCommand?: IChatAgentCommand;783requestId: string;784timestamp?: number;785vote?: ChatAgentVoteDirection;786voteDownReason?: ChatAgentVoteDownReason;787result?: IChatAgentResult;788followups?: ReadonlyArray<IChatFollowup>;789isCompleteAddedRequest?: boolean;790shouldBeRemovedOnSend?: IChatRequestDisablement;791shouldBeBlocked?: boolean;792restoredId?: string;793modelState?: ResponseModelStateT;794timeSpentWaiting?: number;795/**796* undefined means it will be set later.797*/798codeBlockInfos: ICodeBlockInfo[] | undefined;799}800801type ResponseModelStateT =802| { value: ResponseModelState.Pending }803| { value: ResponseModelState.NeedsInput }804| { value: ResponseModelState.Complete | ResponseModelState.Cancelled | ResponseModelState.Failed; completedAt: number };805806export class ChatResponseModel extends Disposable implements IChatResponseModel {807private readonly _onDidChange = this._register(new Emitter<ChatResponseModelChangeReason>());808readonly onDidChange = this._onDidChange.event;809810public readonly id: string;811public readonly requestId: string;812private _session: ChatModel;813private _agent: IChatAgentData | undefined;814private _slashCommand: IChatAgentCommand | undefined;815private _modelState = observableValue<ResponseModelStateT>(this, { value: ResponseModelState.Pending });816private _vote?: ChatAgentVoteDirection;817private _voteDownReason?: ChatAgentVoteDownReason;818private _result?: IChatAgentResult;819private _shouldBeRemovedOnSend: IChatRequestDisablement | undefined;820public readonly isCompleteAddedRequest: boolean;821private readonly _shouldBeBlocked = observableValue<boolean>(this, false);822private readonly _timestamp: number;823private _timeSpentWaitingAccumulator: number;824825public confirmationAdjustedTimestamp: IObservable<number>;826827public get shouldBeBlocked(): IObservable<boolean> {828return this._shouldBeBlocked;829}830831public get request(): IChatRequestModel | undefined {832return this.session.getRequests().find(r => r.id === this.requestId);833}834835public get session() {836return this._session;837}838839public get shouldBeRemovedOnSend() {840return this._shouldBeRemovedOnSend;841}842843public get isComplete(): boolean {844return this._modelState.get().value !== ResponseModelState.Pending && this._modelState.get().value !== ResponseModelState.NeedsInput;845}846847public get timestamp(): number {848return this._timestamp;849}850851public set shouldBeRemovedOnSend(disablement: IChatRequestDisablement | undefined) {852this._shouldBeRemovedOnSend = disablement;853this._onDidChange.fire(defaultChatResponseModelChangeReason);854}855856public get isCanceled(): boolean {857return this._modelState.get().value === ResponseModelState.Cancelled;858}859860public get completedAt(): number | undefined {861const state = this._modelState.get();862if (state.value === ResponseModelState.Complete || state.value === ResponseModelState.Cancelled || state.value === ResponseModelState.Failed) {863return state.completedAt;864}865return undefined;866}867868public get state(): ResponseModelState {869const state = this._modelState.get().value;870if (state === ResponseModelState.Complete && !!this._result?.errorDetails && this.result?.errorDetails?.code !== 'canceled') {871// This check covers sessions created in previous vscode versions which saved a failed response as 'Complete'872return ResponseModelState.Failed;873}874875return state;876}877878public get vote(): ChatAgentVoteDirection | undefined {879return this._vote;880}881882public get voteDownReason(): ChatAgentVoteDownReason | undefined {883return this._voteDownReason;884}885886public get followups(): IChatFollowup[] | undefined {887return this._followups;888}889890private _response: Response;891private _finalizedResponse?: IResponse;892public get entireResponse(): IResponse {893return this._finalizedResponse || this._response;894}895896public get result(): IChatAgentResult | undefined {897return this._result;898}899900public get username(): string {901return this.session.responderUsername;902}903904public get avatarIcon(): ThemeIcon | URI | undefined {905return this.session.responderAvatarIcon;906}907908private _followups?: IChatFollowup[];909910public get agent(): IChatAgentData | undefined {911return this._agent;912}913914public get slashCommand(): IChatAgentCommand | undefined {915return this._slashCommand;916}917918private _agentOrSlashCommandDetected: boolean | undefined;919public get agentOrSlashCommandDetected(): boolean {920return this._agentOrSlashCommandDetected ?? false;921}922923private _usedContext: IChatUsedContext | undefined;924public get usedContext(): IChatUsedContext | undefined {925return this._usedContext;926}927928private readonly _contentReferences: IChatContentReference[] = [];929public get contentReferences(): ReadonlyArray<IChatContentReference> {930return Array.from(this._contentReferences);931}932933private readonly _codeCitations: IChatCodeCitation[] = [];934public get codeCitations(): ReadonlyArray<IChatCodeCitation> {935return this._codeCitations;936}937938private readonly _progressMessages: IChatProgressMessage[] = [];939public get progressMessages(): ReadonlyArray<IChatProgressMessage> {940return this._progressMessages;941}942943private _isStale: boolean = false;944public get isStale(): boolean {945return this._isStale;946}947948949readonly isPendingConfirmation: IObservable<{ startedWaitingAt: number; detail?: string } | undefined>;950951readonly isInProgress: IObservable<boolean>;952953private _responseView?: ResponseView;954public get response(): IResponse {955const undoStop = this._shouldBeRemovedOnSend?.afterUndoStop;956if (!undoStop) {957return this._finalizedResponse || this._response;958}959960if (this._responseView?.undoStop !== undoStop) {961this._responseView = new ResponseView(this._response, undoStop);962}963964return this._responseView;965}966967private _codeBlockInfos: ICodeBlockInfo[] | undefined;968public get codeBlockInfos(): ICodeBlockInfo[] | undefined {969return this._codeBlockInfos;970}971972constructor(params: IChatResponseModelParameters) {973super();974975this._session = params.session;976this._agent = params.agent;977this._slashCommand = params.slashCommand;978this.requestId = params.requestId;979this._timestamp = params.timestamp || Date.now();980if (params.modelState) {981this._modelState.set(params.modelState, undefined);982}983this._timeSpentWaitingAccumulator = params.timeSpentWaiting || 0;984this._vote = params.vote;985this._voteDownReason = params.voteDownReason;986this._result = params.result;987this._followups = params.followups ? [...params.followups] : undefined;988this.isCompleteAddedRequest = params.isCompleteAddedRequest ?? false;989this._shouldBeRemovedOnSend = params.shouldBeRemovedOnSend;990this._shouldBeBlocked.set(params.shouldBeBlocked ?? false, undefined);991992// If we are creating a response with some existing content, consider it stale993this._isStale = Array.isArray(params.responseContent) && (params.responseContent.length !== 0 || isMarkdownString(params.responseContent) && params.responseContent.value.length !== 0);994995this._response = this._register(new Response(params.responseContent));996this._codeBlockInfos = params.codeBlockInfos ? [...params.codeBlockInfos] : undefined;997998const signal = observableSignalFromEvent(this, this.onDidChange);9991000const _pendingInfo = signal.map((_value, r): string | undefined => {1001signal.read(r);10021003for (const part of this._response.value) {1004if (part.kind === 'toolInvocation' && part.state.read(r).type === IChatToolInvocation.StateKind.WaitingForConfirmation) {1005const title = part.confirmationMessages?.title;1006return title ? (isMarkdownString(title) ? title.value : title) : undefined;1007}1008if (part.kind === 'confirmation' && !part.isUsed) {1009return part.title;1010}1011if (part.kind === 'elicitation2' && part.state.read(r) === ElicitationState.Pending) {1012const title = part.title;1013return isMarkdownString(title) ? title.value : title;1014}1015}10161017return undefined;1018});10191020const _startedWaitingAt = _pendingInfo.map(p => !!p).map(p => p ? Date.now() : undefined);1021this.isPendingConfirmation = _startedWaitingAt.map((waiting, r) => waiting ? { startedWaitingAt: waiting, detail: _pendingInfo.read(r) } : undefined);10221023this.isInProgress = signal.map((_value, r) => {10241025signal.read(r);10261027return !_pendingInfo.read(r)1028&& !this.shouldBeRemovedOnSend1029&& (this._modelState.read(r).value === ResponseModelState.Pending || this._modelState.read(r).value === ResponseModelState.NeedsInput);1030});10311032this._register(this._response.onDidChangeValue(() => this._onDidChange.fire(defaultChatResponseModelChangeReason)));1033this.id = params.restoredId ?? 'response_' + generateUuid();10341035this._register(this._session.onDidChange((e) => {1036if (e.kind === 'setCheckpoint') {1037const isDisabled = e.disabledResponseIds.has(this.id);1038this._shouldBeBlocked.set(isDisabled, undefined);1039}1040}));10411042let lastStartedWaitingAt: number | undefined = undefined;1043this.confirmationAdjustedTimestamp = derived(reader => {1044const pending = this.isPendingConfirmation.read(reader);1045if (pending) {1046this._modelState.set({ value: ResponseModelState.NeedsInput }, undefined);1047if (!lastStartedWaitingAt) {1048lastStartedWaitingAt = pending.startedWaitingAt;1049}1050} else if (lastStartedWaitingAt) {1051// Restore state to Pending if it was set to NeedsInput by this observable1052if (this._modelState.read(reader).value === ResponseModelState.NeedsInput) {1053this._modelState.set({ value: ResponseModelState.Pending }, undefined);1054}1055this._timeSpentWaitingAccumulator += Date.now() - lastStartedWaitingAt;1056lastStartedWaitingAt = undefined;1057}10581059return this._timestamp + this._timeSpentWaitingAccumulator;1060}).recomputeInitiallyAndOnChange(this._store);1061}10621063initializeCodeBlockInfos(codeBlockInfo: ICodeBlockInfo[]): void {1064if (this._codeBlockInfos) {1065throw new BugIndicatingError('Code block infos have already been initialized');1066}1067this._codeBlockInfos = [...codeBlockInfo];1068}10691070/**1071* Apply a progress update to the actual response content.1072*/1073updateContent(responsePart: IChatProgressResponseContent | IChatTextEdit | IChatNotebookEdit, quiet?: boolean) {1074this._response.updateContent(responsePart, quiet);1075}10761077/**1078* Adds an undo stop at the current position in the stream.1079*/1080addUndoStop(undoStop: IChatUndoStop) {1081this._onDidChange.fire({ reason: 'undoStop', id: undoStop.id });1082this._response.updateContent(undoStop, true);1083}10841085/**1086* Apply one of the progress updates that are not part of the actual response content.1087*/1088applyReference(progress: IChatUsedContext | IChatContentReference) {1089if (progress.kind === 'usedContext') {1090this._usedContext = progress;1091} else if (progress.kind === 'reference') {1092this._contentReferences.push(progress);1093this._onDidChange.fire(defaultChatResponseModelChangeReason);1094}1095}10961097applyCodeCitation(progress: IChatCodeCitation) {1098this._codeCitations.push(progress);1099this._response.addCitation(progress);1100this._onDidChange.fire(defaultChatResponseModelChangeReason);1101}11021103setAgent(agent: IChatAgentData, slashCommand?: IChatAgentCommand) {1104this._agent = agent;1105this._slashCommand = slashCommand;1106this._agentOrSlashCommandDetected = !agent.isDefault || !!slashCommand;1107this._onDidChange.fire(defaultChatResponseModelChangeReason);1108}11091110setResult(result: IChatAgentResult): void {1111this._result = result;1112this._onDidChange.fire(defaultChatResponseModelChangeReason);1113}11141115complete(): void {1116// No-op if it's already complete1117if (this.isComplete) {1118return;1119}1120if (this._result?.errorDetails?.responseIsRedacted) {1121this._response.clear();1122}11231124// Canceled sessions can be considered 'Complete'1125const state = !!this._result?.errorDetails && this._result.errorDetails.code !== 'canceled' ? ResponseModelState.Failed : ResponseModelState.Complete;1126this._modelState.set({ value: state, completedAt: Date.now() }, undefined);1127this._onDidChange.fire({ reason: 'completedRequest' });1128}11291130cancel(): void {1131this._modelState.set({ value: ResponseModelState.Cancelled, completedAt: Date.now() }, undefined);1132this._onDidChange.fire({ reason: 'completedRequest' });1133}11341135setFollowups(followups: IChatFollowup[] | undefined): void {1136this._followups = followups;1137this._onDidChange.fire(defaultChatResponseModelChangeReason); // Fire so that command followups get rendered on the row1138}11391140setVote(vote: ChatAgentVoteDirection): void {1141this._vote = vote;1142this._onDidChange.fire(defaultChatResponseModelChangeReason);1143}11441145setVoteDownReason(reason: ChatAgentVoteDownReason | undefined): void {1146this._voteDownReason = reason;1147this._onDidChange.fire(defaultChatResponseModelChangeReason);1148}11491150setEditApplied(edit: IChatTextEditGroup, editCount: number): boolean {1151if (!this.response.value.includes(edit)) {1152return false;1153}1154if (!edit.state) {1155return false;1156}1157edit.state.applied = editCount; // must not be edit.edits.length1158this._onDidChange.fire(defaultChatResponseModelChangeReason);1159return true;1160}11611162adoptTo(session: ChatModel) {1163this._session = session;1164this._onDidChange.fire(defaultChatResponseModelChangeReason);1165}116611671168finalizeUndoState(): void {1169this._finalizedResponse = this.response;1170this._responseView = undefined;1171this._shouldBeRemovedOnSend = undefined;1172}11731174toJSON(): ISerializableChatResponseData {1175const modelState = this._modelState.get();1176const pendingConfirmation = this.isPendingConfirmation.get();11771178return {1179responseId: this.id,1180result: this.result,1181responseMarkdownInfo: this.codeBlockInfos?.map<ISerializableMarkdownInfo>(info => ({ suggestionId: info.suggestionId })),1182followups: this.followups,1183modelState: modelState.value === ResponseModelState.Pending || modelState.value === ResponseModelState.NeedsInput ? { value: ResponseModelState.Cancelled, completedAt: Date.now() } : modelState,1184vote: this.vote,1185voteDownReason: this.voteDownReason,1186slashCommand: this.slashCommand,1187usedContext: this.usedContext,1188contentReferences: this.contentReferences,1189codeCitations: this.codeCitations,1190timestamp: this._timestamp,1191timeSpentWaiting: (pendingConfirmation ? Date.now() - pendingConfirmation.startedWaitingAt : 0) + this._timeSpentWaitingAccumulator,1192} satisfies WithDefinedProps<ISerializableChatResponseData>;1193}1194}119511961197export interface IChatRequestDisablement {1198requestId: string;1199afterUndoStop?: string;1200}12011202/**1203* Information about a chat request that needs user input to continue.1204*/1205export interface IChatRequestNeedsInputInfo {1206/** The chat session title */1207readonly title: string;1208/** Optional detail message, e.g., "<toolname> needs approval to run." */1209readonly detail?: string;1210}12111212export interface IChatModel extends IDisposable {1213readonly onDidDispose: Event<void>;1214readonly onDidChange: Event<IChatChangeEvent>;1215/** @deprecated Use {@link sessionResource} instead */1216readonly sessionId: string;1217/** Milliseconds timestamp this chat model was created. */1218readonly timestamp: number;1219readonly timing: IChatSessionTiming;1220readonly sessionResource: URI;1221readonly initialLocation: ChatAgentLocation;1222readonly title: string;1223readonly hasCustomTitle: boolean;1224/** True whenever a request is currently running */1225readonly requestInProgress: IObservable<boolean>;1226/** Provides session information when a request needs user interaction to continue */1227readonly requestNeedsInput: IObservable<IChatRequestNeedsInputInfo | undefined>;1228readonly inputPlaceholder?: string;1229readonly editingSession?: IChatEditingSession | undefined;1230readonly checkpoint: IChatRequestModel | undefined;1231startEditingSession(isGlobalEditingSession?: boolean, transferFromSession?: IChatEditingSession): void;1232/** Input model for managing input state */1233readonly inputModel: IInputModel;1234readonly hasRequests: boolean;1235readonly lastRequest: IChatRequestModel | undefined;1236/** Whether this model will be kept alive while it is running or has edits */1237readonly willKeepAlive: boolean;1238readonly lastRequestObs: IObservable<IChatRequestModel | undefined>;1239getRequests(): IChatRequestModel[];1240setCheckpoint(requestId: string | undefined): void;12411242toExport(): IExportableChatData;1243toJSON(): ISerializableChatData;1244readonly contributedChatSession: IChatSessionContext | undefined;1245}12461247export interface ISerializableChatsData {1248[sessionId: string]: ISerializableChatData;1249}12501251export type ISerializableChatAgentData = UriDto<IChatAgentData>;12521253interface ISerializableChatResponseData {1254responseId?: string;1255result?: IChatAgentResult; // Optional for backcompat1256responseMarkdownInfo?: ISerializableMarkdownInfo[];1257followups?: ReadonlyArray<IChatFollowup>;1258modelState?: ResponseModelStateT;1259vote?: ChatAgentVoteDirection;1260voteDownReason?: ChatAgentVoteDownReason;1261timestamp?: number;1262slashCommand?: IChatAgentCommand;1263/** For backward compat: should be optional */1264usedContext?: IChatUsedContext;1265contentReferences?: ReadonlyArray<IChatContentReference>;1266codeCitations?: ReadonlyArray<IChatCodeCitation>;1267timeSpentWaiting?: number;1268}12691270export interface ISerializableChatRequestData extends ISerializableChatResponseData {1271requestId: string;1272message: string | IParsedChatRequest; // string => old format1273/** Is really like "prompt data". This is the message in the format in which the agent gets it + variable values. */1274variableData: IChatRequestVariableData;1275response: ReadonlyArray<IMarkdownString | IChatResponseProgressFileTreeData | IChatContentInlineReference | IChatAgentMarkdownContentWithVulnerability | IChatThinkingPart> | undefined;12761277/**Old, persisted name for shouldBeRemovedOnSend */1278isHidden?: boolean;1279shouldBeRemovedOnSend?: IChatRequestDisablement;1280agent?: ISerializableChatAgentData;1281workingSet?: UriComponents[];1282// responseErrorDetails: IChatResponseErrorDetails | undefined;1283/** @deprecated modelState is used instead now */1284isCanceled?: boolean;1285timestamp?: number;1286confirmation?: string;1287editedFileEvents?: IChatAgentEditedFileEvent[];1288modelId?: string;1289}12901291export interface ISerializableMarkdownInfo {1292readonly suggestionId: EditSuggestionId;1293}12941295export interface IExportableChatData {1296initialLocation: ChatAgentLocation | undefined;1297requests: ISerializableChatRequestData[];1298responderUsername: string;1299responderAvatarIconUri: ThemeIcon | UriComponents | undefined; // Keeping Uri name for backcompat1300}13011302/*1303NOTE: 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.1304*/13051306export interface ISerializableChatData1 extends IExportableChatData {1307sessionId: string;1308creationDate: number;13091310/** 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. */1311isNew?: boolean;1312}13131314export interface ISerializableChatData2 extends ISerializableChatData1 {1315version: 2;1316lastMessageDate: number;1317computedTitle: string | undefined;1318}13191320export interface ISerializableChatData3 extends Omit<ISerializableChatData2, 'version' | 'computedTitle'> {1321version: 3;1322customTitle: string | undefined;1323/**1324* Whether the session had pending edits when it was stored.1325* todo@connor4312 This will be cleaned up with the globalization of edits.1326*/1327hasPendingEdits?: boolean;1328/** Current draft input state (added later, fully backwards compatible) */1329inputState?: ISerializableChatModelInputState;1330}13311332/**1333* Input model for managing chat input state independently from the chat model.1334* This keeps display logic separated from the core chat model.1335*1336* The input model:1337* - Manages the current draft state (text, attachments, mode, model selection, cursor/selection)1338* - Provides an observable interface for reactive UI updates1339* - Automatically persists through the chat model's serialization1340* - Enables bidirectional sync between the UI (ChatInputPart) and the model1341* - Uses `undefined` state to indicate no persisted state (new/empty chat)1342*1343* This architecture ensures that:1344* - Input state is preserved when moving chats between editor/sidebar/window1345* - No manual state transfer is needed when switching contexts1346* - The UI stays in sync with the persisted state1347* - New chats use UI defaults (persisted preferences) instead of hardcoded values1348*/1349export interface IInputModel {1350/** Observable for current input state (undefined for new/uninitialized chats) */1351readonly state: IObservable<IChatModelInputState | undefined>;13521353/** Update the input state (partial update) */1354setState(state: Partial<IChatModelInputState>): void;13551356/** Clear input state (after sending or clearing) */1357clearState(): void;13581359/** Serializes the state */1360toJSON(): ISerializableChatModelInputState | undefined;1361}13621363/**1364* Represents the current state of the chat input that hasn't been sent yet.1365* This is the "draft" state that should be preserved across sessions.1366*/1367export interface IChatModelInputState {1368/** Current attachments in the input */1369attachments: readonly IChatRequestVariableEntry[];13701371/** Currently selected chat mode */1372mode: {1373/** Mode ID (e.g., 'ask', 'edit', 'agent', or custom mode ID) */1374id: string;1375/** Mode kind for builtin modes */1376kind: ChatModeKind | undefined;1377};13781379/** Currently selected language model, if any */1380selectedModel: ILanguageModelChatMetadataAndIdentifier | undefined;13811382/** Current input text */1383inputText: string;13841385/** Current selection ranges */1386selections: ISelection[];13871388/** Contributed stored state */1389contrib: Record<string, unknown>;1390}13911392/**1393* Serializable version of IChatModelInputState1394*/1395export interface ISerializableChatModelInputState {1396attachments: readonly IChatRequestVariableEntry[];1397mode: {1398id: string;1399kind: ChatModeKind | undefined;1400};1401selectedModel: {1402identifier: string;1403metadata: ILanguageModelChatMetadata;1404} | undefined;1405inputText: string;1406selections: ISelection[];1407contrib: Record<string, unknown>;1408}14091410/**1411* Chat data that has been parsed and normalized to the current format.1412*/1413export type ISerializableChatData = ISerializableChatData3;14141415/**1416* Chat data that has been loaded but not normalized, and could be any format1417*/1418export type ISerializableChatDataIn = ISerializableChatData1 | ISerializableChatData2 | ISerializableChatData3;14191420/**1421* Normalize chat data from storage to the current format.1422* TODO- ChatModel#_deserialize and reviveSerializedAgent also still do some normalization and maybe that should be done in here too.1423*/1424export function normalizeSerializableChatData(raw: ISerializableChatDataIn): ISerializableChatData {1425normalizeOldFields(raw);14261427if (!('version' in raw)) {1428return {1429version: 3,1430...raw,1431lastMessageDate: raw.creationDate,1432customTitle: undefined,1433};1434}14351436if (raw.version === 2) {1437return {1438...raw,1439version: 3,1440customTitle: raw.computedTitle1441};1442}14431444return raw;1445}14461447function normalizeOldFields(raw: ISerializableChatDataIn): void {1448// Fill in fields that very old chat data may be missing1449if (!raw.sessionId) {1450raw.sessionId = generateUuid();1451}14521453if (!raw.creationDate) {1454raw.creationDate = getLastYearDate();1455}14561457if ('version' in raw && (raw.version === 2 || raw.version === 3)) {1458if (!raw.lastMessageDate) {1459// A bug led to not porting creationDate properly, and that was copied to lastMessageDate, so fix that up if missing.1460raw.lastMessageDate = getLastYearDate();1461}1462}14631464// eslint-disable-next-line @typescript-eslint/no-explicit-any, local/code-no-any-casts1465if ((raw.initialLocation as any) === 'editing-session') {1466raw.initialLocation = ChatAgentLocation.Chat;1467}1468}14691470function getLastYearDate(): number {1471const lastYearDate = new Date();1472lastYearDate.setFullYear(lastYearDate.getFullYear() - 1);1473return lastYearDate.getTime();1474}14751476export function isExportableSessionData(obj: unknown): obj is IExportableChatData {1477return !!obj &&1478Array.isArray((obj as IExportableChatData).requests) &&1479typeof (obj as IExportableChatData).responderUsername === 'string';1480}14811482export function isSerializableSessionData(obj: unknown): obj is ISerializableChatData {1483const data = obj as ISerializableChatData;1484return isExportableSessionData(obj) &&1485typeof data.creationDate === 'number' &&1486typeof data.sessionId === 'string' &&1487obj.requests.every((request: ISerializableChatRequestData) =>1488!request.usedContext /* for backward compat allow missing usedContext */ || isIUsedContext(request.usedContext)1489);1490}14911492export type IChatChangeEvent =1493| IChatInitEvent1494| IChatAddRequestEvent | IChatChangedRequestEvent | IChatRemoveRequestEvent1495| IChatAddResponseEvent1496| IChatSetAgentEvent1497| IChatMoveEvent1498| IChatSetHiddenEvent1499| IChatCompletedRequestEvent1500| IChatSetCheckpointEvent1501| IChatSetCustomTitleEvent1502;15031504export interface IChatAddRequestEvent {1505kind: 'addRequest';1506request: IChatRequestModel;1507}15081509export interface IChatSetCheckpointEvent {1510kind: 'setCheckpoint';1511disabledRequestIds: Set<string>;1512disabledResponseIds: Set<string>;1513}15141515export interface IChatChangedRequestEvent {1516kind: 'changedRequest';1517request: IChatRequestModel;1518}15191520export interface IChatCompletedRequestEvent {1521kind: 'completedRequest';1522request: IChatRequestModel;1523}15241525export interface IChatAddResponseEvent {1526kind: 'addResponse';1527response: IChatResponseModel;1528}15291530export const enum ChatRequestRemovalReason {1531/**1532* "Normal" remove1533*/1534Removal,15351536/**1537* Removed because the request will be resent1538*/1539Resend,15401541/**1542* Remove because the request is moving to another model1543*/1544Adoption1545}15461547export interface IChatRemoveRequestEvent {1548kind: 'removeRequest';1549requestId: string;1550responseId?: string;1551reason: ChatRequestRemovalReason;1552}15531554export interface IChatSetHiddenEvent {1555kind: 'setHidden';1556}15571558export interface IChatMoveEvent {1559kind: 'move';1560target: URI;1561range: IRange;1562}15631564export interface IChatSetAgentEvent {1565kind: 'setAgent';1566agent: IChatAgentData;1567command?: IChatAgentCommand;1568}15691570export interface IChatSetCustomTitleEvent {1571kind: 'setCustomTitle';1572title: string;1573}15741575export interface IChatInitEvent {1576kind: 'initialize';1577}15781579/**1580* Internal implementation of IInputModel1581*/1582class InputModel implements IInputModel {1583private readonly _state: ReturnType<typeof observableValue<IChatModelInputState | undefined>>;1584readonly state: IObservable<IChatModelInputState | undefined>;15851586constructor(initialState: IChatModelInputState | undefined) {1587this._state = observableValueOpts({ debugName: 'inputModelState', equalsFn: equals }, initialState);1588this.state = this._state;1589}15901591setState(state: Partial<IChatModelInputState>): void {1592const current = this._state.get();1593this._state.set({1594// If current is undefined, provide defaults for required fields1595attachments: [],1596mode: { id: 'agent', kind: ChatModeKind.Agent },1597selectedModel: undefined,1598inputText: '',1599selections: [],1600contrib: {},1601...current,1602...state1603}, undefined);1604}16051606clearState(): void {1607this._state.set(undefined, undefined);1608}16091610toJSON(): ISerializableChatModelInputState | undefined {1611const value = this.state.get();1612if (!value) {1613return undefined;1614}16151616return {1617contrib: value.contrib,1618attachments: value.attachments,1619mode: value.mode,1620selectedModel: value.selectedModel ? {1621identifier: value.selectedModel.identifier,1622metadata: value.selectedModel.metadata1623} : undefined,1624inputText: value.inputText,1625selections: value.selections1626};1627}1628}16291630export class ChatModel extends Disposable implements IChatModel {1631static getDefaultTitle(requests: (ISerializableChatRequestData | IChatRequestModel)[]): string {1632const firstRequestMessage = requests.at(0)?.message ?? '';1633const message = typeof firstRequestMessage === 'string' ?1634firstRequestMessage :1635firstRequestMessage.text;1636return message.split('\n')[0].substring(0, 200);1637}16381639private readonly _onDidDispose = this._register(new Emitter<void>());1640readonly onDidDispose = this._onDidDispose.event;16411642private readonly _onDidChange = this._register(new Emitter<IChatChangeEvent>());1643readonly onDidChange = this._onDidChange.event;16441645private _requests: ChatRequestModel[];16461647private _contributedChatSession: IChatSessionContext | undefined;1648public get contributedChatSession(): IChatSessionContext | undefined {1649return this._contributedChatSession;1650}1651public setContributedChatSession(session: IChatSessionContext | undefined) {1652this._contributedChatSession = session;1653}1654readonly lastRequestObs: IObservable<IChatRequestModel | undefined>;16551656// TODO to be clear, this is not the same as the id from the session object, which belongs to the provider.1657// It's easier to be able to identify this model before its async initialization is complete1658private readonly _sessionId: string;1659/** @deprecated Use {@link sessionResource} instead */1660get sessionId(): string {1661return this._sessionId;1662}16631664private readonly _sessionResource: URI;1665get sessionResource(): URI {1666return this._sessionResource;1667}16681669readonly requestInProgress: IObservable<boolean>;1670readonly requestNeedsInput: IObservable<IChatRequestNeedsInputInfo | undefined>;16711672/** Input model for managing input state */1673readonly inputModel: InputModel;16741675get hasRequests(): boolean {1676return this._requests.length > 0;1677}16781679get lastRequest(): ChatRequestModel | undefined {1680return this._requests.at(-1);1681}16821683private _timestamp: number;1684get timestamp(): number {1685return this._timestamp;1686}16871688get timing(): IChatSessionTiming {1689const lastResponse = this._requests.at(-1)?.response;1690return {1691startTime: this._timestamp,1692endTime: lastResponse?.completedAt ?? lastResponse?.timestamp1693};1694}16951696private _lastMessageDate: number;1697get lastMessageDate(): number {1698return this._lastMessageDate;1699}17001701private get _defaultAgent() {1702return this.chatAgentService.getDefaultAgent(ChatAgentLocation.Chat, ChatModeKind.Ask);1703}17041705private readonly _initialResponderUsername: string | undefined;1706get responderUsername(): string {1707return this._defaultAgent?.fullName ??1708this._initialResponderUsername ?? '';1709}17101711private readonly _initialResponderAvatarIconUri: ThemeIcon | URI | undefined;1712get responderAvatarIcon(): ThemeIcon | URI | undefined {1713return this._defaultAgent?.metadata.themeIcon ??1714this._initialResponderAvatarIconUri;1715}17161717private _isImported = false;1718get isImported(): boolean {1719return this._isImported;1720}17211722private _customTitle: string | undefined;1723get customTitle(): string | undefined {1724return this._customTitle;1725}17261727get title(): string {1728return this._customTitle || ChatModel.getDefaultTitle(this._requests);1729}17301731get hasCustomTitle(): boolean {1732return this._customTitle !== undefined;1733}17341735private _editingSession: IChatEditingSession | undefined;17361737get editingSession(): IChatEditingSession | undefined {1738return this._editingSession;1739}17401741private readonly _initialLocation: ChatAgentLocation;1742get initialLocation(): ChatAgentLocation {1743return this._initialLocation;1744}17451746private readonly _canUseTools: boolean = true;1747get canUseTools(): boolean {1748return this._canUseTools;1749}17501751private _disableBackgroundKeepAlive: boolean;1752get willKeepAlive(): boolean {1753return !this._disableBackgroundKeepAlive;1754}17551756constructor(1757initialData: ISerializableChatData | IExportableChatData | undefined,1758initialModelProps: { initialLocation: ChatAgentLocation; canUseTools: boolean; inputState?: ISerializableChatModelInputState; resource?: URI; sessionId?: string; disableBackgroundKeepAlive?: boolean },1759@IConfigurationService private readonly configurationService: IConfigurationService,1760@ILogService private readonly logService: ILogService,1761@IChatAgentService private readonly chatAgentService: IChatAgentService,1762@IChatEditingService private readonly chatEditingService: IChatEditingService,1763@IChatService private readonly chatService: IChatService,1764) {1765super();17661767const isValidExportedData = isExportableSessionData(initialData);1768const isValidFullData = isValidExportedData && isSerializableSessionData(initialData);1769if (initialData && !isValidExportedData) {1770this.logService.warn(`ChatModel#constructor: Loaded malformed session data: ${JSON.stringify(initialData)}`);1771}17721773this._isImported = !!initialData && isValidExportedData && !isValidFullData;1774this._sessionId = (isValidFullData && initialData.sessionId) || initialModelProps.sessionId || generateUuid();1775this._sessionResource = initialModelProps.resource ?? LocalChatSessionUri.forSession(this._sessionId);1776this._disableBackgroundKeepAlive = initialModelProps.disableBackgroundKeepAlive ?? false;17771778this._requests = initialData ? this._deserialize(initialData) : [];1779this._timestamp = (isValidFullData && initialData.creationDate) || Date.now();1780this._lastMessageDate = (isValidFullData && initialData.lastMessageDate) || this._timestamp;1781this._customTitle = isValidFullData ? initialData.customTitle : undefined;17821783// Initialize input model from serialized data (undefined for new chats)1784const serializedInputState = initialModelProps.inputState || (isValidFullData && initialData.inputState ? initialData.inputState : undefined);1785this.inputModel = new InputModel(serializedInputState && {1786attachments: serializedInputState.attachments,1787mode: serializedInputState.mode,1788selectedModel: serializedInputState.selectedModel && {1789identifier: serializedInputState.selectedModel.identifier,1790metadata: serializedInputState.selectedModel.metadata1791},1792contrib: serializedInputState.contrib,1793inputText: serializedInputState.inputText,1794selections: serializedInputState.selections1795});17961797this._initialResponderUsername = initialData?.responderUsername;1798this._initialResponderAvatarIconUri = isUriComponents(initialData?.responderAvatarIconUri) ? URI.revive(initialData.responderAvatarIconUri) : initialData?.responderAvatarIconUri;17991800this._initialLocation = initialData?.initialLocation ?? initialModelProps.initialLocation;1801this._canUseTools = initialModelProps.canUseTools;18021803this.lastRequestObs = observableFromEvent(this, this.onDidChange, () => this._requests.at(-1));18041805this._register(autorun(reader => {1806const request = this.lastRequestObs.read(reader);1807if (!request?.response) {1808return;1809}18101811reader.store.add(request.response.onDidChange(async ev => {1812if (!this._editingSession || ev.reason !== 'completedRequest') {1813return;1814}18151816if (1817request === this._requests.at(-1) &&1818request.session.sessionResource.scheme !== Schemas.vscodeLocalChatSession &&1819this.configurationService.getValue<boolean>('chat.checkpoints.showFileChanges') === true &&1820this._editingSession.hasEditsInRequest(request.id)1821) {1822const diffs = this._editingSession.getDiffsForFilesInRequest(request.id);1823request.response?.updateContent(editEntriesToMultiDiffData(diffs), true);1824}18251826this._onDidChange.fire({ kind: 'completedRequest', request });1827}));1828}));18291830this.requestInProgress = this.lastRequestObs.map((request, r) => {1831return request?.response?.isInProgress.read(r) ?? false;1832});18331834this.requestNeedsInput = this.lastRequestObs.map((request, r) => {1835const pendingInfo = request?.response?.isPendingConfirmation.read(r);1836if (!pendingInfo) {1837return undefined;1838}1839return {1840title: this.title,1841detail: pendingInfo.detail,1842};1843});18441845// Retain a reference to itself when a request is in progress, so the ChatModel stays alive in the background1846// only while running a request. TODO also keep it alive for 5min or so so we don't have to dispose/restore too often?1847if (this.initialLocation === ChatAgentLocation.Chat && !initialModelProps.disableBackgroundKeepAlive) {1848const selfRef = this._register(new MutableDisposable<IChatModelReference>());1849this._register(autorun(r => {1850const inProgress = this.requestInProgress.read(r);1851const needsInput = this.requestNeedsInput.read(r);1852const shouldStayAlive = inProgress || !!needsInput;1853if (shouldStayAlive && !selfRef.value) {1854selfRef.value = chatService.getActiveSessionReference(this._sessionResource);1855} else if (!shouldStayAlive && selfRef.value) {1856selfRef.clear();1857}1858}));1859}1860}18611862startEditingSession(isGlobalEditingSession?: boolean, transferFromSession?: IChatEditingSession): void {1863const session = this._editingSession ??= this._register(1864transferFromSession1865? this.chatEditingService.transferEditingSession(this, transferFromSession)1866: isGlobalEditingSession1867? this.chatEditingService.startOrContinueGlobalEditingSession(this)1868: this.chatEditingService.createEditingSession(this)1869);18701871if (!this._disableBackgroundKeepAlive) {1872// todo@connor4312: hold onto a reference so background sessions don't1873// trigger early disposal. This will be cleaned up with the globalization of edits.1874const selfRef = this._register(new MutableDisposable<IChatModelReference>());1875this._register(autorun(r => {1876const hasModified = session.entries.read(r).some(e => e.state.read(r) === ModifiedFileEntryState.Modified);1877if (hasModified && !selfRef.value) {1878selfRef.value = this.chatService.getActiveSessionReference(this._sessionResource);1879} else if (!hasModified && selfRef.value) {1880selfRef.clear();1881}1882}));1883}18841885this._register(autorun(reader => {1886this._setDisabledRequests(session.requestDisablement.read(reader));1887}));1888}18891890private currentEditedFileEvents = new ResourceMap<IChatAgentEditedFileEvent>();1891notifyEditingAction(action: IChatEditingSessionAction): void {1892const state = action.outcome === 'accepted' ? ChatRequestEditedFileEventKind.Keep :1893action.outcome === 'rejected' ? ChatRequestEditedFileEventKind.Undo :1894action.outcome === 'userModified' ? ChatRequestEditedFileEventKind.UserModification : null;1895if (state === null) {1896return;1897}18981899if (!this.currentEditedFileEvents.has(action.uri) || this.currentEditedFileEvents.get(action.uri)?.eventKind === ChatRequestEditedFileEventKind.Keep) {1900this.currentEditedFileEvents.set(action.uri, { eventKind: state, uri: action.uri });1901}1902}19031904private _deserialize(obj: IExportableChatData | ISerializableChatData): ChatRequestModel[] {1905const requests = obj.requests;1906if (!Array.isArray(requests)) {1907this.logService.error(`Ignoring malformed session data: ${JSON.stringify(obj)}`);1908return [];1909}19101911try {1912return requests.map((raw: ISerializableChatRequestData) => {1913const parsedRequest =1914typeof raw.message === 'string'1915? this.getParsedRequestFromString(raw.message)1916: reviveParsedChatRequest(raw.message);19171918// Old messages don't have variableData, or have it in the wrong (non-array) shape1919const variableData: IChatRequestVariableData = this.reviveVariableData(raw.variableData);1920const request = new ChatRequestModel({1921session: this,1922message: parsedRequest,1923variableData,1924timestamp: raw.timestamp ?? -1,1925restoredId: raw.requestId,1926confirmation: raw.confirmation,1927editedFileEvents: raw.editedFileEvents,1928modelId: raw.modelId,1929});1930request.shouldBeRemovedOnSend = raw.isHidden ? { requestId: raw.requestId } : raw.shouldBeRemovedOnSend;1931// eslint-disable-next-line @typescript-eslint/no-explicit-any, local/code-no-any-casts1932if (raw.response || raw.result || (raw as any).responseErrorDetails) {1933const agent = (raw.agent && 'metadata' in raw.agent) ? // Check for the new format, ignore entries in the old format1934reviveSerializedAgent(raw.agent) : undefined;19351936// Port entries from old format1937const result = 'responseErrorDetails' in raw ?1938// eslint-disable-next-line local/code-no-dangerous-type-assertions1939{ errorDetails: raw.responseErrorDetails } as IChatAgentResult : raw.result;1940request.response = new ChatResponseModel({1941responseContent: raw.response ?? [new MarkdownString(raw.response)],1942session: this,1943agent,1944slashCommand: raw.slashCommand,1945requestId: request.id,1946modelState: raw.modelState || { value: raw.isCanceled ? ResponseModelState.Cancelled : ResponseModelState.Complete, completedAt: 'lastMessageDate' in obj ? obj.lastMessageDate : Date.now() },1947vote: raw.vote,1948timestamp: raw.timestamp,1949voteDownReason: raw.voteDownReason,1950result,1951followups: raw.followups,1952restoredId: raw.responseId,1953timeSpentWaiting: raw.timeSpentWaiting,1954shouldBeBlocked: request.shouldBeBlocked.get(),1955codeBlockInfos: raw.responseMarkdownInfo?.map<ICodeBlockInfo>(info => ({ suggestionId: info.suggestionId })),1956});1957request.response.shouldBeRemovedOnSend = raw.isHidden ? { requestId: raw.requestId } : raw.shouldBeRemovedOnSend;1958if (raw.usedContext) { // @ulugbekna: if this's a new vscode sessions, doc versions are incorrect anyway?1959request.response.applyReference(revive(raw.usedContext));1960}19611962raw.contentReferences?.forEach(r => request.response!.applyReference(revive(r)));1963raw.codeCitations?.forEach(c => request.response!.applyCodeCitation(revive(c)));1964}1965return request;1966});1967} catch (error) {1968this.logService.error('Failed to parse chat data', error);1969return [];1970}1971}19721973private reviveVariableData(raw: IChatRequestVariableData): IChatRequestVariableData {1974const variableData = raw && Array.isArray(raw.variables)1975? raw :1976{ variables: [] };19771978variableData.variables = variableData.variables.map<IChatRequestVariableEntry>(IChatRequestVariableEntry.fromExport);19791980return variableData;1981}19821983private getParsedRequestFromString(message: string): IParsedChatRequest {1984// TODO These offsets won't be used, but chat replies need to go through the parser as well1985const parts = [new ChatRequestTextPart(new OffsetRange(0, message.length), { startColumn: 1, startLineNumber: 1, endColumn: 1, endLineNumber: 1 }, message)];1986return {1987text: message,1988parts1989};1990}1991199219931994getRequests(): ChatRequestModel[] {1995return this._requests;1996}19971998resetCheckpoint(): void {1999for (const request of this._requests) {2000request.setShouldBeBlocked(false);2001}2002}20032004setCheckpoint(requestId: string | undefined) {2005let checkpoint: ChatRequestModel | undefined;2006let checkpointIndex = -1;2007if (requestId !== undefined) {2008this._requests.forEach((request, index) => {2009if (request.id === requestId) {2010checkpointIndex = index;2011checkpoint = request;2012request.setShouldBeBlocked(true);2013}2014});20152016if (!checkpoint) {2017return; // Invalid request ID2018}2019}20202021const disabledRequestIds = new Set<string>();2022const disabledResponseIds = new Set<string>();2023for (let i = this._requests.length - 1; i >= 0; i -= 1) {2024const request = this._requests[i];2025if (this._checkpoint && !checkpoint) {2026request.setShouldBeBlocked(false);2027} else if (checkpoint && i >= checkpointIndex) {2028request.setShouldBeBlocked(true);2029disabledRequestIds.add(request.id);2030if (request.response) {2031disabledResponseIds.add(request.response.id);2032}2033} else if (checkpoint && i < checkpointIndex) {2034request.setShouldBeBlocked(false);2035}2036}20372038this._checkpoint = checkpoint;2039this._onDidChange.fire({2040kind: 'setCheckpoint',2041disabledRequestIds,2042disabledResponseIds2043});2044}20452046private _checkpoint: ChatRequestModel | undefined = undefined;2047public get checkpoint() {2048return this._checkpoint;2049}20502051private _setDisabledRequests(requestIds: IChatRequestDisablement[]) {2052this._requests.forEach((request) => {2053const shouldBeRemovedOnSend = requestIds.find(r => r.requestId === request.id);2054request.shouldBeRemovedOnSend = shouldBeRemovedOnSend;2055if (request.response) {2056request.response.shouldBeRemovedOnSend = shouldBeRemovedOnSend;2057}2058});20592060this._onDidChange.fire({ kind: 'setHidden' });2061}20622063addRequest(message: IParsedChatRequest, variableData: IChatRequestVariableData, attempt: number, modeInfo?: IChatRequestModeInfo, chatAgent?: IChatAgentData, slashCommand?: IChatAgentCommand, confirmation?: string, locationData?: IChatLocationData, attachments?: IChatRequestVariableEntry[], isCompleteAddedRequest?: boolean, modelId?: string, userSelectedTools?: UserSelectedTools, id?: string): ChatRequestModel {2064const editedFileEvents = [...this.currentEditedFileEvents.values()];2065this.currentEditedFileEvents.clear();2066const request = new ChatRequestModel({2067restoredId: id,2068session: this,2069message,2070variableData,2071timestamp: Date.now(),2072attempt,2073modeInfo,2074confirmation,2075locationData,2076attachedContext: attachments,2077isCompleteAddedRequest,2078modelId,2079editedFileEvents: editedFileEvents.length ? editedFileEvents : undefined,2080userSelectedTools,2081});2082request.response = new ChatResponseModel({2083responseContent: [],2084session: this,2085agent: chatAgent,2086slashCommand,2087requestId: request.id,2088isCompleteAddedRequest,2089codeBlockInfos: undefined,2090});20912092this._requests.push(request);2093this._lastMessageDate = Date.now();2094this._onDidChange.fire({ kind: 'addRequest', request });2095return request;2096}20972098public setCustomTitle(title: string): void {2099this._customTitle = title;2100this._onDidChange.fire({ kind: 'setCustomTitle', title });2101}21022103updateRequest(request: ChatRequestModel, variableData: IChatRequestVariableData) {2104request.variableData = variableData;2105this._onDidChange.fire({ kind: 'changedRequest', request });2106}21072108adoptRequest(request: ChatRequestModel): void {2109// this doesn't use `removeRequest` because it must not dispose the request object2110const oldOwner = request.session;2111const index = oldOwner._requests.findIndex((candidate: ChatRequestModel) => candidate.id === request.id);21122113if (index === -1) {2114return;2115}21162117oldOwner._requests.splice(index, 1);21182119request.adoptTo(this);2120request.response?.adoptTo(this);2121this._requests.push(request);21222123oldOwner._onDidChange.fire({ kind: 'removeRequest', requestId: request.id, responseId: request.response?.id, reason: ChatRequestRemovalReason.Adoption });2124this._onDidChange.fire({ kind: 'addRequest', request });2125}21262127acceptResponseProgress(request: ChatRequestModel, progress: IChatProgress, quiet?: boolean): void {2128if (!request.response) {2129request.response = new ChatResponseModel({2130responseContent: [],2131session: this,2132requestId: request.id,2133codeBlockInfos: undefined,2134});2135}21362137if (request.response.isComplete) {2138throw new Error('acceptResponseProgress: Adding progress to a completed response');2139}21402141if (progress.kind === 'usedContext' || progress.kind === 'reference') {2142request.response.applyReference(progress);2143} else if (progress.kind === 'codeCitation') {2144request.response.applyCodeCitation(progress);2145} else if (progress.kind === 'move') {2146this._onDidChange.fire({ kind: 'move', target: progress.uri, range: progress.range });2147} else if (progress.kind === 'codeblockUri' && progress.isEdit) {2148request.response.addUndoStop({ id: progress.undoStopId ?? generateUuid(), kind: 'undoStop' });2149request.response.updateContent(progress, quiet);2150} else if (progress.kind === 'progressTaskResult') {2151// Should have been handled upstream, not sent to model2152this.logService.error(`Couldn't handle progress: ${JSON.stringify(progress)}`);2153} else {2154request.response.updateContent(progress, quiet);2155}2156}21572158removeRequest(id: string, reason: ChatRequestRemovalReason = ChatRequestRemovalReason.Removal): void {2159const index = this._requests.findIndex(request => request.id === id);2160const request = this._requests[index];21612162if (index !== -1) {2163this._onDidChange.fire({ kind: 'removeRequest', requestId: request.id, responseId: request.response?.id, reason });2164this._requests.splice(index, 1);2165request.response?.dispose();2166}2167}21682169cancelRequest(request: ChatRequestModel): void {2170if (request.response) {2171request.response.cancel();2172}2173}21742175setResponse(request: ChatRequestModel, result: IChatAgentResult): void {2176if (!request.response) {2177request.response = new ChatResponseModel({2178responseContent: [],2179session: this,2180requestId: request.id,2181codeBlockInfos: undefined,2182});2183}21842185request.response.setResult(result);2186}21872188setFollowups(request: ChatRequestModel, followups: IChatFollowup[] | undefined): void {2189if (!request.response) {2190// Maybe something went wrong?2191return;2192}2193request.response.setFollowups(followups);2194}21952196setResponseModel(request: ChatRequestModel, response: ChatResponseModel): void {2197request.response = response;2198this._onDidChange.fire({ kind: 'addResponse', response });2199}22002201toExport(): IExportableChatData {2202return {2203responderUsername: this.responderUsername,2204responderAvatarIconUri: this.responderAvatarIcon,2205initialLocation: this.initialLocation,2206requests: this._requests.map((r): ISerializableChatRequestData => {2207const message = {2208...r.message,2209// eslint-disable-next-line @typescript-eslint/no-explicit-any2210parts: r.message.parts.map((p: any) => p && 'toJSON' in p ? (p.toJSON as Function)() : p)2211};2212const agent = r.response?.agent;2213const agentJson = agent && 'toJSON' in agent ? (agent.toJSON as Function)() :2214agent ? { ...agent } : undefined;2215return {2216requestId: r.id,2217message,2218variableData: IChatRequestVariableData.toExport(r.variableData),2219response: r.response ?2220r.response.entireResponse.value.map(item => {2221// Keeping the shape of the persisted data the same for back compat2222if (item.kind === 'treeData') {2223return item.treeData;2224} else if (item.kind === 'markdownContent') {2225return item.content;2226} else {2227// eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any2228return item as any; // TODO2229}2230})2231: undefined,2232shouldBeRemovedOnSend: r.shouldBeRemovedOnSend,2233agent: agentJson,2234timestamp: r.timestamp,2235confirmation: r.confirmation,2236editedFileEvents: r.editedFileEvents,2237modelId: r.modelId,2238...r.response?.toJSON(),2239};2240}),2241};2242}22432244toJSON(): ISerializableChatData {2245return {2246version: 3,2247...this.toExport(),2248sessionId: this.sessionId,2249creationDate: this._timestamp,2250lastMessageDate: this._lastMessageDate,2251customTitle: this._customTitle,2252hasPendingEdits: !!(this._editingSession?.entries.get().some(e => e.state.get() === ModifiedFileEntryState.Modified)),2253inputState: this.inputModel.toJSON(),2254};2255}22562257override dispose() {2258this._requests.forEach(r => r.response?.dispose());2259this._onDidDispose.fire();22602261super.dispose();2262}2263}22642265export function updateRanges(variableData: IChatRequestVariableData, diff: number): IChatRequestVariableData {2266return {2267variables: variableData.variables.map(v => ({2268...v,2269range: v.range && {2270start: v.range.start - diff,2271endExclusive: v.range.endExclusive - diff2272}2273}))2274};2275}22762277export function canMergeMarkdownStrings(md1: IMarkdownString, md2: IMarkdownString): boolean {2278if (md1.baseUri && md2.baseUri) {2279const baseUriEquals = md1.baseUri.scheme === md2.baseUri.scheme2280&& md1.baseUri.authority === md2.baseUri.authority2281&& md1.baseUri.path === md2.baseUri.path2282&& md1.baseUri.query === md2.baseUri.query2283&& md1.baseUri.fragment === md2.baseUri.fragment;2284if (!baseUriEquals) {2285return false;2286}2287} else if (md1.baseUri || md2.baseUri) {2288return false;2289}22902291return equals(md1.isTrusted, md2.isTrusted) &&2292md1.supportHtml === md2.supportHtml &&2293md1.supportThemeIcons === md2.supportThemeIcons;2294}22952296export function appendMarkdownString(md1: IMarkdownString, md2: IMarkdownString | string): IMarkdownString {2297const appendedValue = typeof md2 === 'string' ? md2 : md2.value;2298return {2299value: md1.value + appendedValue,2300isTrusted: md1.isTrusted,2301supportThemeIcons: md1.supportThemeIcons,2302supportHtml: md1.supportHtml,2303baseUri: md1.baseUri2304};2305}23062307export function getCodeCitationsMessage(citations: ReadonlyArray<IChatCodeCitation>): string {2308if (citations.length === 0) {2309return '';2310}23112312const licenseTypes = citations.reduce((set, c) => set.add(c.license), new Set<string>());2313const label = licenseTypes.size === 1 ?2314localize('codeCitation', "Similar code found with 1 license type", licenseTypes.size) :2315localize('codeCitations', "Similar code found with {0} license types", licenseTypes.size);2316return label;2317}23182319export enum ChatRequestEditedFileEventKind {2320Keep = 1,2321Undo = 2,2322UserModification = 3,2323}23242325export interface IChatAgentEditedFileEvent {2326readonly uri: URI;2327readonly eventKind: ChatRequestEditedFileEventKind;2328}23292330/** URI for a resource embedded in a chat request/response */2331export namespace ChatResponseResource {2332export const scheme = 'vscode-chat-response-resource';23332334export function createUri(sessionResource: URI, toolCallId: string, index: number, basename?: string): URI {2335return URI.from({2336scheme: ChatResponseResource.scheme,2337authority: encodeHex(VSBuffer.fromString(sessionResource.toString())),2338path: `/tool/${toolCallId}/${index}` + (basename ? `/${basename}` : ''),2339});2340}23412342export function parseUri(uri: URI): undefined | { sessionResource: URI; toolCallId: string; index: number } {2343if (uri.scheme !== ChatResponseResource.scheme) {2344return undefined;2345}23462347const parts = uri.path.split('/');2348if (parts.length < 5) {2349return undefined;2350}23512352const [, kind, toolCallId, index] = parts;2353if (kind !== 'tool') {2354return undefined;2355}23562357let sessionResource: URI;2358try {2359sessionResource = URI.parse(decodeHex(uri.authority).toString());2360} catch (e) {2361if (e instanceof SyntaxError) { // pre-1.108 local session ID2362sessionResource = LocalChatSessionUri.forSession(uri.authority);2363} else {2364throw e;2365}2366}23672368return {2369sessionResource,2370toolCallId: toolCallId,2371index: Number(index),2372};2373}2374}237523762377