Path: blob/main/src/vs/workbench/contrib/chat/common/model/chatModel.ts
5251 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, constObservable, derived, observableFromEvent, observableSignalFromEvent, observableValue, observableValueOpts } from '../../../../../base/common/observable.js';17import { basename, isEqual } from '../../../../../base/common/resources.js';18import { hasKey, WithDefinedProps } from '../../../../../base/common/types.js';19import { URI, UriDto } from '../../../../../base/common/uri.js';20import { generateUuid } from '../../../../../base/common/uuid.js';21import { IRange } from '../../../../../editor/common/core/range.js';22import { OffsetRange } from '../../../../../editor/common/core/ranges/offsetRange.js';23import { ISelection } from '../../../../../editor/common/core/selection.js';24import { TextEdit } from '../../../../../editor/common/languages.js';25import { EditSuggestionId } from '../../../../../editor/common/textModelEditSource.js';26import { localize } from '../../../../../nls.js';27import { ILogService } from '../../../../../platform/log/common/log.js';28import { CellUri, ICellEditOperation } from '../../../notebook/common/notebookCommon.js';29import { ChatRequestToolReferenceEntry, IChatRequestVariableEntry, isImplicitVariableEntry, isStringImplicitContextValue, isStringVariableEntry } from '../attachments/chatVariableEntries.js';30import { migrateLegacyTerminalToolSpecificData } from '../chat.js';31import { ChatAgentVoteDirection, ChatAgentVoteDownReason, ChatRequestQueueKind, ChatResponseClearToPreviousToolInvocationReason, ElicitationState, IChatAgentMarkdownContentWithVulnerability, IChatClearToPreviousToolInvocation, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatEditingSessionAction, IChatElicitationRequest, IChatElicitationRequestSerialized, IChatExternalToolInvocationUpdate, IChatExtensionsContent, IChatFollowup, IChatHookPart, IChatLocationData, IChatMarkdownContent, IChatMcpServersStarting, IChatMcpServersStartingSerialized, IChatModelReference, IChatMultiDiffData, IChatMultiDiffDataSerialized, IChatNotebookEdit, IChatProgress, IChatProgressMessage, IChatPullRequestContent, IChatQuestionCarousel, IChatResponseCodeblockUriPart, IChatResponseProgressFileTreeData, IChatSendRequestOptions, IChatService, IChatSessionContext, IChatSessionTiming, IChatTask, IChatTaskSerialized, IChatTextEdit, IChatThinkingPart, IChatToolInvocation, IChatToolInvocationSerialized, IChatTreeData, IChatUndoStop, IChatUsage, IChatUsedContext, IChatWarningMessage, IChatWorkspaceEdit, ResponseModelState, isIUsedContext } from '../chatService/chatService.js';32import { ChatAgentLocation, ChatModeKind } from '../constants.js';33import { ChatToolInvocation } from './chatProgressTypes/chatToolInvocation.js';34import { ToolDataSource, IToolData } from '../tools/languageModelToolsService.js';35import { IChatEditingService, IChatEditingSession, ModifiedFileEntryState } 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';40import { ObjectMutationLog } from './objectMutationLog.js';414243/**44* Represents a queued chat request waiting to be processed.45*/46export interface IChatPendingRequest {47readonly request: IChatRequestModel;48readonly kind: ChatRequestQueueKind;49/**50* The options that were passed to sendRequest when this request was queued.51* userSelectedTools is snapshotted to a static observable at queue time.52*/53readonly sendOptions: IChatSendRequestOptions;54}5556/**57* Serializable version of IChatSendRequestOptions for pending requests.58* Excludes observables and non-serializable fields.59*/60export interface ISerializableSendOptions {61modeInfo?: IChatRequestModeInfo;62userSelectedModelId?: string;63/** Static snapshot of user-selected tools (not an observable) */64userSelectedTools?: UserSelectedTools;65location?: ChatAgentLocation;66locationData?: IChatLocationData;67attempt?: number;68noCommandDetection?: boolean;69agentId?: string;70agentIdSilent?: string;71slashCommand?: string;72confirmation?: string;73}7475/**76* Serializable representation of a pending chat request.77*/78export interface ISerializablePendingRequestData {79id: string;80request: ISerializableChatRequestData;81kind: ChatRequestQueueKind;82sendOptions: ISerializableSendOptions;83}8485export const CHAT_ATTACHABLE_IMAGE_MIME_TYPES: Record<string, string> = {86png: 'image/png',87jpg: 'image/jpeg',88jpeg: 'image/jpeg',89gif: 'image/gif',90webp: 'image/webp',91};9293export function getAttachableImageExtension(mimeType: string): string | undefined {94return Object.entries(CHAT_ATTACHABLE_IMAGE_MIME_TYPES).find(([_, value]) => value === mimeType)?.[0];95}9697export interface IChatRequestVariableData {98variables: readonly IChatRequestVariableEntry[];99}100101export namespace IChatRequestVariableData {102export function toExport(data: IChatRequestVariableData): IChatRequestVariableData {103return { variables: data.variables.map(IChatRequestVariableEntry.toExport) };104}105}106107export interface IChatRequestModel {108readonly id: string;109readonly timestamp: number;110readonly version: number;111readonly modeInfo?: IChatRequestModeInfo;112readonly session: IChatModel;113readonly message: IParsedChatRequest;114readonly attempt: number;115readonly variableData: IChatRequestVariableData;116readonly confirmation?: string;117readonly locationData?: IChatLocationData;118readonly attachedContext?: IChatRequestVariableEntry[];119readonly isCompleteAddedRequest: boolean;120readonly response?: IChatResponseModel;121readonly editedFileEvents?: IChatAgentEditedFileEvent[];122shouldBeRemovedOnSend: IChatRequestDisablement | undefined;123readonly shouldBeBlocked: IObservable<boolean>;124setShouldBeBlocked(value: boolean): void;125readonly modelId?: string;126readonly userSelectedTools?: UserSelectedTools;127}128129export interface ICodeBlockInfo {130readonly suggestionId: EditSuggestionId;131}132133export interface IChatTextEditGroupState {134sha1: string;135applied: number;136}137138export interface IChatTextEditGroup {139uri: URI;140edits: TextEdit[][];141state?: IChatTextEditGroupState;142kind: 'textEditGroup';143done: boolean | undefined;144isExternalEdit?: boolean;145}146147export function isCellTextEditOperation(value: unknown): value is ICellTextEditOperation {148const candidate = value as ICellTextEditOperation;149return !!candidate && !!candidate.edit && !!candidate.uri && URI.isUri(candidate.uri);150}151152export function isCellTextEditOperationArray(value: ICellTextEditOperation[] | ICellEditOperation[]): value is ICellTextEditOperation[] {153return value.some(isCellTextEditOperation);154}155156export interface ICellTextEditOperation {157edit: TextEdit;158uri: URI;159}160161export interface IChatNotebookEditGroup {162uri: URI;163edits: (ICellTextEditOperation[] | ICellEditOperation[])[];164state?: IChatTextEditGroupState;165kind: 'notebookEditGroup';166done: boolean | undefined;167isExternalEdit?: boolean;168}169170/**171* Progress kinds that are included in the history of a response.172* Excludes "internal" types that are included in history.173*/174export type IChatProgressHistoryResponseContent =175| IChatMarkdownContent176| IChatAgentMarkdownContentWithVulnerability177| IChatResponseCodeblockUriPart178| IChatTreeData179| IChatMultiDiffDataSerialized180| IChatContentInlineReference181| IChatProgressMessage182| IChatCommandButton183| IChatWarningMessage184| IChatTask185| IChatTaskSerialized186| IChatTextEditGroup187| IChatNotebookEditGroup188| IChatConfirmation189| IChatQuestionCarousel190| IChatExtensionsContent191| IChatThinkingPart192| IChatHookPart193| IChatPullRequestContent194| IChatWorkspaceEdit;195196/**197* "Normal" progress kinds that are rendered as parts of the stream of content.198*/199export type IChatProgressResponseContent =200| IChatProgressHistoryResponseContent201| IChatToolInvocation202| IChatToolInvocationSerialized203| IChatMultiDiffData204| IChatUndoStop205| IChatElicitationRequest206| IChatElicitationRequestSerialized207| IChatClearToPreviousToolInvocation208| IChatMcpServersStarting209| IChatMcpServersStartingSerialized;210211export type IChatProgressResponseContentSerialized = Exclude<IChatProgressResponseContent,212| IChatToolInvocation213| IChatElicitationRequest214| IChatTask215| IChatMultiDiffData216| IChatMcpServersStarting217>;218219const nonHistoryKinds = new Set(['toolInvocation', 'toolInvocationSerialized', 'undoStop']);220function isChatProgressHistoryResponseContent(content: IChatProgressResponseContent): content is IChatProgressHistoryResponseContent {221return !nonHistoryKinds.has(content.kind);222}223224export function toChatHistoryContent(content: ReadonlyArray<IChatProgressResponseContent>): IChatProgressHistoryResponseContent[] {225return content.filter(isChatProgressHistoryResponseContent);226}227228export type IChatProgressRenderableResponseContent = Exclude<IChatProgressResponseContent, IChatContentInlineReference | IChatAgentMarkdownContentWithVulnerability | IChatResponseCodeblockUriPart>;229230export interface IResponse {231readonly value: ReadonlyArray<IChatProgressResponseContent>;232getMarkdown(): string;233toString(): string;234}235236export interface IChatResponseModel {237readonly onDidChange: Event<ChatResponseModelChangeReason>;238readonly id: string;239readonly requestId: string;240readonly request: IChatRequestModel | undefined;241readonly username: string;242readonly session: IChatModel;243readonly agent?: IChatAgentData;244readonly usedContext: IChatUsedContext | undefined;245readonly contentReferences: ReadonlyArray<IChatContentReference>;246readonly codeCitations: ReadonlyArray<IChatCodeCitation>;247readonly progressMessages: ReadonlyArray<IChatProgressMessage>;248readonly slashCommand?: IChatAgentCommand;249readonly agentOrSlashCommandDetected: boolean;250/** View of the response shown to the user, may have parts omitted from undo stops. */251readonly response: IResponse;252/** Entire response from the model. */253readonly entireResponse: IResponse;254/** Milliseconds timestamp when this chat response was created. */255readonly timestamp: number;256/** Milliseconds timestamp when this chat response was completed or cancelled. */257readonly completedAt?: number;258/** The state of this response */259readonly state: ResponseModelState;260/** @internal */261readonly stateT: ResponseModelStateT;262/**263* Adjusted millisecond timestamp that excludes the duration during which264* the model was pending user confirmation. `Date.now() - confirmationAdjustedTimestamp`265* will return the amount of time the response was busy generating content.266* This is updated only when `isPendingConfirmation` changes state.267*/268readonly confirmationAdjustedTimestamp: IObservable<number>;269readonly isComplete: boolean;270readonly isCanceled: boolean;271readonly isPendingConfirmation: IObservable<{ startedWaitingAt: number; detail?: string } | undefined>;272readonly isInProgress: IObservable<boolean>;273readonly shouldBeRemovedOnSend: IChatRequestDisablement | undefined;274readonly shouldBeBlocked: IObservable<boolean>;275readonly isCompleteAddedRequest: boolean;276/** 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. */277readonly isStale: boolean;278readonly vote: ChatAgentVoteDirection | undefined;279readonly voteDownReason: ChatAgentVoteDownReason | undefined;280readonly followups?: IChatFollowup[] | undefined;281readonly result?: IChatAgentResult;282readonly usage?: IChatUsage;283readonly codeBlockInfos: ICodeBlockInfo[] | undefined;284285initializeCodeBlockInfos(codeBlockInfo: ICodeBlockInfo[]): void;286addUndoStop(undoStop: IChatUndoStop): void;287setVote(vote: ChatAgentVoteDirection): void;288setVoteDownReason(reason: ChatAgentVoteDownReason | undefined): void;289setUsage(usage: IChatUsage): void;290setEditApplied(edit: IChatTextEditGroup, editCount: number): boolean;291updateContent(progress: IChatProgressResponseContent | IChatTextEdit | IChatNotebookEdit | IChatTask | IChatExternalToolInvocationUpdate, quiet?: boolean): void;292/**293* Adopts any partially-undo {@link response} as the {@link entireResponse}.294* Only valid when {@link isComplete}. This is needed because otherwise an295* undone and then diverged state would start showing old data because the296* undo stops would no longer exist in the model.297*/298finalizeUndoState(): void;299}300301export type ChatResponseModelChangeReason =302| { reason: 'other' }303| { reason: 'completedRequest' }304| { reason: 'undoStop'; id: string };305306export const defaultChatResponseModelChangeReason: ChatResponseModelChangeReason = { reason: 'other' };307308export interface IChatRequestModeInfo {309kind: ChatModeKind | undefined; // is undefined in case of modeId == 'apply'310isBuiltin: boolean;311modeInstructions: IChatRequestModeInstructions | undefined;312modeId: 'ask' | 'agent' | 'edit' | 'custom' | 'applyCodeBlock' | undefined;313applyCodeBlockSuggestionId: EditSuggestionId | undefined;314}315316export interface IChatRequestModeInstructions {317readonly name: string;318readonly content: string;319readonly toolReferences: readonly ChatRequestToolReferenceEntry[];320readonly metadata?: Record<string, boolean | string | number>;321}322323export interface IChatRequestModelParameters {324session: ChatModel;325message: IParsedChatRequest;326variableData: IChatRequestVariableData;327timestamp: number;328attempt?: number;329modeInfo?: IChatRequestModeInfo;330confirmation?: string;331locationData?: IChatLocationData;332attachedContext?: IChatRequestVariableEntry[];333isCompleteAddedRequest?: boolean;334modelId?: string;335restoredId?: string;336editedFileEvents?: IChatAgentEditedFileEvent[];337userSelectedTools?: UserSelectedTools;338}339340export class ChatRequestModel implements IChatRequestModel {341public readonly id: string;342public response: ChatResponseModel | undefined;343public shouldBeRemovedOnSend: IChatRequestDisablement | undefined;344public readonly timestamp: number;345public readonly message: IParsedChatRequest;346public readonly isCompleteAddedRequest: boolean;347public readonly modelId?: string;348public readonly modeInfo?: IChatRequestModeInfo;349public readonly userSelectedTools?: UserSelectedTools;350351private readonly _shouldBeBlocked = observableValue<boolean>(this, false);352public get shouldBeBlocked(): IObservable<boolean> {353return this._shouldBeBlocked;354}355356public setShouldBeBlocked(value: boolean): void {357this._shouldBeBlocked.set(value, undefined);358}359360private _session: ChatModel;361private readonly _attempt: number;362private _variableData: IChatRequestVariableData;363private readonly _confirmation?: string;364private readonly _locationData?: IChatLocationData;365private readonly _attachedContext?: IChatRequestVariableEntry[];366private readonly _editedFileEvents?: IChatAgentEditedFileEvent[];367368public get session(): ChatModel {369return this._session;370}371372public get attempt(): number {373return this._attempt;374}375376public get variableData(): IChatRequestVariableData {377return this._variableData;378}379380public set variableData(v: IChatRequestVariableData) {381this._version++;382this._variableData = v;383}384385public get confirmation(): string | undefined {386return this._confirmation;387}388389public get locationData(): IChatLocationData | undefined {390return this._locationData;391}392393public get attachedContext(): IChatRequestVariableEntry[] | undefined {394return this._attachedContext;395}396397public get editedFileEvents(): IChatAgentEditedFileEvent[] | undefined {398return this._editedFileEvents;399}400401private _version = 0;402public get version(): number {403return this._version;404}405406constructor(params: IChatRequestModelParameters) {407this._session = params.session;408this.message = params.message;409this._variableData = params.variableData;410this.timestamp = params.timestamp;411this._attempt = params.attempt ?? 0;412this.modeInfo = params.modeInfo;413this._confirmation = params.confirmation;414this._locationData = params.locationData;415this._attachedContext = params.attachedContext;416this.isCompleteAddedRequest = params.isCompleteAddedRequest ?? false;417this.modelId = params.modelId;418this.id = params.restoredId ?? 'request_' + generateUuid();419this._editedFileEvents = params.editedFileEvents;420this.userSelectedTools = params.userSelectedTools;421}422423adoptTo(session: ChatModel) {424this._session = session;425}426}427428class AbstractResponse implements IResponse {429protected _responseParts: IChatProgressResponseContent[];430431/**432* A stringified representation of response data which might be presented to a screenreader or used when copying a response.433*/434protected _responseRepr = '';435436/**437* Just the markdown content of the response, used for determining the rendering rate of markdown438*/439protected _markdownContent = '';440441get value(): IChatProgressResponseContent[] {442return this._responseParts;443}444445constructor(value: IChatProgressResponseContent[]) {446this._responseParts = value;447this._updateRepr();448}449450toString(): string {451return this._responseRepr;452}453454/**455* _Just_ the content of markdown parts in the response456*/457getMarkdown(): string {458return this._markdownContent;459}460461protected _updateRepr() {462this._responseRepr = this.partsToRepr(this._responseParts);463464this._markdownContent = this._responseParts.map(part => {465if (part.kind === 'inlineReference') {466return this.inlineRefToRepr(part);467} else if (part.kind === 'markdownContent' || part.kind === 'markdownVuln') {468return part.content.value;469} else {470return '';471}472})473.filter(s => s.length > 0)474.join('');475}476477private partsToRepr(parts: readonly IChatProgressResponseContent[]): string {478const blocks: string[] = [];479let currentBlockSegments: string[] = [];480let hasEditGroupsAfterLastClear = false;481482for (const part of parts) {483let segment: { text: string; isBlock?: boolean } | undefined;484switch (part.kind) {485case 'clearToPreviousToolInvocation':486currentBlockSegments = [];487blocks.length = 0;488hasEditGroupsAfterLastClear = false; // Reset edit groups flag when clearing489continue;490case 'treeData':491case 'progressMessage':492case 'codeblockUri':493case 'extensions':494case 'pullRequest':495case 'undoStop':496case 'workspaceEdit':497case 'elicitation2':498case 'elicitationSerialized':499case 'thinking':500case 'hook':501case 'multiDiffData':502case 'mcpServersStarting':503case 'questionCarousel':504// Ignore505continue;506case 'toolInvocation':507case 'toolInvocationSerialized':508// Include tool invocations in the copy text509segment = this.getToolInvocationText(part);510break;511case 'inlineReference':512segment = { text: this.inlineRefToRepr(part) };513break;514case 'command':515segment = { text: part.command.title, isBlock: true };516break;517case 'textEditGroup':518case 'notebookEditGroup':519// Mark that we have edit groups after the last clear520hasEditGroupsAfterLastClear = true;521// Skip individual edit groups to avoid duplication522continue;523case 'confirmation':524if (part.message instanceof MarkdownString) {525segment = { text: `${part.title}\n${part.message.value}`, isBlock: true };526break;527}528segment = { text: `${part.title}\n${part.message}`, isBlock: true };529break;530case 'markdownContent':531case 'markdownVuln':532case 'progressTask':533case 'progressTaskSerialized':534case 'warning':535segment = { text: part.content.value };536break;537default:538// Ignore any unknown/obsolete parts, but assert that all are handled:539softAssertNever(part);540continue;541}542543if (segment.isBlock) {544if (currentBlockSegments.length) {545blocks.push(currentBlockSegments.join(''));546currentBlockSegments = [];547}548blocks.push(segment.text);549} else {550currentBlockSegments.push(segment.text);551}552}553554if (currentBlockSegments.length) {555blocks.push(currentBlockSegments.join(''));556}557558// Add consolidated edit summary at the end if there were any edit groups after the last clear559if (hasEditGroupsAfterLastClear) {560blocks.push(localize('editsSummary', "Made changes."));561}562563return blocks.join('\n\n');564}565566private inlineRefToRepr(part: IChatContentInlineReference) {567if ('uri' in part.inlineReference) {568return this.uriToRepr(part.inlineReference.uri);569}570571return 'name' in part.inlineReference572? '`' + part.inlineReference.name + '`'573: this.uriToRepr(part.inlineReference);574}575576private getToolInvocationText(toolInvocation: IChatToolInvocation | IChatToolInvocationSerialized): { text: string; isBlock?: boolean } {577// Extract the message and input details578let message = '';579let input = '';580581if (toolInvocation.pastTenseMessage) {582message = typeof toolInvocation.pastTenseMessage === 'string'583? toolInvocation.pastTenseMessage584: toolInvocation.pastTenseMessage.value;585} else {586message = typeof toolInvocation.invocationMessage === 'string'587? toolInvocation.invocationMessage588: toolInvocation.invocationMessage.value;589}590591// Handle different types of tool invocations592if (toolInvocation.toolSpecificData) {593if (toolInvocation.toolSpecificData.kind === 'terminal') {594message = 'Ran terminal command';595const terminalData = migrateLegacyTerminalToolSpecificData(toolInvocation.toolSpecificData);596input = terminalData.commandLine.userEdited ?? terminalData.commandLine.toolEdited ?? terminalData.commandLine.original;597}598}599600// Format the tool invocation text601let text = message;602if (input) {603text += `: ${input}`;604}605606// For completed tool invocations, also include the result details if available607if (toolInvocation.kind === 'toolInvocationSerialized' || (toolInvocation.kind === 'toolInvocation' && IChatToolInvocation.isComplete(toolInvocation))) {608const resultDetails = IChatToolInvocation.resultDetails(toolInvocation);609if (resultDetails && 'input' in resultDetails) {610const resultPrefix = toolInvocation.kind === 'toolInvocationSerialized' || IChatToolInvocation.isComplete(toolInvocation) ? 'Completed' : 'Errored';611text += `\n${resultPrefix} with input: ${resultDetails.input}`;612}613}614615return { text, isBlock: true };616}617618private uriToRepr(uri: URI): string {619if (uri.scheme === Schemas.http || uri.scheme === Schemas.https) {620return uri.toString(false);621}622623return basename(uri);624}625}626627/** A view of a subset of a response */628class ResponseView extends AbstractResponse {629constructor(630_response: IResponse,631public readonly undoStop: string,632) {633let idx = _response.value.findIndex(v => v.kind === 'undoStop' && v.id === undoStop);634// Undo stops are inserted before `codeblockUri`'s, which are preceeded by a635// markdownContent containing the opening code fence. Adjust the index636// backwards to avoid a buggy response if it looked like this happened.637if (_response.value[idx + 1]?.kind === 'codeblockUri' && _response.value[idx - 1]?.kind === 'markdownContent') {638idx--;639}640641super(idx === -1 ? _response.value.slice() : _response.value.slice(0, idx));642}643}644645export class Response extends AbstractResponse implements IDisposable {646private _onDidChangeValue = new Emitter<void>();647public get onDidChangeValue() {648return this._onDidChangeValue.event;649}650651private _citations: IChatCodeCitation[] = [];652653654constructor(value: IMarkdownString | ReadonlyArray<SerializedChatResponsePart>) {655super(asArray(value).map((v) => (656'kind' in v ? v :657isMarkdownString(v) ? { content: v, kind: 'markdownContent' } satisfies IChatMarkdownContent :658{ kind: 'treeData', treeData: v }659)));660}661662dispose(): void {663this._onDidChangeValue.dispose();664}665666667clear(): void {668this._responseParts = [];669this._updateRepr(true);670}671672clearToPreviousToolInvocation(message?: string): void {673// look through the response parts and find the last tool invocation, then slice the response parts to that point674let lastToolInvocationIndex = -1;675for (let i = this._responseParts.length - 1; i >= 0; i--) {676const part = this._responseParts[i];677if (part.kind === 'toolInvocation' || part.kind === 'toolInvocationSerialized') {678lastToolInvocationIndex = i;679break;680}681}682if (lastToolInvocationIndex !== -1) {683this._responseParts = this._responseParts.slice(0, lastToolInvocationIndex + 1);684} else {685this._responseParts = [];686}687if (message) {688this._responseParts.push({ kind: 'warning', content: new MarkdownString(message) });689}690this._updateRepr(true);691}692693updateContent(progress: IChatProgressResponseContent | IChatTextEdit | IChatNotebookEdit | IChatTask | IChatExternalToolInvocationUpdate, quiet?: boolean): void {694if (progress.kind === 'clearToPreviousToolInvocation') {695if (progress.reason === ChatResponseClearToPreviousToolInvocationReason.CopyrightContentRetry) {696this.clearToPreviousToolInvocation(localize('copyrightContentRetry', "Response cleared due to possible match to public code, retrying with modified prompt."));697} else if (progress.reason === ChatResponseClearToPreviousToolInvocationReason.FilteredContentRetry) {698this.clearToPreviousToolInvocation(localize('filteredContentRetry', "Response cleared due to content safety filters, retrying with modified prompt."));699} else {700this.clearToPreviousToolInvocation();701}702return;703} else if (progress.kind === 'markdownContent') {704705// last response which is NOT a text edit group because we do want to support heterogenous streaming but not have706// the MD be chopped up by text edit groups (and likely other non-renderable parts)707const lastResponsePart = this._responseParts708.filter(p => p.kind !== 'textEditGroup')709.at(-1);710711if (!lastResponsePart || lastResponsePart.kind !== 'markdownContent' || !canMergeMarkdownStrings(lastResponsePart.content, progress.content)) {712// The last part can't be merged with- not markdown, or markdown with different permissions713this._responseParts.push(progress);714} else {715// Don't modify the current object, since it's being diffed by the renderer716const idx = this._responseParts.indexOf(lastResponsePart);717this._responseParts[idx] = { ...lastResponsePart, content: appendMarkdownString(lastResponsePart.content, progress.content) };718}719this._updateRepr(quiet);720} else if (progress.kind === 'thinking') {721722// tries to split thinking chunks if it is an array. only while certain models give us array chunks.723const lastResponsePart = this._responseParts724.filter(p => p.kind !== 'textEditGroup')725.at(-1);726727const lastText = lastResponsePart && lastResponsePart.kind === 'thinking'728? (Array.isArray(lastResponsePart.value) ? lastResponsePart.value.join('') : (lastResponsePart.value || ''))729: '';730const currText = Array.isArray(progress.value) ? progress.value.join('') : (progress.value || '');731const isEmpty = (s: string) => s.length === 0;732733// Do not merge if either the current or last thinking chunk is empty; empty chunks separate thinking734if (!lastResponsePart735|| lastResponsePart.kind !== 'thinking'736|| isEmpty(currText)737|| isEmpty(lastText)738|| !canMergeMarkdownStrings(new MarkdownString(lastText), new MarkdownString(currText))) {739this._responseParts.push(progress);740} else {741const idx = this._responseParts.indexOf(lastResponsePart);742this._responseParts[idx] = {743...lastResponsePart,744value: appendMarkdownString(new MarkdownString(lastText), new MarkdownString(currText)).value745};746}747this._updateRepr(quiet);748} else if (progress.kind === 'textEdit' || progress.kind === 'notebookEdit') {749// merge edits for the same file no matter when they come in750const notebookUri = CellUri.parse(progress.uri)?.notebook;751const uri = notebookUri ?? progress.uri;752const isExternalEdit = progress.isExternalEdit;753754if (progress.kind === 'textEdit' && !notebookUri) {755// Text edits to a regular (non-notebook) file756this._mergeOrPushTextEditGroup(uri, progress.edits, progress.done, isExternalEdit);757} else if (progress.kind === 'textEdit') {758// Text edits to a notebook cell - convert to ICellTextEditOperation759const cellEdits = progress.edits.map(edit => ({ uri: progress.uri, edit }));760this._mergeOrPushNotebookEditGroup(uri, cellEdits, progress.done, isExternalEdit);761} else {762// Notebook cell edits (ICellEditOperation)763this._mergeOrPushNotebookEditGroup(uri, progress.edits, progress.done, isExternalEdit);764}765this._updateRepr(quiet);766} else if (progress.kind === 'progressTask') {767// Add a new resolving part768const responsePosition = this._responseParts.push(progress) - 1;769this._updateRepr(quiet);770771const disp = progress.onDidAddProgress(() => {772this._updateRepr(false);773});774775progress.task?.().then((content) => {776// Stop listening for progress updates once the task settles777disp.dispose();778779// Replace the resolving part's content with the resolved response780if (typeof content === 'string') {781(this._responseParts[responsePosition] as IChatTask).content = new MarkdownString(content);782}783this._updateRepr(false);784});785786} else if (progress.kind === 'toolInvocation') {787autorunSelfDisposable(reader => {788progress.state.read(reader); // update repr when state changes789this._updateRepr(false);790791if (IChatToolInvocation.isComplete(progress, reader)) {792reader.dispose();793}794});795this._responseParts.push(progress);796this._updateRepr(quiet);797} else if (progress.kind === 'externalToolInvocationUpdate') {798this._handleExternalToolInvocationUpdate(progress);799this._updateRepr(quiet);800} else {801this._responseParts.push(progress);802this._updateRepr(quiet);803}804}805806public addCitation(citation: IChatCodeCitation) {807this._citations.push(citation);808this._updateRepr();809}810811private _mergeOrPushTextEditGroup(uri: URI, edits: TextEdit[], done: boolean | undefined, isExternalEdit: boolean | undefined): void {812for (const candidate of this._responseParts) {813if (candidate.kind === 'textEditGroup' && !candidate.done && isEqual(candidate.uri, uri)) {814candidate.edits.push(edits);815candidate.done = done;816return;817}818}819this._responseParts.push({ kind: 'textEditGroup', uri, edits: [edits], done, isExternalEdit });820}821822private _mergeOrPushNotebookEditGroup(uri: URI, edits: ICellTextEditOperation[] | ICellEditOperation[], done: boolean | undefined, isExternalEdit: boolean | undefined): void {823for (const candidate of this._responseParts) {824if (candidate.kind === 'notebookEditGroup' && !candidate.done && isEqual(candidate.uri, uri)) {825candidate.edits.push(edits);826candidate.done = done;827return;828}829}830this._responseParts.push({ kind: 'notebookEditGroup', uri, edits: [edits], done, isExternalEdit });831}832833private _handleExternalToolInvocationUpdate(progress: IChatExternalToolInvocationUpdate): void {834// Look for existing invocation in the response parts835const existingInvocation = this._responseParts.findLast(836(part): part is ChatToolInvocation => part.kind === 'toolInvocation' && part.toolCallId === progress.toolCallId837);838839if (existingInvocation) {840if (progress.isComplete) {841existingInvocation.didExecuteTool({842content: [],843toolResultMessage: progress.pastTenseMessage,844toolResultError: progress.errorMessage,845});846}847if (progress.toolSpecificData !== undefined) {848existingInvocation.toolSpecificData = progress.toolSpecificData;849}850return;851}852853// Create a new external tool invocation854const toolData: IToolData = {855id: progress.toolName,856source: ToolDataSource.External,857displayName: progress.toolName,858modelDescription: progress.toolName,859};860861const invocation = new ChatToolInvocation(862{863invocationMessage: progress.invocationMessage,864pastTenseMessage: progress.pastTenseMessage,865toolSpecificData: progress.toolSpecificData,866},867toolData,868progress.toolCallId,869progress.subagentInvocationId,870undefined, // parameters871{},872undefined // chatRequestId873);874875if (progress.isComplete) {876// Already completed on first push877invocation.didExecuteTool({878content: [],879toolResultMessage: progress.pastTenseMessage,880toolResultError: progress.errorMessage,881});882if (progress.toolSpecificData !== undefined) {883invocation.toolSpecificData = progress.toolSpecificData;884}885}886887this._responseParts.push(invocation);888}889890protected override _updateRepr(quiet?: boolean) {891super._updateRepr();892if (!this._onDidChangeValue) {893return; // called from parent constructor894}895896this._responseRepr += this._citations.length ? '\n\n' + getCodeCitationsMessage(this._citations) : '';897898if (!quiet) {899this._onDidChangeValue.fire();900}901}902}903904export interface IChatResponseModelParameters {905responseContent: IMarkdownString | ReadonlyArray<SerializedChatResponsePart>;906session: ChatModel;907agent?: IChatAgentData;908slashCommand?: IChatAgentCommand;909requestId: string;910timestamp?: number;911vote?: ChatAgentVoteDirection;912voteDownReason?: ChatAgentVoteDownReason;913result?: IChatAgentResult;914followups?: ReadonlyArray<IChatFollowup>;915isCompleteAddedRequest?: boolean;916shouldBeRemovedOnSend?: IChatRequestDisablement;917shouldBeBlocked?: boolean;918restoredId?: string;919modelState?: ResponseModelStateT;920timeSpentWaiting?: number;921/**922* undefined means it will be set later.923*/924codeBlockInfos: ICodeBlockInfo[] | undefined;925}926927export type ResponseModelStateT =928| { value: ResponseModelState.Pending }929| { value: ResponseModelState.NeedsInput }930| { value: ResponseModelState.Complete | ResponseModelState.Cancelled | ResponseModelState.Failed; completedAt: number };931932export class ChatResponseModel extends Disposable implements IChatResponseModel {933private readonly _onDidChange = this._register(new Emitter<ChatResponseModelChangeReason>());934readonly onDidChange = this._onDidChange.event;935936public readonly id: string;937public readonly requestId: string;938private _session: ChatModel;939private _agent: IChatAgentData | undefined;940private _slashCommand: IChatAgentCommand | undefined;941private _modelState = observableValue<ResponseModelStateT>(this, { value: ResponseModelState.Pending });942private _vote?: ChatAgentVoteDirection;943private _voteDownReason?: ChatAgentVoteDownReason;944private _result?: IChatAgentResult;945private _usage?: IChatUsage;946private _shouldBeRemovedOnSend: IChatRequestDisablement | undefined;947public readonly isCompleteAddedRequest: boolean;948private readonly _shouldBeBlocked = observableValue<boolean>(this, false);949private readonly _timestamp: number;950private _timeSpentWaitingAccumulator: number;951952public confirmationAdjustedTimestamp: IObservable<number>;953954public get shouldBeBlocked(): IObservable<boolean> {955return this._shouldBeBlocked;956}957958public get request(): IChatRequestModel | undefined {959return this.session.getRequests().find(r => r.id === this.requestId);960}961962public get session() {963return this._session;964}965966public get shouldBeRemovedOnSend() {967return this._shouldBeRemovedOnSend;968}969970public get isComplete(): boolean {971return this._modelState.get().value !== ResponseModelState.Pending && this._modelState.get().value !== ResponseModelState.NeedsInput;972}973974public get timestamp(): number {975return this._timestamp;976}977978public set shouldBeRemovedOnSend(disablement: IChatRequestDisablement | undefined) {979if (this._shouldBeRemovedOnSend === disablement) {980return;981}982983this._shouldBeRemovedOnSend = disablement;984this._onDidChange.fire(defaultChatResponseModelChangeReason);985}986987public get isCanceled(): boolean {988return this._modelState.get().value === ResponseModelState.Cancelled;989}990991public get completedAt(): number | undefined {992const state = this._modelState.get();993if (state.value === ResponseModelState.Complete || state.value === ResponseModelState.Cancelled || state.value === ResponseModelState.Failed) {994return state.completedAt;995}996return undefined;997}998999public get state(): ResponseModelState {1000const state = this._modelState.get().value;1001if (state === ResponseModelState.Complete && !!this._result?.errorDetails && this.result?.errorDetails?.code !== 'canceled') {1002// This check covers sessions created in previous vscode versions which saved a failed response as 'Complete'1003return ResponseModelState.Failed;1004}10051006return state;1007}10081009public get stateT(): ResponseModelStateT {1010return this._modelState.get();1011}10121013public get vote(): ChatAgentVoteDirection | undefined {1014return this._vote;1015}10161017public get voteDownReason(): ChatAgentVoteDownReason | undefined {1018return this._voteDownReason;1019}10201021public get followups(): IChatFollowup[] | undefined {1022return this._followups;1023}10241025private _response: Response;1026private _finalizedResponse?: IResponse;1027public get entireResponse(): IResponse {1028return this._finalizedResponse || this._response;1029}10301031public get result(): IChatAgentResult | undefined {1032return this._result;1033}10341035public get usage(): IChatUsage | undefined {1036return this._usage;1037}10381039public get username(): string {1040return this.session.responderUsername;1041}10421043private _followups?: IChatFollowup[];10441045public get agent(): IChatAgentData | undefined {1046return this._agent;1047}10481049public get slashCommand(): IChatAgentCommand | undefined {1050return this._slashCommand;1051}10521053private _agentOrSlashCommandDetected: boolean | undefined;1054public get agentOrSlashCommandDetected(): boolean {1055return this._agentOrSlashCommandDetected ?? false;1056}10571058private _usedContext: IChatUsedContext | undefined;1059public get usedContext(): IChatUsedContext | undefined {1060return this._usedContext;1061}10621063private readonly _contentReferences: IChatContentReference[] = [];1064public get contentReferences(): ReadonlyArray<IChatContentReference> {1065return Array.from(this._contentReferences);1066}10671068private readonly _codeCitations: IChatCodeCitation[] = [];1069public get codeCitations(): ReadonlyArray<IChatCodeCitation> {1070return this._codeCitations;1071}10721073private readonly _progressMessages: IChatProgressMessage[] = [];1074public get progressMessages(): ReadonlyArray<IChatProgressMessage> {1075return this._progressMessages;1076}10771078private _isStale: boolean = false;1079public get isStale(): boolean {1080return this._isStale;1081}108210831084readonly isPendingConfirmation: IObservable<{ startedWaitingAt: number; detail?: string } | undefined>;10851086readonly isInProgress: IObservable<boolean>;10871088private _responseView?: ResponseView;1089public get response(): IResponse {1090const undoStop = this._shouldBeRemovedOnSend?.afterUndoStop;1091if (!undoStop) {1092return this._finalizedResponse || this._response;1093}10941095if (this._responseView?.undoStop !== undoStop) {1096this._responseView = new ResponseView(this._response, undoStop);1097}10981099return this._responseView;1100}11011102private _codeBlockInfos: ICodeBlockInfo[] | undefined;1103public get codeBlockInfos(): ICodeBlockInfo[] | undefined {1104return this._codeBlockInfos;1105}11061107constructor(params: IChatResponseModelParameters) {1108super();11091110this._session = params.session;1111this._agent = params.agent;1112this._slashCommand = params.slashCommand;1113this.requestId = params.requestId;1114this._timestamp = params.timestamp || Date.now();1115if (params.modelState) {1116this._modelState.set(params.modelState, undefined);1117}1118this._timeSpentWaitingAccumulator = params.timeSpentWaiting || 0;1119this._vote = params.vote;1120this._voteDownReason = params.voteDownReason;1121this._result = params.result;1122this._followups = params.followups ? [...params.followups] : undefined;1123this.isCompleteAddedRequest = params.isCompleteAddedRequest ?? false;1124this._shouldBeRemovedOnSend = params.shouldBeRemovedOnSend;1125this._shouldBeBlocked.set(params.shouldBeBlocked ?? false, undefined);11261127// If we are creating a response with some existing content, consider it stale1128this._isStale = Array.isArray(params.responseContent) && (params.responseContent.length !== 0 || isMarkdownString(params.responseContent) && params.responseContent.value.length !== 0);11291130this._response = this._register(new Response(params.responseContent));1131this._codeBlockInfos = params.codeBlockInfos ? [...params.codeBlockInfos] : undefined;11321133const signal = observableSignalFromEvent(this, this.onDidChange);11341135const _pendingInfo = signal.map((_value, r): string | undefined => {1136signal.read(r);11371138for (const part of this._response.value) {1139if (part.kind === 'toolInvocation') {1140const state = part.state.read(r);1141if (state.type === IChatToolInvocation.StateKind.WaitingForConfirmation) {1142const title = state.confirmationMessages?.title;1143return title ? (isMarkdownString(title) ? title.value : title) : undefined;1144}1145if (state.type === IChatToolInvocation.StateKind.WaitingForPostApproval) {1146return localize('waitingForPostApproval', "Approve tool result?");1147}1148}1149if (part.kind === 'confirmation' && !part.isUsed) {1150return part.title;1151}1152if (part.kind === 'elicitation2' && part.state.read(r) === ElicitationState.Pending) {1153const title = part.title;1154return isMarkdownString(title) ? title.value : title;1155}1156}11571158return undefined;1159});11601161const _startedWaitingAt = _pendingInfo.map(p => !!p).map(p => p ? Date.now() : undefined);1162this.isPendingConfirmation = _startedWaitingAt.map((waiting, r) => waiting ? { startedWaitingAt: waiting, detail: _pendingInfo.read(r) } : undefined);11631164this.isInProgress = signal.map((_value, r) => {11651166signal.read(r);11671168return !_pendingInfo.read(r)1169&& !this.shouldBeRemovedOnSend1170&& (this._modelState.read(r).value === ResponseModelState.Pending || this._modelState.read(r).value === ResponseModelState.NeedsInput);1171});11721173this._register(this._response.onDidChangeValue(() => this._onDidChange.fire(defaultChatResponseModelChangeReason)));1174this.id = params.restoredId ?? 'response_' + generateUuid();11751176let lastStartedWaitingAt: number | undefined = undefined;1177this.confirmationAdjustedTimestamp = derived(reader => {1178const pending = this.isPendingConfirmation.read(reader);1179if (pending) {1180this._modelState.set({ value: ResponseModelState.NeedsInput }, undefined);1181if (!lastStartedWaitingAt) {1182lastStartedWaitingAt = pending.startedWaitingAt;1183}1184} else if (lastStartedWaitingAt) {1185// Restore state to Pending if it was set to NeedsInput by this observable1186if (this._modelState.read(reader).value === ResponseModelState.NeedsInput) {1187this._modelState.set({ value: ResponseModelState.Pending }, undefined);1188}1189this._timeSpentWaitingAccumulator += Date.now() - lastStartedWaitingAt;1190lastStartedWaitingAt = undefined;1191}11921193return this._timestamp + this._timeSpentWaitingAccumulator;1194}).recomputeInitiallyAndOnChange(this._store);1195}11961197initializeCodeBlockInfos(codeBlockInfo: ICodeBlockInfo[]): void {1198if (this._codeBlockInfos) {1199throw new BugIndicatingError('Code block infos have already been initialized');1200}1201this._codeBlockInfos = [...codeBlockInfo];1202}12031204setBlockedState(isBlocked: boolean): void {1205this._shouldBeBlocked.set(isBlocked, undefined);1206}12071208/**1209* Apply a progress update to the actual response content.1210*/1211updateContent(responsePart: IChatProgressResponseContent | IChatTextEdit | IChatNotebookEdit | IChatExternalToolInvocationUpdate, quiet?: boolean) {1212this._response.updateContent(responsePart, quiet);1213}12141215/**1216* Adds an undo stop at the current position in the stream.1217*/1218addUndoStop(undoStop: IChatUndoStop) {1219this._onDidChange.fire({ reason: 'undoStop', id: undoStop.id });1220this._response.updateContent(undoStop, true);1221}12221223/**1224* Apply one of the progress updates that are not part of the actual response content.1225*/1226applyReference(progress: IChatUsedContext | IChatContentReference) {1227if (progress.kind === 'usedContext') {1228this._usedContext = progress;1229} else if (progress.kind === 'reference') {1230this._contentReferences.push(progress);1231this._onDidChange.fire(defaultChatResponseModelChangeReason);1232}1233}12341235applyCodeCitation(progress: IChatCodeCitation) {1236this._codeCitations.push(progress);1237this._response.addCitation(progress);1238this._onDidChange.fire(defaultChatResponseModelChangeReason);1239}12401241setAgent(agent: IChatAgentData, slashCommand?: IChatAgentCommand) {1242this._agent = agent;1243this._slashCommand = slashCommand;1244this._agentOrSlashCommandDetected = !agent.isDefault || !!slashCommand;1245this._onDidChange.fire(defaultChatResponseModelChangeReason);1246}12471248setResult(result: IChatAgentResult): void {1249this._result = result;1250this._onDidChange.fire(defaultChatResponseModelChangeReason);1251}12521253setUsage(usage: IChatUsage): void {1254this._usage = usage;1255this._onDidChange.fire(defaultChatResponseModelChangeReason);1256}12571258complete(): void {1259// No-op if it's already complete1260if (this.isComplete) {1261return;1262}1263if (this._result?.errorDetails?.responseIsRedacted) {1264this._response.clear();1265}12661267// Canceled sessions can be considered 'Complete'1268const state = !!this._result?.errorDetails && this._result.errorDetails.code !== 'canceled' ? ResponseModelState.Failed : ResponseModelState.Complete;1269this._modelState.set({ value: state, completedAt: Date.now() }, undefined);1270this._onDidChange.fire({ reason: 'completedRequest' });1271}12721273cancel(): void {1274this._modelState.set({ value: ResponseModelState.Cancelled, completedAt: Date.now() }, undefined);1275this._onDidChange.fire({ reason: 'completedRequest' });1276}12771278setFollowups(followups: IChatFollowup[] | undefined): void {1279this._followups = followups;1280this._onDidChange.fire(defaultChatResponseModelChangeReason); // Fire so that command followups get rendered on the row1281}12821283setVote(vote: ChatAgentVoteDirection): void {1284this._vote = vote;1285this._onDidChange.fire(defaultChatResponseModelChangeReason);1286}12871288setVoteDownReason(reason: ChatAgentVoteDownReason | undefined): void {1289this._voteDownReason = reason;1290this._onDidChange.fire(defaultChatResponseModelChangeReason);1291}12921293setEditApplied(edit: IChatTextEditGroup, editCount: number): boolean {1294if (!this.response.value.includes(edit)) {1295return false;1296}1297if (!edit.state) {1298return false;1299}1300edit.state.applied = editCount; // must not be edit.edits.length1301this._onDidChange.fire(defaultChatResponseModelChangeReason);1302return true;1303}13041305adoptTo(session: ChatModel) {1306this._session = session;1307this._onDidChange.fire(defaultChatResponseModelChangeReason);1308}130913101311finalizeUndoState(): void {1312this._finalizedResponse = this.response;1313this._responseView = undefined;1314this._shouldBeRemovedOnSend = undefined;1315}13161317toJSON(): ISerializableChatResponseData {1318const modelState = this._modelState.get();1319const pendingConfirmation = this.isPendingConfirmation.get();13201321return {1322responseId: this.id,1323result: this.result,1324responseMarkdownInfo: this.codeBlockInfos?.map<ISerializableMarkdownInfo>(info => ({ suggestionId: info.suggestionId })),1325followups: this.followups,1326modelState: modelState.value === ResponseModelState.Pending || modelState.value === ResponseModelState.NeedsInput ? { value: ResponseModelState.Cancelled, completedAt: Date.now() } : modelState,1327vote: this.vote,1328voteDownReason: this.voteDownReason,1329slashCommand: this.slashCommand,1330usedContext: this.usedContext,1331contentReferences: this.contentReferences,1332codeCitations: this.codeCitations,1333timestamp: this._timestamp,1334timeSpentWaiting: (pendingConfirmation ? Date.now() - pendingConfirmation.startedWaitingAt : 0) + this._timeSpentWaitingAccumulator,1335} satisfies WithDefinedProps<ISerializableChatResponseData>;1336}1337}133813391340export interface IChatRequestDisablement {1341requestId: string;1342afterUndoStop?: string;1343}13441345/**1346* Information about a chat request that needs user input to continue.1347*/1348export interface IChatRequestNeedsInputInfo {1349/** The chat session title */1350readonly title: string;1351/** Optional detail message, e.g., "<toolname> needs approval to run." */1352readonly detail?: string;1353}13541355export interface IChatModel extends IDisposable {1356readonly onDidDispose: Event<void>;1357readonly onDidChange: Event<IChatChangeEvent>;1358/** @deprecated Use {@link sessionResource} instead */1359readonly sessionId: string;1360/** Milliseconds timestamp this chat model was created. */1361readonly timestamp: number;1362readonly timing: IChatSessionTiming;1363readonly sessionResource: URI;1364readonly initialLocation: ChatAgentLocation;1365readonly title: string;1366readonly hasCustomTitle: boolean;1367readonly responderUsername: string;1368/** True whenever a request is currently running */1369readonly requestInProgress: IObservable<boolean>;1370/** Provides session information when a request needs user interaction to continue */1371readonly requestNeedsInput: IObservable<IChatRequestNeedsInputInfo | undefined>;1372readonly inputPlaceholder?: string;1373readonly editingSession?: IChatEditingSession | undefined;1374readonly checkpoint: IChatRequestModel | undefined;1375startEditingSession(isGlobalEditingSession?: boolean, transferFromSession?: IChatEditingSession): void;1376/** Input model for managing input state */1377readonly inputModel: IInputModel;1378readonly hasRequests: boolean;1379readonly lastRequest: IChatRequestModel | undefined;1380/** Whether this model will be kept alive while it is running or has edits */1381readonly willKeepAlive: boolean;1382readonly lastRequestObs: IObservable<IChatRequestModel | undefined>;1383getRequests(): IChatRequestModel[];1384setCheckpoint(requestId: string | undefined): void;13851386toExport(): IExportableChatData;1387toJSON(): ISerializableChatData;1388readonly contributedChatSession: IChatSessionContext | undefined;13891390readonly repoData: IExportableRepoData | undefined;1391setRepoData(data: IExportableRepoData | undefined): void;13921393readonly onDidChangePendingRequests: Event<void>;1394getPendingRequests(): readonly IChatPendingRequest[];1395}13961397export interface ISerializableChatsData {1398[sessionId: string]: ISerializableChatData;1399}14001401export type ISerializableChatAgentData = UriDto<IChatAgentData>;14021403interface ISerializableChatResponseData {1404responseId?: string;1405result?: IChatAgentResult; // Optional for backcompat1406responseMarkdownInfo?: ISerializableMarkdownInfo[];1407followups?: ReadonlyArray<IChatFollowup>;1408modelState?: ResponseModelStateT;1409vote?: ChatAgentVoteDirection;1410voteDownReason?: ChatAgentVoteDownReason;1411timestamp?: number;1412slashCommand?: IChatAgentCommand;1413/** For backward compat: should be optional */1414usedContext?: IChatUsedContext;1415contentReferences?: ReadonlyArray<IChatContentReference>;1416codeCitations?: ReadonlyArray<IChatCodeCitation>;1417timeSpentWaiting?: number;1418}14191420export type SerializedChatResponsePart = IMarkdownString | IChatResponseProgressFileTreeData | IChatContentInlineReference | IChatAgentMarkdownContentWithVulnerability | IChatThinkingPart | IChatProgressResponseContentSerialized | IChatQuestionCarousel;14211422export interface ISerializableChatRequestData extends ISerializableChatResponseData {1423requestId: string;1424message: string | IParsedChatRequest; // string => old format1425/** Is really like "prompt data". This is the message in the format in which the agent gets it + variable values. */1426variableData: IChatRequestVariableData;1427response: ReadonlyArray<SerializedChatResponsePart> | undefined;14281429/**Old, persisted name for shouldBeRemovedOnSend */1430isHidden?: boolean;1431shouldBeRemovedOnSend?: IChatRequestDisablement;1432agent?: ISerializableChatAgentData;1433// responseErrorDetails: IChatResponseErrorDetails | undefined;1434/** @deprecated modelState is used instead now */1435isCanceled?: boolean;1436timestamp?: number;1437confirmation?: string;1438editedFileEvents?: IChatAgentEditedFileEvent[];1439modelId?: string;1440}14411442export interface ISerializableMarkdownInfo {1443readonly suggestionId: EditSuggestionId;1444}14451446/**1447* Repository state captured for chat session export.1448* Enables reproducing the workspace state by cloning, checking out the commit, and applying diffs.1449*/1450export interface IExportableRepoData {1451/**1452* Classification of the workspace's version control state.1453* - `remote-git`: Git repo with a configured remote URL1454* - `local-git`: Git repo without any remote (local only)1455* - `plain-folder`: Not a git repository1456*/1457workspaceType: 'remote-git' | 'local-git' | 'plain-folder';14581459/**1460* Sync status between local and remote.1461* - `synced`: Local HEAD matches remote tracking branch (fully pushed)1462* - `unpushed`: Local has commits not pushed to the remote tracking branch1463* - `unpublished`: Local branch has no remote tracking branch configured1464* - `local-only`: No remote configured (local git repo only)1465* - `no-git`: Not a git repository1466*/1467syncStatus: 'synced' | 'unpushed' | 'unpublished' | 'local-only' | 'no-git';14681469/**1470* Remote URL of the repository (e.g., https://github.com/org/repo.git).1471* Undefined if no remote is configured.1472*/1473remoteUrl?: string;14741475/**1476* Vendor/host of the remote repository.1477* Undefined if no remote is configured.1478*/1479remoteVendor?: 'github' | 'ado' | 'other';14801481/**1482* Remote tracking branch for the current branch (e.g., "origin/feature/my-work").1483* Undefined if branch is unpublished or no remote.1484*/1485remoteTrackingBranch?: string;14861487/**1488* Default remote branch used as base for unpublished branches (e.g., "origin/main").1489* Helpful for computing merge-base when branch has no tracking.1490*/1491remoteBaseBranch?: string;14921493/**1494* Commit hash of the remote tracking branch HEAD.1495* Undefined if branch has no remote tracking branch.1496*/1497remoteHeadCommit?: string;14981499/**1500* Name of the current local branch (e.g., "feature/my-work").1501*/1502localBranch?: string;15031504/**1505* Commit hash of the local HEAD when captured.1506*/1507localHeadCommit?: string;15081509/**1510* Working tree diffs (uncommitted changes).1511*/1512diffs?: IExportableRepoDiff[];15131514/**1515* Status of the diffs collection.1516* - `included`: Diffs were successfully captured and included1517* - `tooManyChanges`: Diffs skipped because >100 files changed (degenerate case like mass renames)1518* - `tooLarge`: Diffs skipped because total size exceeded 900KB1519* - `trimmedForStorage`: Diffs were trimmed to save storage (older session)1520* - `noChanges`: No working tree changes detected1521* - `notCaptured`: Diffs not captured (default/undefined case)1522*/1523diffsStatus?: 'included' | 'tooManyChanges' | 'tooLarge' | 'trimmedForStorage' | 'noChanges' | 'notCaptured';15241525/**1526* Number of changed files detected, even if diffs were not included.1527*/1528changedFileCount?: number;1529}15301531/**1532* A file change exported as a unified diff patch compatible with `git apply`.1533*/1534export interface IExportableRepoDiff {1535relativePath: string;1536changeType: 'added' | 'modified' | 'deleted' | 'renamed';1537oldRelativePath?: string;1538unifiedDiff?: string;1539status: string;1540}15411542export interface IExportableChatData {1543initialLocation: ChatAgentLocation | undefined;1544requests: ISerializableChatRequestData[];1545responderUsername: string;1546}15471548/*1549NOTE: 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.1550*/15511552export interface ISerializableChatData1 extends IExportableChatData {1553sessionId: string;1554creationDate: number;1555}15561557export interface ISerializableChatData2 extends ISerializableChatData1 {1558version: 2;1559computedTitle: string | undefined;1560}15611562export interface ISerializableChatData3 extends Omit<ISerializableChatData2, 'version' | 'computedTitle'> {1563version: 3;1564customTitle: string | undefined;1565/**1566* Whether the session had pending edits when it was stored.1567* todo@connor4312 This will be cleaned up with the globalization of edits.1568*/1569hasPendingEdits?: boolean;1570/** Current draft input state (added later, fully backwards compatible) */1571inputState?: ISerializableChatModelInputState;1572repoData?: IExportableRepoData;1573/** Pending requests that were queued but not yet processed */1574pendingRequests?: ISerializablePendingRequestData[];1575}15761577/**1578* Input model for managing chat input state independently from the chat model.1579* This keeps display logic separated from the core chat model.1580*1581* The input model:1582* - Manages the current draft state (text, attachments, mode, model selection, cursor/selection)1583* - Provides an observable interface for reactive UI updates1584* - Automatically persists through the chat model's serialization1585* - Enables bidirectional sync between the UI (ChatInputPart) and the model1586* - Uses `undefined` state to indicate no persisted state (new/empty chat)1587*1588* This architecture ensures that:1589* - Input state is preserved when moving chats between editor/sidebar/window1590* - No manual state transfer is needed when switching contexts1591* - The UI stays in sync with the persisted state1592* - New chats use UI defaults (persisted preferences) instead of hardcoded values1593*/1594export interface IInputModel {1595/** Observable for current input state (undefined for new/uninitialized chats) */1596readonly state: IObservable<IChatModelInputState | undefined>;15971598/** Update the input state (partial update) */1599setState(state: Partial<IChatModelInputState>): void;16001601/** Clear input state (after sending or clearing) */1602clearState(): void;16031604/** Serializes the state */1605toJSON(): ISerializableChatModelInputState | undefined;1606}16071608/**1609* Represents the current state of the chat input that hasn't been sent yet.1610* This is the "draft" state that should be preserved across sessions.1611*/1612export interface IChatModelInputState {1613/** Current attachments in the input */1614attachments: readonly IChatRequestVariableEntry[];16151616/** Currently selected chat mode */1617mode: {1618/** Mode ID (e.g., 'ask', 'edit', 'agent', or custom mode ID) */1619id: string;1620/** Mode kind for builtin modes */1621kind: ChatModeKind | undefined;1622};16231624/** Currently selected language model, if any */1625selectedModel: ILanguageModelChatMetadataAndIdentifier | undefined;16261627/** Current input text */1628inputText: string;16291630/** Current selection ranges */1631selections: ISelection[];16321633/** Contributed stored state */1634contrib: Record<string, unknown>;1635}16361637/**1638* Serializable version of IChatModelInputState1639*/1640export interface ISerializableChatModelInputState {1641attachments: readonly IChatRequestVariableEntry[];1642mode: {1643id: string;1644kind: ChatModeKind | undefined;1645};1646selectedModel: {1647identifier: string;1648metadata: ILanguageModelChatMetadata;1649} | undefined;1650inputText: string;1651selections: ISelection[];1652contrib: Record<string, unknown>;1653}16541655/**1656* Chat data that has been parsed and normalized to the current format.1657*/1658export type ISerializableChatData = ISerializableChatData3;16591660export type IChatDataSerializerLog = ObjectMutationLog<IChatModel, ISerializableChatData>;16611662export interface ISerializedChatDataReference {1663value: ISerializableChatData | IExportableChatData;1664serializer: IChatDataSerializerLog;1665}16661667/**1668* Chat data that has been loaded but not normalized, and could be any format1669*/1670export type ISerializableChatDataIn = ISerializableChatData1 | ISerializableChatData2 | ISerializableChatData3;16711672/**1673* Normalize chat data from storage to the current format.1674* TODO- ChatModel#_deserialize and reviveSerializedAgent also still do some normalization and maybe that should be done in here too.1675*/1676export function normalizeSerializableChatData(raw: ISerializableChatDataIn): ISerializableChatData {1677normalizeOldFields(raw);16781679if (!('version' in raw)) {1680return {1681version: 3,1682...raw,1683customTitle: undefined,1684};1685}16861687if (raw.version === 2) {1688return {1689...raw,1690version: 3,1691customTitle: raw.computedTitle1692};1693}16941695return raw;1696}16971698function normalizeOldFields(raw: ISerializableChatDataIn): void {1699// Fill in fields that very old chat data may be missing1700if (!raw.sessionId) {1701raw.sessionId = generateUuid();1702}17031704if (!raw.creationDate) {1705raw.creationDate = getLastYearDate();1706}17071708// eslint-disable-next-line @typescript-eslint/no-explicit-any, local/code-no-any-casts1709if ((raw.initialLocation as any) === 'editing-session') {1710raw.initialLocation = ChatAgentLocation.Chat;1711}1712}17131714function getLastYearDate(): number {1715const lastYearDate = new Date();1716lastYearDate.setFullYear(lastYearDate.getFullYear() - 1);1717return lastYearDate.getTime();1718}17191720export function isExportableSessionData(obj: unknown): obj is IExportableChatData {1721return !!obj &&1722Array.isArray((obj as IExportableChatData).requests) &&1723typeof (obj as IExportableChatData).responderUsername === 'string';1724}17251726export function isSerializableSessionData(obj: unknown): obj is ISerializableChatData {1727const data = obj as ISerializableChatData;1728return isExportableSessionData(obj) &&1729typeof data.creationDate === 'number' &&1730typeof data.sessionId === 'string' &&1731obj.requests.every((request: ISerializableChatRequestData) =>1732!request.usedContext /* for backward compat allow missing usedContext */ || isIUsedContext(request.usedContext)1733);1734}17351736export type IChatChangeEvent =1737| IChatInitEvent1738| IChatAddRequestEvent | IChatChangedRequestEvent | IChatRemoveRequestEvent1739| IChatAddResponseEvent1740| IChatSetAgentEvent1741| IChatMoveEvent1742| IChatSetHiddenEvent1743| IChatCompletedRequestEvent1744| IChatSetCustomTitleEvent1745;17461747export interface IChatAddRequestEvent {1748kind: 'addRequest';1749request: IChatRequestModel;1750}17511752export interface IChatChangedRequestEvent {1753kind: 'changedRequest';1754request: IChatRequestModel;1755}17561757export interface IChatCompletedRequestEvent {1758kind: 'completedRequest';1759request: IChatRequestModel;1760}17611762export interface IChatAddResponseEvent {1763kind: 'addResponse';1764response: IChatResponseModel;1765}17661767export const enum ChatRequestRemovalReason {1768/**1769* "Normal" remove1770*/1771Removal,17721773/**1774* Removed because the request will be resent1775*/1776Resend,17771778/**1779* Remove because the request is moving to another model1780*/1781Adoption1782}17831784export interface IChatRemoveRequestEvent {1785kind: 'removeRequest';1786requestId: string;1787responseId?: string;1788reason: ChatRequestRemovalReason;1789}17901791export interface IChatSetHiddenEvent {1792kind: 'setHidden';1793}17941795export interface IChatMoveEvent {1796kind: 'move';1797target: URI;1798range: IRange;1799}18001801export interface IChatSetAgentEvent {1802kind: 'setAgent';1803agent: IChatAgentData;1804command?: IChatAgentCommand;1805}18061807export interface IChatSetCustomTitleEvent {1808kind: 'setCustomTitle';1809title: string;1810}18111812export interface IChatInitEvent {1813kind: 'initialize';1814}18151816/**1817* Internal implementation of IInputModel1818*/1819class InputModel implements IInputModel {1820private readonly _state: ReturnType<typeof observableValue<IChatModelInputState | undefined>>;1821readonly state: IObservable<IChatModelInputState | undefined>;18221823constructor(initialState: IChatModelInputState | undefined) {1824this._state = observableValueOpts({ debugName: 'inputModelState', equalsFn: equals }, initialState);1825this.state = this._state;1826}18271828setState(state: Partial<IChatModelInputState>): void {1829const current = this._state.get();1830this._state.set({1831// If current is undefined, provide defaults for required fields1832attachments: [],1833mode: { id: 'agent', kind: ChatModeKind.Agent },1834selectedModel: undefined,1835inputText: '',1836selections: [],1837contrib: {},1838...current,1839...state1840}, undefined);1841}18421843clearState(): void {1844this._state.set(undefined, undefined);1845}18461847toJSON(): ISerializableChatModelInputState | undefined {1848const value = this.state.get();1849if (!value) {1850return undefined;1851}18521853// Filter out extension-contributed context items (kind: 'string' or implicit entries with StringChatContextValue)1854// These have handles that become invalid after window reload and cannot be properly restored.1855const persistableAttachments = value.attachments.filter(attachment => {1856if (isStringVariableEntry(attachment)) {1857return false;1858}1859if (isImplicitVariableEntry(attachment) && isStringImplicitContextValue(attachment.value)) {1860return false;1861}1862return true;1863});18641865return {1866contrib: value.contrib,1867attachments: persistableAttachments,1868mode: value.mode,1869selectedModel: value.selectedModel ? {1870identifier: value.selectedModel.identifier,1871metadata: value.selectedModel.metadata1872} : undefined,1873inputText: value.inputText,1874selections: value.selections1875};1876}1877}18781879export class ChatModel extends Disposable implements IChatModel {1880static getDefaultTitle(requests: (ISerializableChatRequestData | IChatRequestModel)[]): string {1881const firstRequestMessage = requests.at(0)?.message ?? '';1882const message = typeof firstRequestMessage === 'string' ?1883firstRequestMessage :1884firstRequestMessage.text;1885return message.split('\n')[0].substring(0, 200);1886}18871888private readonly _onDidDispose = this._register(new Emitter<void>());1889readonly onDidDispose = this._onDidDispose.event;18901891private readonly _onDidChange = this._register(new Emitter<IChatChangeEvent>());1892readonly onDidChange = this._onDidChange.event;18931894private readonly _pendingRequests: IChatPendingRequest[] = [];1895private readonly _onDidChangePendingRequests = this._register(new Emitter<void>());1896readonly onDidChangePendingRequests = this._onDidChangePendingRequests.event;18971898private _requests: ChatRequestModel[];18991900private _contributedChatSession: IChatSessionContext | undefined;1901public get contributedChatSession(): IChatSessionContext | undefined {1902return this._contributedChatSession;1903}1904public setContributedChatSession(session: IChatSessionContext | undefined) {1905this._contributedChatSession = session;1906}19071908private _repoData: IExportableRepoData | undefined;1909public get repoData(): IExportableRepoData | undefined {1910return this._repoData;1911}1912public setRepoData(data: IExportableRepoData | undefined): void {1913this._repoData = data;1914}19151916getPendingRequests(): readonly IChatPendingRequest[] {1917return this._pendingRequests;1918}19191920setPendingRequests(requests: readonly { requestId: string; kind: ChatRequestQueueKind }[]): void {1921const existingMap = new Map(this._pendingRequests.map(p => [p.request.id, p]));1922const newPending: IChatPendingRequest[] = [];1923for (const { requestId, kind } of requests) {1924const existing = existingMap.get(requestId);1925if (existing) {1926// Update kind if changed, keep existing request and sendOptions1927newPending.push(existing.kind === kind ? existing : { request: existing.request, kind, sendOptions: existing.sendOptions });1928}1929}1930this._pendingRequests.length = 0;1931this._pendingRequests.push(...newPending);1932this._onDidChangePendingRequests.fire();1933}19341935/**1936* @internal Used by ChatService to add a request to the queue.1937* Steering messages are placed before queued messages.1938*/1939addPendingRequest(request: ChatRequestModel, kind: ChatRequestQueueKind, sendOptions: IChatSendRequestOptions): IChatPendingRequest {1940const pendingRequest: IChatPendingRequest = {1941request,1942kind,1943sendOptions,1944};19451946if (kind === ChatRequestQueueKind.Steering) {1947// Insert after the last steering message, or at the beginning if there is none1948let insertIndex = 0;1949for (let i = 0; i < this._pendingRequests.length; i++) {1950if (this._pendingRequests[i].kind === ChatRequestQueueKind.Steering) {1951insertIndex = i + 1;1952} else {1953break;1954}1955}1956this._pendingRequests.splice(insertIndex, 0, pendingRequest);1957} else {1958// Queued messages always go at the end1959this._pendingRequests.push(pendingRequest);1960}19611962this._onDidChangePendingRequests.fire();1963return pendingRequest;1964}19651966/**1967* @internal Used by ChatService to remove a pending request1968*/1969removePendingRequest(id: string): void {1970const index = this._pendingRequests.findIndex(r => r.request.id === id);1971if (index !== -1) {1972this._pendingRequests.splice(index, 1);1973this._onDidChangePendingRequests.fire();1974}1975}19761977/**1978* @internal Used by ChatService to dequeue the next pending request1979*/1980dequeuePendingRequest(): IChatPendingRequest | undefined {1981const request = this._pendingRequests.shift();1982if (request) {1983this._onDidChangePendingRequests.fire();1984}1985return request;1986}19871988/**1989* @internal Used by ChatService to clear all pending requests1990*/1991clearPendingRequests(): void {1992if (this._pendingRequests.length > 0) {1993this._pendingRequests.length = 0;1994this._onDidChangePendingRequests.fire();1995}1996}19971998readonly lastRequestObs: IObservable<IChatRequestModel | undefined>;19992000// TODO to be clear, this is not the same as the id from the session object, which belongs to the provider.2001// It's easier to be able to identify this model before its async initialization is complete2002private readonly _sessionId: string;2003/** @deprecated Use {@link sessionResource} instead */2004get sessionId(): string {2005return this._sessionId;2006}20072008private readonly _sessionResource: URI;2009get sessionResource(): URI {2010return this._sessionResource;2011}20122013readonly requestInProgress: IObservable<boolean>;2014readonly requestNeedsInput: IObservable<IChatRequestNeedsInputInfo | undefined>;20152016/** Input model for managing input state */2017readonly inputModel: InputModel;20182019get hasRequests(): boolean {2020return this._requests.length > 0;2021}20222023get lastRequest(): ChatRequestModel | undefined {2024return this._requests.at(-1);2025}20262027private _timestamp: number;2028get timestamp(): number {2029return this._timestamp;2030}20312032get timing(): IChatSessionTiming {2033const lastRequest = this._requests.at(-1);2034const lastResponse = lastRequest?.response;2035const lastRequestStarted = lastRequest?.timestamp;2036const lastRequestEnded = lastResponse?.completedAt ?? lastResponse?.timestamp;2037return {2038created: this._timestamp,2039lastRequestStarted,2040lastRequestEnded,2041};2042}20432044get lastMessageDate(): number {2045return this._requests.at(-1)?.timestamp ?? this._timestamp;2046}20472048private get _defaultAgent() {2049return this.chatAgentService.getDefaultAgent(ChatAgentLocation.Chat, ChatModeKind.Ask);2050}20512052private readonly _initialResponderUsername: string | undefined;2053get responderUsername(): string {2054return this._defaultAgent?.fullName ??2055this._initialResponderUsername ?? '';2056}20572058private _isImported = false;2059get isImported(): boolean {2060return this._isImported;2061}20622063private _customTitle: string | undefined;2064get customTitle(): string | undefined {2065return this._customTitle;2066}20672068get title(): string {2069return this._customTitle || ChatModel.getDefaultTitle(this._requests);2070}20712072get hasCustomTitle(): boolean {2073return this._customTitle !== undefined;2074}20752076private _editingSession: IChatEditingSession | undefined;20772078get editingSession(): IChatEditingSession | undefined {2079return this._editingSession;2080}20812082private readonly _initialLocation: ChatAgentLocation;2083get initialLocation(): ChatAgentLocation {2084return this._initialLocation;2085}20862087private readonly _canUseTools: boolean = true;2088get canUseTools(): boolean {2089return this._canUseTools;2090}20912092private _disableBackgroundKeepAlive: boolean;2093get willKeepAlive(): boolean {2094return !this._disableBackgroundKeepAlive;2095}20962097public dataSerializer?: IChatDataSerializerLog;20982099constructor(2100dataRef: ISerializedChatDataReference | undefined,2101initialModelProps: { initialLocation: ChatAgentLocation; canUseTools: boolean; inputState?: ISerializableChatModelInputState; resource?: URI; sessionId?: string; disableBackgroundKeepAlive?: boolean },2102@ILogService private readonly logService: ILogService,2103@IChatAgentService private readonly chatAgentService: IChatAgentService,2104@IChatEditingService private readonly chatEditingService: IChatEditingService,2105@IChatService private readonly chatService: IChatService,2106) {2107super();21082109const initialData = dataRef?.value;2110const isValidExportedData = isExportableSessionData(initialData);2111const isValidFullData = isValidExportedData && isSerializableSessionData(initialData);2112if (initialData && !isValidExportedData) {2113this.logService.warn(`ChatModel#constructor: Loaded malformed session data: ${JSON.stringify(initialData)}`);2114}21152116this._isImported = !!initialData && isValidExportedData && !isValidFullData;2117this._sessionId = (isValidFullData && initialData.sessionId) || initialModelProps.sessionId || generateUuid();2118this._sessionResource = initialModelProps.resource ?? LocalChatSessionUri.forSession(this._sessionId);2119this._disableBackgroundKeepAlive = initialModelProps.disableBackgroundKeepAlive ?? false;21202121this._requests = initialData ? this._deserialize(initialData) : [];2122this._timestamp = (isValidFullData && initialData.creationDate) || Date.now();2123this._customTitle = isValidFullData ? initialData.customTitle : undefined;21242125// Initialize input model from serialized data (undefined for new chats)2126const serializedInputState = initialModelProps.inputState || (isValidFullData && initialData.inputState ? initialData.inputState : undefined);2127this.inputModel = new InputModel(serializedInputState && {2128attachments: serializedInputState.attachments,2129mode: serializedInputState.mode,2130selectedModel: serializedInputState.selectedModel && {2131identifier: serializedInputState.selectedModel.identifier,2132metadata: serializedInputState.selectedModel.metadata2133},2134contrib: serializedInputState.contrib,2135inputText: serializedInputState.inputText,2136selections: serializedInputState.selections2137});21382139this.dataSerializer = dataRef?.serializer;2140this._initialResponderUsername = initialData?.responderUsername;21412142this._repoData = isValidFullData && initialData.repoData ? initialData.repoData : undefined;21432144// Hydrate pending requests from serialized data2145if (isValidFullData && initialData.pendingRequests) {2146this._pendingRequests = this._deserializePendingRequests(initialData.pendingRequests);2147}21482149this._initialLocation = initialData?.initialLocation ?? initialModelProps.initialLocation;21502151this._canUseTools = initialModelProps.canUseTools;21522153this.lastRequestObs = observableFromEvent(this, this.onDidChange, () => this._requests.at(-1));21542155this._register(autorun(reader => {2156const request = this.lastRequestObs.read(reader);2157if (!request?.response) {2158return;2159}21602161reader.store.add(request.response.onDidChange(async ev => {2162if (!this._editingSession || ev.reason !== 'completedRequest') {2163return;2164}21652166this._onDidChange.fire({ kind: 'completedRequest', request });2167}));2168}));21692170this.requestInProgress = this.lastRequestObs.map((request, r) => {2171return request?.response?.isInProgress.read(r) ?? false;2172});21732174this.requestNeedsInput = this.lastRequestObs.map((request, r) => {2175const pendingInfo = request?.response?.isPendingConfirmation.read(r);2176if (!pendingInfo) {2177return undefined;2178}2179return {2180title: this.title,2181detail: pendingInfo.detail,2182};2183});21842185// Retain a reference to itself when a request is in progress, so the ChatModel stays alive in the background2186// only while running a request. TODO also keep it alive for 5min or so so we don't have to dispose/restore too often?2187if (this.initialLocation === ChatAgentLocation.Chat && !initialModelProps.disableBackgroundKeepAlive) {2188const selfRef = this._register(new MutableDisposable<IChatModelReference>());2189this._register(autorun(r => {2190const inProgress = this.requestInProgress.read(r);2191const needsInput = this.requestNeedsInput.read(r);2192const shouldStayAlive = inProgress || !!needsInput;2193if (shouldStayAlive && !selfRef.value) {2194selfRef.value = chatService.getActiveSessionReference(this._sessionResource);2195} else if (!shouldStayAlive && selfRef.value) {2196selfRef.clear();2197}2198}));2199}2200}22012202startEditingSession(isGlobalEditingSession?: boolean, transferFromSession?: IChatEditingSession): void {2203const session = this._editingSession ??= this._register(2204transferFromSession2205? this.chatEditingService.transferEditingSession(this, transferFromSession)2206: isGlobalEditingSession2207? this.chatEditingService.startOrContinueGlobalEditingSession(this)2208: this.chatEditingService.createEditingSession(this)2209);22102211if (!this._disableBackgroundKeepAlive) {2212// todo@connor4312: hold onto a reference so background sessions don't2213// trigger early disposal. This will be cleaned up with the globalization of edits.2214const selfRef = this._register(new MutableDisposable<IChatModelReference>());2215this._register(autorun(r => {2216const hasModified = session.entries.read(r).some(e => e.state.read(r) === ModifiedFileEntryState.Modified);2217if (hasModified && !selfRef.value) {2218selfRef.value = this.chatService.getActiveSessionReference(this._sessionResource);2219} else if (!hasModified && selfRef.value) {2220selfRef.clear();2221}2222}));2223}22242225this._register(autorun(reader => {2226this._setDisabledRequests(session.requestDisablement.read(reader));2227}));2228}22292230private currentEditedFileEvents = new ResourceMap<IChatAgentEditedFileEvent>();2231notifyEditingAction(action: IChatEditingSessionAction): void {2232const state = action.outcome === 'accepted' ? ChatRequestEditedFileEventKind.Keep :2233action.outcome === 'rejected' ? ChatRequestEditedFileEventKind.Undo :2234action.outcome === 'userModified' ? ChatRequestEditedFileEventKind.UserModification : null;2235if (state === null) {2236return;2237}22382239if (!this.currentEditedFileEvents.has(action.uri) || this.currentEditedFileEvents.get(action.uri)?.eventKind === ChatRequestEditedFileEventKind.Keep) {2240this.currentEditedFileEvents.set(action.uri, { eventKind: state, uri: action.uri });2241}2242}22432244private _deserialize(obj: IExportableChatData | ISerializedChatDataReference): ChatRequestModel[] {2245const requests = hasKey(obj, { serializer: true }) ? obj.value.requests : obj.requests;2246if (!Array.isArray(requests)) {2247this.logService.error(`Ignoring malformed session data: ${JSON.stringify(obj)}`);2248return [];2249}22502251try {2252return requests.map(r => this._deserializeRequest(r));2253} catch (error) {2254this.logService.error('Failed to parse chat data', error);2255return [];2256}2257}22582259private _deserializeRequest(raw: ISerializableChatRequestData): ChatRequestModel {2260const parsedRequest =2261typeof raw.message === 'string'2262? this.getParsedRequestFromString(raw.message)2263: reviveParsedChatRequest(raw.message);22642265// Old messages don't have variableData, or have it in the wrong (non-array) shape2266const variableData: IChatRequestVariableData = this.reviveVariableData(raw.variableData);2267const request = new ChatRequestModel({2268session: this,2269message: parsedRequest,2270variableData,2271timestamp: raw.timestamp ?? -1,2272restoredId: raw.requestId,2273confirmation: raw.confirmation,2274editedFileEvents: raw.editedFileEvents,2275modelId: raw.modelId,2276});2277request.shouldBeRemovedOnSend = raw.isHidden ? { requestId: raw.requestId } : raw.shouldBeRemovedOnSend;2278// eslint-disable-next-line @typescript-eslint/no-explicit-any, local/code-no-any-casts2279if (raw.response || raw.result || (raw as any).responseErrorDetails) {2280const agent = (raw.agent && 'metadata' in raw.agent) ? // Check for the new format, ignore entries in the old format2281reviveSerializedAgent(raw.agent) : undefined;22822283// Port entries from old format2284const result = 'responseErrorDetails' in raw ?2285// eslint-disable-next-line local/code-no-dangerous-type-assertions2286{ errorDetails: raw.responseErrorDetails } as IChatAgentResult : raw.result;2287let modelState = raw.modelState || { value: raw.isCanceled ? ResponseModelState.Cancelled : ResponseModelState.Complete, completedAt: Date.now() };2288if (modelState.value === ResponseModelState.Pending || modelState.value === ResponseModelState.NeedsInput) {2289modelState = { value: ResponseModelState.Cancelled, completedAt: Date.now() };2290}22912292request.response = new ChatResponseModel({2293responseContent: raw.response ?? [new MarkdownString(raw.response)],2294session: this,2295agent,2296slashCommand: raw.slashCommand,2297requestId: request.id,2298modelState,2299vote: raw.vote,2300timestamp: raw.timestamp,2301voteDownReason: raw.voteDownReason,2302result,2303followups: raw.followups,2304restoredId: raw.responseId,2305timeSpentWaiting: raw.timeSpentWaiting,2306shouldBeBlocked: request.shouldBeBlocked.get(),2307codeBlockInfos: raw.responseMarkdownInfo?.map<ICodeBlockInfo>(info => ({ suggestionId: info.suggestionId })),2308});2309request.response.shouldBeRemovedOnSend = raw.isHidden ? { requestId: raw.requestId } : raw.shouldBeRemovedOnSend;2310if (raw.usedContext) { // @ulugbekna: if this's a new vscode sessions, doc versions are incorrect anyway?2311request.response.applyReference(revive(raw.usedContext));2312}23132314raw.contentReferences?.forEach(r => request.response!.applyReference(revive(r)));2315raw.codeCitations?.forEach(c => request.response!.applyCodeCitation(revive(c)));2316}2317return request;2318}23192320private reviveVariableData(raw: IChatRequestVariableData): IChatRequestVariableData {2321const variableData = raw && Array.isArray(raw.variables)2322? raw :2323{ variables: [] };23242325variableData.variables = variableData.variables.map<IChatRequestVariableEntry>(IChatRequestVariableEntry.fromExport);23262327return variableData;2328}23292330private getParsedRequestFromString(message: string): IParsedChatRequest {2331// TODO These offsets won't be used, but chat replies need to go through the parser as well2332const parts = [new ChatRequestTextPart(new OffsetRange(0, message.length), { startColumn: 1, startLineNumber: 1, endColumn: 1, endLineNumber: 1 }, message)];2333return {2334text: message,2335parts2336};2337}23382339/**2340* Hydrates pending requests from serialized data.2341* For each serialized pending request, finds the matching request model and adds it to the pending queue.2342*/2343private _deserializePendingRequests(pendingRequests: ISerializablePendingRequestData[]): IChatPendingRequest[] {2344try {2345return pendingRequests.map(pending => ({2346id: pending.id,2347request: this._deserializeRequest(pending.request),2348kind: pending.kind,2349sendOptions: {2350...pending.sendOptions,2351userSelectedTools: pending.sendOptions.userSelectedTools2352? constObservable(pending.sendOptions.userSelectedTools)2353: undefined,2354}2355}));2356} catch (e) {2357this.logService.error('Failed to parse pending chat requests', e);2358return [];2359}2360}2361236223632364getRequests(): ChatRequestModel[] {2365return this._requests;2366}23672368resetCheckpoint(): void {2369for (const request of this._requests) {2370request.setShouldBeBlocked(false);2371if (request.response) {2372request.response.setBlockedState(false);2373}2374}2375}23762377setCheckpoint(requestId: string | undefined) {2378let checkpoint: ChatRequestModel | undefined;2379let checkpointIndex = -1;2380if (requestId !== undefined) {2381this._requests.forEach((request, index) => {2382if (request.id === requestId) {2383checkpointIndex = index;2384checkpoint = request;2385request.setShouldBeBlocked(true);2386}2387});23882389if (!checkpoint) {2390return; // Invalid request ID2391}2392}23932394for (let i = this._requests.length - 1; i >= 0; i -= 1) {2395const request = this._requests[i];2396if (this._checkpoint && !checkpoint) {2397request.setShouldBeBlocked(false);2398if (request.response) {2399request.response.setBlockedState(false);2400}2401} else if (checkpoint && i >= checkpointIndex) {2402request.setShouldBeBlocked(true);2403if (request.response) {2404request.response.setBlockedState(true);2405}2406} else if (checkpoint && i < checkpointIndex) {2407request.setShouldBeBlocked(false);2408if (request.response) {2409request.response.setBlockedState(false);2410}2411}2412}24132414this._checkpoint = checkpoint;2415}24162417private _checkpoint: ChatRequestModel | undefined = undefined;2418public get checkpoint() {2419return this._checkpoint;2420}24212422private _setDisabledRequests(requestIds: IChatRequestDisablement[]) {2423this._requests.forEach((request) => {2424const shouldBeRemovedOnSend = requestIds.find(r => r.requestId === request.id);2425request.shouldBeRemovedOnSend = shouldBeRemovedOnSend;2426if (request.response) {2427request.response.shouldBeRemovedOnSend = shouldBeRemovedOnSend;2428}2429});24302431this._onDidChange.fire({ kind: 'setHidden' });2432}24332434addRequest(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 {2435const editedFileEvents = [...this.currentEditedFileEvents.values()];2436this.currentEditedFileEvents.clear();2437const request = new ChatRequestModel({2438restoredId: id,2439session: this,2440message,2441variableData,2442timestamp: Date.now(),2443attempt,2444modeInfo,2445confirmation,2446locationData,2447attachedContext: attachments,2448isCompleteAddedRequest,2449modelId,2450editedFileEvents: editedFileEvents.length ? editedFileEvents : undefined,2451userSelectedTools,2452});2453request.response = new ChatResponseModel({2454responseContent: [],2455session: this,2456agent: chatAgent,2457slashCommand,2458requestId: request.id,2459isCompleteAddedRequest,2460codeBlockInfos: undefined,2461});24622463this._requests.push(request);2464this._onDidChange.fire({ kind: 'addRequest', request });2465return request;2466}24672468public setCustomTitle(title: string): void {2469this._customTitle = title;2470this._onDidChange.fire({ kind: 'setCustomTitle', title });2471}24722473updateRequest(request: ChatRequestModel, variableData: IChatRequestVariableData) {2474request.variableData = variableData;2475this._onDidChange.fire({ kind: 'changedRequest', request });2476}24772478adoptRequest(request: ChatRequestModel): void {2479// this doesn't use `removeRequest` because it must not dispose the request object2480const oldOwner = request.session;2481const index = oldOwner._requests.findIndex((candidate: ChatRequestModel) => candidate.id === request.id);24822483if (index === -1) {2484return;2485}24862487oldOwner._requests.splice(index, 1);24882489request.adoptTo(this);2490request.response?.adoptTo(this);2491this._requests.push(request);24922493oldOwner._onDidChange.fire({ kind: 'removeRequest', requestId: request.id, responseId: request.response?.id, reason: ChatRequestRemovalReason.Adoption });2494this._onDidChange.fire({ kind: 'addRequest', request });2495}24962497acceptResponseProgress(request: ChatRequestModel, progress: IChatProgress, quiet?: boolean): void {2498if (!request.response) {2499request.response = new ChatResponseModel({2500responseContent: [],2501session: this,2502requestId: request.id,2503codeBlockInfos: undefined,2504});2505}25062507if (request.response.isComplete) {2508throw new Error('acceptResponseProgress: Adding progress to a completed response');2509}25102511if (progress.kind === 'usedContext' || progress.kind === 'reference') {2512request.response.applyReference(progress);2513} else if (progress.kind === 'codeCitation') {2514request.response.applyCodeCitation(progress);2515} else if (progress.kind === 'move') {2516this._onDidChange.fire({ kind: 'move', target: progress.uri, range: progress.range });2517} else if (progress.kind === 'codeblockUri' && progress.isEdit) {2518request.response.addUndoStop({ id: progress.undoStopId ?? generateUuid(), kind: 'undoStop' });2519request.response.updateContent(progress, quiet);2520} else if (progress.kind === 'progressTaskResult') {2521// Should have been handled upstream, not sent to model2522this.logService.error(`Couldn't handle progress: ${JSON.stringify(progress)}`);2523} else {2524request.response.updateContent(progress, quiet);2525}2526}25272528removeRequest(id: string, reason: ChatRequestRemovalReason = ChatRequestRemovalReason.Removal): void {2529const index = this._requests.findIndex(request => request.id === id);2530const request = this._requests[index];25312532if (index !== -1) {2533this._onDidChange.fire({ kind: 'removeRequest', requestId: request.id, responseId: request.response?.id, reason });2534this._requests.splice(index, 1);2535request.response?.dispose();2536}2537}25382539cancelRequest(request: ChatRequestModel): void {2540if (request.response) {2541request.response.cancel();2542}2543}25442545setResponse(request: ChatRequestModel, result: IChatAgentResult): void {2546if (!request.response) {2547request.response = new ChatResponseModel({2548responseContent: [],2549session: this,2550requestId: request.id,2551codeBlockInfos: undefined,2552});2553}25542555request.response.setResult(result);2556}25572558setFollowups(request: ChatRequestModel, followups: IChatFollowup[] | undefined): void {2559if (!request.response) {2560// Maybe something went wrong?2561return;2562}2563request.response.setFollowups(followups);2564}25652566setResponseModel(request: ChatRequestModel, response: ChatResponseModel): void {2567request.response = response;2568this._onDidChange.fire({ kind: 'addResponse', response });2569}25702571toExport(): IExportableChatData {2572return {2573responderUsername: this.responderUsername,2574initialLocation: this.initialLocation,2575requests: this._requests.map((r): ISerializableChatRequestData => {2576const message = {2577...r.message,2578// eslint-disable-next-line @typescript-eslint/no-explicit-any2579parts: r.message.parts.map((p: any) => p && 'toJSON' in p ? (p.toJSON as Function)() : p)2580};2581const agent = r.response?.agent;2582const agentJson = agent && 'toJSON' in agent ? (agent.toJSON as Function)() :2583agent ? { ...agent } : undefined;2584return {2585requestId: r.id,2586message,2587variableData: IChatRequestVariableData.toExport(r.variableData),2588response: r.response ?2589r.response.entireResponse.value.map(item => {2590// Keeping the shape of the persisted data the same for back compat2591if (item.kind === 'treeData') {2592return item.treeData;2593} else if (item.kind === 'markdownContent') {2594return item.content;2595} else {2596// eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any2597return item as any; // TODO2598}2599})2600: undefined,2601shouldBeRemovedOnSend: r.shouldBeRemovedOnSend,2602agent: agentJson,2603timestamp: r.timestamp,2604confirmation: r.confirmation,2605editedFileEvents: r.editedFileEvents,2606modelId: r.modelId,2607...r.response?.toJSON(),2608};2609}),2610};2611}26122613toJSON(): ISerializableChatData {2614return {2615version: 3,2616...this.toExport(),2617sessionId: this.sessionId,2618creationDate: this._timestamp,2619customTitle: this._customTitle,2620inputState: this.inputModel.toJSON(),2621repoData: this._repoData,2622};2623}26242625override dispose() {2626this._requests.forEach(r => r.response?.dispose());2627this._onDidDispose.fire();26282629super.dispose();2630}2631}26322633export function updateRanges(variableData: IChatRequestVariableData, diff: number): IChatRequestVariableData {2634return {2635variables: variableData.variables.map(v => ({2636...v,2637range: v.range && {2638start: v.range.start - diff,2639endExclusive: v.range.endExclusive - diff2640}2641}))2642};2643}26442645export function canMergeMarkdownStrings(md1: IMarkdownString, md2: IMarkdownString): boolean {2646if (md1.baseUri && md2.baseUri) {2647const baseUriEquals = md1.baseUri.scheme === md2.baseUri.scheme2648&& md1.baseUri.authority === md2.baseUri.authority2649&& md1.baseUri.path === md2.baseUri.path2650&& md1.baseUri.query === md2.baseUri.query2651&& md1.baseUri.fragment === md2.baseUri.fragment;2652if (!baseUriEquals) {2653return false;2654}2655} else if (md1.baseUri || md2.baseUri) {2656return false;2657}26582659return equals(md1.isTrusted, md2.isTrusted) &&2660md1.supportHtml === md2.supportHtml &&2661md1.supportThemeIcons === md2.supportThemeIcons;2662}26632664export function appendMarkdownString(md1: IMarkdownString, md2: IMarkdownString | string): IMarkdownString {2665const appendedValue = typeof md2 === 'string' ? md2 : md2.value;2666return {2667value: md1.value + appendedValue,2668isTrusted: md1.isTrusted,2669supportThemeIcons: md1.supportThemeIcons,2670supportHtml: md1.supportHtml,2671baseUri: md1.baseUri2672};2673}26742675export function getCodeCitationsMessage(citations: ReadonlyArray<IChatCodeCitation>): string {2676if (citations.length === 0) {2677return '';2678}26792680const licenseTypes = citations.reduce((set, c) => set.add(c.license), new Set<string>());2681const label = licenseTypes.size === 1 ?2682localize('codeCitation', "Similar code found with 1 license type", licenseTypes.size) :2683localize('codeCitations', "Similar code found with {0} license types", licenseTypes.size);2684return label;2685}26862687/**2688* Converts IChatSendRequestOptions to a serializable format by extracting only2689* serializable fields and converting observables to static values.2690*/2691export function serializeSendOptions(options: IChatSendRequestOptions): ISerializableSendOptions {2692return {2693modeInfo: options.modeInfo,2694userSelectedModelId: options.userSelectedModelId,2695userSelectedTools: options.userSelectedTools?.get(),2696location: options.location,2697locationData: options.locationData,2698attempt: options.attempt,2699noCommandDetection: options.noCommandDetection,2700agentId: options.agentId,2701agentIdSilent: options.agentIdSilent,2702slashCommand: options.slashCommand,2703confirmation: options.confirmation,2704};2705}27062707export enum ChatRequestEditedFileEventKind {2708Keep = 1,2709Undo = 2,2710UserModification = 3,2711}27122713export interface IChatAgentEditedFileEvent {2714readonly uri: URI;2715readonly eventKind: ChatRequestEditedFileEventKind;2716}27172718/** URI for a resource embedded in a chat request/response */2719export namespace ChatResponseResource {2720export const scheme = 'vscode-chat-response-resource';27212722export function createUri(sessionResource: URI, toolCallId: string, index: number, basename?: string): URI {2723return URI.from({2724scheme: ChatResponseResource.scheme,2725authority: encodeHex(VSBuffer.fromString(sessionResource.toString())),2726path: `/tool/${toolCallId}/${index}` + (basename ? `/${basename}` : ''),2727});2728}27292730export function parseUri(uri: URI): undefined | { sessionResource: URI; toolCallId: string; index: number } {2731if (uri.scheme !== ChatResponseResource.scheme) {2732return undefined;2733}27342735const parts = uri.path.split('/');2736if (parts.length < 5) {2737return undefined;2738}27392740const [, kind, toolCallId, index] = parts;2741if (kind !== 'tool') {2742return undefined;2743}27442745let sessionResource: URI;2746try {2747sessionResource = URI.parse(decodeHex(uri.authority).toString());2748} catch (e) {2749if (e instanceof SyntaxError) { // pre-1.108 local session ID2750sessionResource = LocalChatSessionUri.forSession(uri.authority);2751} else {2752throw e;2753}2754}27552756return {2757sessionResource,2758toolCallId: toolCallId,2759index: Number(index),2760};2761}2762}276327642765