Path: blob/main/src/vs/workbench/contrib/chat/common/model/chatModel.ts
5252 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 === 'questionCarousel' && !part.isUsed) {1153return localize('waitingAnswer', "Answer questions to continue...");1154}1155if (part.kind === 'elicitation2' && part.state.read(r) === ElicitationState.Pending) {1156const title = part.title;1157return isMarkdownString(title) ? title.value : title;1158}1159}11601161return undefined;1162});11631164const _startedWaitingAt = _pendingInfo.map(p => !!p).map(p => p ? Date.now() : undefined);1165this.isPendingConfirmation = _startedWaitingAt.map((waiting, r) => waiting ? { startedWaitingAt: waiting, detail: _pendingInfo.read(r) } : undefined);11661167this.isInProgress = signal.map((_value, r) => {11681169signal.read(r);11701171return !_pendingInfo.read(r)1172&& !this.shouldBeRemovedOnSend1173&& (this._modelState.read(r).value === ResponseModelState.Pending || this._modelState.read(r).value === ResponseModelState.NeedsInput);1174});11751176this._register(this._response.onDidChangeValue(() => this._onDidChange.fire(defaultChatResponseModelChangeReason)));1177this.id = params.restoredId ?? 'response_' + generateUuid();11781179let lastStartedWaitingAt: number | undefined = undefined;1180this.confirmationAdjustedTimestamp = derived(reader => {1181const pending = this.isPendingConfirmation.read(reader);1182if (pending) {1183this._modelState.set({ value: ResponseModelState.NeedsInput }, undefined);1184if (!lastStartedWaitingAt) {1185lastStartedWaitingAt = pending.startedWaitingAt;1186}1187} else if (lastStartedWaitingAt) {1188// Restore state to Pending if it was set to NeedsInput by this observable1189if (this._modelState.read(reader).value === ResponseModelState.NeedsInput) {1190this._modelState.set({ value: ResponseModelState.Pending }, undefined);1191}1192this._timeSpentWaitingAccumulator += Date.now() - lastStartedWaitingAt;1193lastStartedWaitingAt = undefined;1194}11951196return this._timestamp + this._timeSpentWaitingAccumulator;1197}).recomputeInitiallyAndOnChange(this._store);1198}11991200initializeCodeBlockInfos(codeBlockInfo: ICodeBlockInfo[]): void {1201if (this._codeBlockInfos) {1202throw new BugIndicatingError('Code block infos have already been initialized');1203}1204this._codeBlockInfos = [...codeBlockInfo];1205}12061207setBlockedState(isBlocked: boolean): void {1208this._shouldBeBlocked.set(isBlocked, undefined);1209}12101211/**1212* Apply a progress update to the actual response content.1213*/1214updateContent(responsePart: IChatProgressResponseContent | IChatTextEdit | IChatNotebookEdit | IChatExternalToolInvocationUpdate, quiet?: boolean) {1215this._response.updateContent(responsePart, quiet);1216}12171218/**1219* Adds an undo stop at the current position in the stream.1220*/1221addUndoStop(undoStop: IChatUndoStop) {1222this._onDidChange.fire({ reason: 'undoStop', id: undoStop.id });1223this._response.updateContent(undoStop, true);1224}12251226/**1227* Apply one of the progress updates that are not part of the actual response content.1228*/1229applyReference(progress: IChatUsedContext | IChatContentReference) {1230if (progress.kind === 'usedContext') {1231this._usedContext = progress;1232} else if (progress.kind === 'reference') {1233this._contentReferences.push(progress);1234this._onDidChange.fire(defaultChatResponseModelChangeReason);1235}1236}12371238applyCodeCitation(progress: IChatCodeCitation) {1239this._codeCitations.push(progress);1240this._response.addCitation(progress);1241this._onDidChange.fire(defaultChatResponseModelChangeReason);1242}12431244setAgent(agent: IChatAgentData, slashCommand?: IChatAgentCommand) {1245this._agent = agent;1246this._slashCommand = slashCommand;1247this._agentOrSlashCommandDetected = !agent.isDefault || !!slashCommand;1248this._onDidChange.fire(defaultChatResponseModelChangeReason);1249}12501251setResult(result: IChatAgentResult): void {1252this._result = result;1253this._onDidChange.fire(defaultChatResponseModelChangeReason);1254}12551256setUsage(usage: IChatUsage): void {1257this._usage = usage;1258this._onDidChange.fire(defaultChatResponseModelChangeReason);1259}12601261complete(): void {1262// No-op if it's already complete1263if (this.isComplete) {1264return;1265}1266if (this._result?.errorDetails?.responseIsRedacted) {1267this._response.clear();1268}12691270// Canceled sessions can be considered 'Complete'1271const state = !!this._result?.errorDetails && this._result.errorDetails.code !== 'canceled' ? ResponseModelState.Failed : ResponseModelState.Complete;1272this._modelState.set({ value: state, completedAt: Date.now() }, undefined);1273this._onDidChange.fire({ reason: 'completedRequest' });1274}12751276cancel(): void {1277this._modelState.set({ value: ResponseModelState.Cancelled, completedAt: Date.now() }, undefined);1278this._onDidChange.fire({ reason: 'completedRequest' });1279}12801281setFollowups(followups: IChatFollowup[] | undefined): void {1282this._followups = followups;1283this._onDidChange.fire(defaultChatResponseModelChangeReason); // Fire so that command followups get rendered on the row1284}12851286setVote(vote: ChatAgentVoteDirection): void {1287this._vote = vote;1288this._onDidChange.fire(defaultChatResponseModelChangeReason);1289}12901291setVoteDownReason(reason: ChatAgentVoteDownReason | undefined): void {1292this._voteDownReason = reason;1293this._onDidChange.fire(defaultChatResponseModelChangeReason);1294}12951296setEditApplied(edit: IChatTextEditGroup, editCount: number): boolean {1297if (!this.response.value.includes(edit)) {1298return false;1299}1300if (!edit.state) {1301return false;1302}1303edit.state.applied = editCount; // must not be edit.edits.length1304this._onDidChange.fire(defaultChatResponseModelChangeReason);1305return true;1306}13071308adoptTo(session: ChatModel) {1309this._session = session;1310this._onDidChange.fire(defaultChatResponseModelChangeReason);1311}131213131314finalizeUndoState(): void {1315this._finalizedResponse = this.response;1316this._responseView = undefined;1317this._shouldBeRemovedOnSend = undefined;1318}13191320toJSON(): ISerializableChatResponseData {1321const modelState = this._modelState.get();1322const pendingConfirmation = this.isPendingConfirmation.get();13231324return {1325responseId: this.id,1326result: this.result,1327responseMarkdownInfo: this.codeBlockInfos?.map<ISerializableMarkdownInfo>(info => ({ suggestionId: info.suggestionId })),1328followups: this.followups,1329modelState: modelState.value === ResponseModelState.Pending || modelState.value === ResponseModelState.NeedsInput ? { value: ResponseModelState.Cancelled, completedAt: Date.now() } : modelState,1330vote: this.vote,1331voteDownReason: this.voteDownReason,1332slashCommand: this.slashCommand,1333usedContext: this.usedContext,1334contentReferences: this.contentReferences,1335codeCitations: this.codeCitations,1336timestamp: this._timestamp,1337timeSpentWaiting: (pendingConfirmation ? Date.now() - pendingConfirmation.startedWaitingAt : 0) + this._timeSpentWaitingAccumulator,1338} satisfies WithDefinedProps<ISerializableChatResponseData>;1339}1340}134113421343export interface IChatRequestDisablement {1344requestId: string;1345afterUndoStop?: string;1346}13471348/**1349* Information about a chat request that needs user input to continue.1350*/1351export interface IChatRequestNeedsInputInfo {1352/** The chat session title */1353readonly title: string;1354/** Optional detail message, e.g., "<toolname> needs approval to run." */1355readonly detail?: string;1356}13571358export interface IChatModel extends IDisposable {1359readonly onDidDispose: Event<void>;1360readonly onDidChange: Event<IChatChangeEvent>;1361/** @deprecated Use {@link sessionResource} instead */1362readonly sessionId: string;1363/** Milliseconds timestamp this chat model was created. */1364readonly timestamp: number;1365readonly timing: IChatSessionTiming;1366readonly sessionResource: URI;1367readonly initialLocation: ChatAgentLocation;1368readonly title: string;1369readonly hasCustomTitle: boolean;1370readonly responderUsername: string;1371/** True whenever a request is currently running */1372readonly requestInProgress: IObservable<boolean>;1373/** Provides session information when a request needs user interaction to continue */1374readonly requestNeedsInput: IObservable<IChatRequestNeedsInputInfo | undefined>;1375readonly inputPlaceholder?: string;1376readonly editingSession?: IChatEditingSession | undefined;1377readonly checkpoint: IChatRequestModel | undefined;1378startEditingSession(isGlobalEditingSession?: boolean, transferFromSession?: IChatEditingSession): void;1379/** Input model for managing input state */1380readonly inputModel: IInputModel;1381readonly hasRequests: boolean;1382readonly lastRequest: IChatRequestModel | undefined;1383/** Whether this model will be kept alive while it is running or has edits */1384readonly willKeepAlive: boolean;1385readonly lastRequestObs: IObservable<IChatRequestModel | undefined>;1386getRequests(): IChatRequestModel[];1387setCheckpoint(requestId: string | undefined): void;13881389toExport(): IExportableChatData;1390toJSON(): ISerializableChatData;1391readonly contributedChatSession: IChatSessionContext | undefined;13921393readonly repoData: IExportableRepoData | undefined;1394setRepoData(data: IExportableRepoData | undefined): void;13951396readonly onDidChangePendingRequests: Event<void>;1397getPendingRequests(): readonly IChatPendingRequest[];1398}13991400export interface ISerializableChatsData {1401[sessionId: string]: ISerializableChatData;1402}14031404export type ISerializableChatAgentData = UriDto<IChatAgentData>;14051406interface ISerializableChatResponseData {1407responseId?: string;1408result?: IChatAgentResult; // Optional for backcompat1409responseMarkdownInfo?: ISerializableMarkdownInfo[];1410followups?: ReadonlyArray<IChatFollowup>;1411modelState?: ResponseModelStateT;1412vote?: ChatAgentVoteDirection;1413voteDownReason?: ChatAgentVoteDownReason;1414timestamp?: number;1415slashCommand?: IChatAgentCommand;1416/** For backward compat: should be optional */1417usedContext?: IChatUsedContext;1418contentReferences?: ReadonlyArray<IChatContentReference>;1419codeCitations?: ReadonlyArray<IChatCodeCitation>;1420timeSpentWaiting?: number;1421}14221423export type SerializedChatResponsePart = IMarkdownString | IChatResponseProgressFileTreeData | IChatContentInlineReference | IChatAgentMarkdownContentWithVulnerability | IChatThinkingPart | IChatProgressResponseContentSerialized | IChatQuestionCarousel;14241425export interface ISerializableChatRequestData extends ISerializableChatResponseData {1426requestId: string;1427message: string | IParsedChatRequest; // string => old format1428/** Is really like "prompt data". This is the message in the format in which the agent gets it + variable values. */1429variableData: IChatRequestVariableData;1430response: ReadonlyArray<SerializedChatResponsePart> | undefined;14311432/**Old, persisted name for shouldBeRemovedOnSend */1433isHidden?: boolean;1434shouldBeRemovedOnSend?: IChatRequestDisablement;1435agent?: ISerializableChatAgentData;1436// responseErrorDetails: IChatResponseErrorDetails | undefined;1437/** @deprecated modelState is used instead now */1438isCanceled?: boolean;1439timestamp?: number;1440confirmation?: string;1441editedFileEvents?: IChatAgentEditedFileEvent[];1442modelId?: string;1443}14441445export interface ISerializableMarkdownInfo {1446readonly suggestionId: EditSuggestionId;1447}14481449/**1450* Repository state captured for chat session export.1451* Enables reproducing the workspace state by cloning, checking out the commit, and applying diffs.1452*/1453export interface IExportableRepoData {1454/**1455* Classification of the workspace's version control state.1456* - `remote-git`: Git repo with a configured remote URL1457* - `local-git`: Git repo without any remote (local only)1458* - `plain-folder`: Not a git repository1459*/1460workspaceType: 'remote-git' | 'local-git' | 'plain-folder';14611462/**1463* Sync status between local and remote.1464* - `synced`: Local HEAD matches remote tracking branch (fully pushed)1465* - `unpushed`: Local has commits not pushed to the remote tracking branch1466* - `unpublished`: Local branch has no remote tracking branch configured1467* - `local-only`: No remote configured (local git repo only)1468* - `no-git`: Not a git repository1469*/1470syncStatus: 'synced' | 'unpushed' | 'unpublished' | 'local-only' | 'no-git';14711472/**1473* Remote URL of the repository (e.g., https://github.com/org/repo.git).1474* Undefined if no remote is configured.1475*/1476remoteUrl?: string;14771478/**1479* Vendor/host of the remote repository.1480* Undefined if no remote is configured.1481*/1482remoteVendor?: 'github' | 'ado' | 'other';14831484/**1485* Remote tracking branch for the current branch (e.g., "origin/feature/my-work").1486* Undefined if branch is unpublished or no remote.1487*/1488remoteTrackingBranch?: string;14891490/**1491* Default remote branch used as base for unpublished branches (e.g., "origin/main").1492* Helpful for computing merge-base when branch has no tracking.1493*/1494remoteBaseBranch?: string;14951496/**1497* Commit hash of the remote tracking branch HEAD.1498* Undefined if branch has no remote tracking branch.1499*/1500remoteHeadCommit?: string;15011502/**1503* Name of the current local branch (e.g., "feature/my-work").1504*/1505localBranch?: string;15061507/**1508* Commit hash of the local HEAD when captured.1509*/1510localHeadCommit?: string;15111512/**1513* Working tree diffs (uncommitted changes).1514*/1515diffs?: IExportableRepoDiff[];15161517/**1518* Status of the diffs collection.1519* - `included`: Diffs were successfully captured and included1520* - `tooManyChanges`: Diffs skipped because >100 files changed (degenerate case like mass renames)1521* - `tooLarge`: Diffs skipped because total size exceeded 900KB1522* - `trimmedForStorage`: Diffs were trimmed to save storage (older session)1523* - `noChanges`: No working tree changes detected1524* - `notCaptured`: Diffs not captured (default/undefined case)1525*/1526diffsStatus?: 'included' | 'tooManyChanges' | 'tooLarge' | 'trimmedForStorage' | 'noChanges' | 'notCaptured';15271528/**1529* Number of changed files detected, even if diffs were not included.1530*/1531changedFileCount?: number;1532}15331534/**1535* A file change exported as a unified diff patch compatible with `git apply`.1536*/1537export interface IExportableRepoDiff {1538relativePath: string;1539changeType: 'added' | 'modified' | 'deleted' | 'renamed';1540oldRelativePath?: string;1541unifiedDiff?: string;1542status: string;1543}15441545export interface IExportableChatData {1546initialLocation: ChatAgentLocation | undefined;1547requests: ISerializableChatRequestData[];1548responderUsername: string;1549}15501551/*1552NOTE: 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.1553*/15541555export interface ISerializableChatData1 extends IExportableChatData {1556sessionId: string;1557creationDate: number;1558}15591560export interface ISerializableChatData2 extends ISerializableChatData1 {1561version: 2;1562computedTitle: string | undefined;1563}15641565export interface ISerializableChatData3 extends Omit<ISerializableChatData2, 'version' | 'computedTitle'> {1566version: 3;1567customTitle: string | undefined;1568/**1569* Whether the session had pending edits when it was stored.1570* todo@connor4312 This will be cleaned up with the globalization of edits.1571*/1572hasPendingEdits?: boolean;1573/** Current draft input state (added later, fully backwards compatible) */1574inputState?: ISerializableChatModelInputState;1575repoData?: IExportableRepoData;1576/** Pending requests that were queued but not yet processed */1577pendingRequests?: ISerializablePendingRequestData[];1578}15791580/**1581* Input model for managing chat input state independently from the chat model.1582* This keeps display logic separated from the core chat model.1583*1584* The input model:1585* - Manages the current draft state (text, attachments, mode, model selection, cursor/selection)1586* - Provides an observable interface for reactive UI updates1587* - Automatically persists through the chat model's serialization1588* - Enables bidirectional sync between the UI (ChatInputPart) and the model1589* - Uses `undefined` state to indicate no persisted state (new/empty chat)1590*1591* This architecture ensures that:1592* - Input state is preserved when moving chats between editor/sidebar/window1593* - No manual state transfer is needed when switching contexts1594* - The UI stays in sync with the persisted state1595* - New chats use UI defaults (persisted preferences) instead of hardcoded values1596*/1597export interface IInputModel {1598/** Observable for current input state (undefined for new/uninitialized chats) */1599readonly state: IObservable<IChatModelInputState | undefined>;16001601/** Update the input state (partial update) */1602setState(state: Partial<IChatModelInputState>): void;16031604/** Clear input state (after sending or clearing) */1605clearState(): void;16061607/** Serializes the state */1608toJSON(): ISerializableChatModelInputState | undefined;1609}16101611/**1612* Represents the current state of the chat input that hasn't been sent yet.1613* This is the "draft" state that should be preserved across sessions.1614*/1615export interface IChatModelInputState {1616/** Current attachments in the input */1617attachments: readonly IChatRequestVariableEntry[];16181619/** Currently selected chat mode */1620mode: {1621/** Mode ID (e.g., 'ask', 'edit', 'agent', or custom mode ID) */1622id: string;1623/** Mode kind for builtin modes */1624kind: ChatModeKind | undefined;1625};16261627/** Currently selected language model, if any */1628selectedModel: ILanguageModelChatMetadataAndIdentifier | undefined;16291630/** Current input text */1631inputText: string;16321633/** Current selection ranges */1634selections: ISelection[];16351636/** Contributed stored state */1637contrib: Record<string, unknown>;1638}16391640/**1641* Serializable version of IChatModelInputState1642*/1643export interface ISerializableChatModelInputState {1644attachments: readonly IChatRequestVariableEntry[];1645mode: {1646id: string;1647kind: ChatModeKind | undefined;1648};1649selectedModel: {1650identifier: string;1651metadata: ILanguageModelChatMetadata;1652} | undefined;1653inputText: string;1654selections: ISelection[];1655contrib: Record<string, unknown>;1656}16571658/**1659* Chat data that has been parsed and normalized to the current format.1660*/1661export type ISerializableChatData = ISerializableChatData3;16621663export type IChatDataSerializerLog = ObjectMutationLog<IChatModel, ISerializableChatData>;16641665export interface ISerializedChatDataReference {1666value: ISerializableChatData | IExportableChatData;1667serializer: IChatDataSerializerLog;1668}16691670/**1671* Chat data that has been loaded but not normalized, and could be any format1672*/1673export type ISerializableChatDataIn = ISerializableChatData1 | ISerializableChatData2 | ISerializableChatData3;16741675/**1676* Normalize chat data from storage to the current format.1677* TODO- ChatModel#_deserialize and reviveSerializedAgent also still do some normalization and maybe that should be done in here too.1678*/1679export function normalizeSerializableChatData(raw: ISerializableChatDataIn): ISerializableChatData {1680normalizeOldFields(raw);16811682if (!('version' in raw)) {1683return {1684version: 3,1685...raw,1686customTitle: undefined,1687};1688}16891690if (raw.version === 2) {1691return {1692...raw,1693version: 3,1694customTitle: raw.computedTitle1695};1696}16971698return raw;1699}17001701function normalizeOldFields(raw: ISerializableChatDataIn): void {1702// Fill in fields that very old chat data may be missing1703if (!raw.sessionId) {1704raw.sessionId = generateUuid();1705}17061707if (!raw.creationDate) {1708raw.creationDate = getLastYearDate();1709}17101711// eslint-disable-next-line @typescript-eslint/no-explicit-any, local/code-no-any-casts1712if ((raw.initialLocation as any) === 'editing-session') {1713raw.initialLocation = ChatAgentLocation.Chat;1714}1715}17161717function getLastYearDate(): number {1718const lastYearDate = new Date();1719lastYearDate.setFullYear(lastYearDate.getFullYear() - 1);1720return lastYearDate.getTime();1721}17221723export function isExportableSessionData(obj: unknown): obj is IExportableChatData {1724return !!obj &&1725Array.isArray((obj as IExportableChatData).requests) &&1726typeof (obj as IExportableChatData).responderUsername === 'string';1727}17281729export function isSerializableSessionData(obj: unknown): obj is ISerializableChatData {1730const data = obj as ISerializableChatData;1731return isExportableSessionData(obj) &&1732typeof data.creationDate === 'number' &&1733typeof data.sessionId === 'string' &&1734obj.requests.every((request: ISerializableChatRequestData) =>1735!request.usedContext /* for backward compat allow missing usedContext */ || isIUsedContext(request.usedContext)1736);1737}17381739export type IChatChangeEvent =1740| IChatInitEvent1741| IChatAddRequestEvent | IChatChangedRequestEvent | IChatRemoveRequestEvent1742| IChatAddResponseEvent1743| IChatSetAgentEvent1744| IChatMoveEvent1745| IChatSetHiddenEvent1746| IChatCompletedRequestEvent1747| IChatSetCustomTitleEvent1748;17491750export interface IChatAddRequestEvent {1751kind: 'addRequest';1752request: IChatRequestModel;1753}17541755export interface IChatChangedRequestEvent {1756kind: 'changedRequest';1757request: IChatRequestModel;1758}17591760export interface IChatCompletedRequestEvent {1761kind: 'completedRequest';1762request: IChatRequestModel;1763}17641765export interface IChatAddResponseEvent {1766kind: 'addResponse';1767response: IChatResponseModel;1768}17691770export const enum ChatRequestRemovalReason {1771/**1772* "Normal" remove1773*/1774Removal,17751776/**1777* Removed because the request will be resent1778*/1779Resend,17801781/**1782* Remove because the request is moving to another model1783*/1784Adoption1785}17861787export interface IChatRemoveRequestEvent {1788kind: 'removeRequest';1789requestId: string;1790responseId?: string;1791reason: ChatRequestRemovalReason;1792}17931794export interface IChatSetHiddenEvent {1795kind: 'setHidden';1796}17971798export interface IChatMoveEvent {1799kind: 'move';1800target: URI;1801range: IRange;1802}18031804export interface IChatSetAgentEvent {1805kind: 'setAgent';1806agent: IChatAgentData;1807command?: IChatAgentCommand;1808}18091810export interface IChatSetCustomTitleEvent {1811kind: 'setCustomTitle';1812title: string;1813}18141815export interface IChatInitEvent {1816kind: 'initialize';1817}18181819/**1820* Internal implementation of IInputModel1821*/1822class InputModel implements IInputModel {1823private readonly _state: ReturnType<typeof observableValue<IChatModelInputState | undefined>>;1824readonly state: IObservable<IChatModelInputState | undefined>;18251826constructor(initialState: IChatModelInputState | undefined) {1827this._state = observableValueOpts({ debugName: 'inputModelState', equalsFn: equals }, initialState);1828this.state = this._state;1829}18301831setState(state: Partial<IChatModelInputState>): void {1832const current = this._state.get();1833this._state.set({1834// If current is undefined, provide defaults for required fields1835attachments: [],1836mode: { id: 'agent', kind: ChatModeKind.Agent },1837selectedModel: undefined,1838inputText: '',1839selections: [],1840contrib: {},1841...current,1842...state1843}, undefined);1844}18451846clearState(): void {1847this._state.set(undefined, undefined);1848}18491850toJSON(): ISerializableChatModelInputState | undefined {1851const value = this.state.get();1852if (!value) {1853return undefined;1854}18551856// Filter out extension-contributed context items (kind: 'string' or implicit entries with StringChatContextValue)1857// These have handles that become invalid after window reload and cannot be properly restored.1858const persistableAttachments = value.attachments.filter(attachment => {1859if (isStringVariableEntry(attachment)) {1860return false;1861}1862if (isImplicitVariableEntry(attachment) && isStringImplicitContextValue(attachment.value)) {1863return false;1864}1865return true;1866});18671868return {1869contrib: value.contrib,1870attachments: persistableAttachments,1871mode: value.mode,1872selectedModel: value.selectedModel ? {1873identifier: value.selectedModel.identifier,1874metadata: value.selectedModel.metadata1875} : undefined,1876inputText: value.inputText,1877selections: value.selections1878};1879}1880}18811882export class ChatModel extends Disposable implements IChatModel {1883static getDefaultTitle(requests: (ISerializableChatRequestData | IChatRequestModel)[]): string {1884const firstRequestMessage = requests.at(0)?.message ?? '';1885const message = typeof firstRequestMessage === 'string' ?1886firstRequestMessage :1887firstRequestMessage.text;1888return message.split('\n')[0].substring(0, 200);1889}18901891private readonly _onDidDispose = this._register(new Emitter<void>());1892readonly onDidDispose = this._onDidDispose.event;18931894private readonly _onDidChange = this._register(new Emitter<IChatChangeEvent>());1895readonly onDidChange = this._onDidChange.event;18961897private readonly _pendingRequests: IChatPendingRequest[] = [];1898private readonly _onDidChangePendingRequests = this._register(new Emitter<void>());1899readonly onDidChangePendingRequests = this._onDidChangePendingRequests.event;19001901private _requests: ChatRequestModel[];19021903private _contributedChatSession: IChatSessionContext | undefined;1904public get contributedChatSession(): IChatSessionContext | undefined {1905return this._contributedChatSession;1906}1907public setContributedChatSession(session: IChatSessionContext | undefined) {1908this._contributedChatSession = session;1909}19101911private _repoData: IExportableRepoData | undefined;1912public get repoData(): IExportableRepoData | undefined {1913return this._repoData;1914}1915public setRepoData(data: IExportableRepoData | undefined): void {1916this._repoData = data;1917}19181919getPendingRequests(): readonly IChatPendingRequest[] {1920return this._pendingRequests;1921}19221923setPendingRequests(requests: readonly { requestId: string; kind: ChatRequestQueueKind }[]): void {1924const existingMap = new Map(this._pendingRequests.map(p => [p.request.id, p]));1925const newPending: IChatPendingRequest[] = [];1926for (const { requestId, kind } of requests) {1927const existing = existingMap.get(requestId);1928if (existing) {1929// Update kind if changed, keep existing request and sendOptions1930newPending.push(existing.kind === kind ? existing : { request: existing.request, kind, sendOptions: existing.sendOptions });1931}1932}1933this._pendingRequests.length = 0;1934this._pendingRequests.push(...newPending);1935this._onDidChangePendingRequests.fire();1936}19371938/**1939* @internal Used by ChatService to add a request to the queue.1940* Steering messages are placed before queued messages.1941*/1942addPendingRequest(request: ChatRequestModel, kind: ChatRequestQueueKind, sendOptions: IChatSendRequestOptions): IChatPendingRequest {1943const pendingRequest: IChatPendingRequest = {1944request,1945kind,1946sendOptions,1947};19481949if (kind === ChatRequestQueueKind.Steering) {1950// Insert after the last steering message, or at the beginning if there is none1951let insertIndex = 0;1952for (let i = 0; i < this._pendingRequests.length; i++) {1953if (this._pendingRequests[i].kind === ChatRequestQueueKind.Steering) {1954insertIndex = i + 1;1955} else {1956break;1957}1958}1959this._pendingRequests.splice(insertIndex, 0, pendingRequest);1960} else {1961// Queued messages always go at the end1962this._pendingRequests.push(pendingRequest);1963}19641965this._onDidChangePendingRequests.fire();1966return pendingRequest;1967}19681969/**1970* @internal Used by ChatService to remove a pending request1971*/1972removePendingRequest(id: string): void {1973const index = this._pendingRequests.findIndex(r => r.request.id === id);1974if (index !== -1) {1975this._pendingRequests.splice(index, 1);1976this._onDidChangePendingRequests.fire();1977}1978}19791980/**1981* @internal Used by ChatService to dequeue the next pending request1982*/1983dequeuePendingRequest(): IChatPendingRequest | undefined {1984const request = this._pendingRequests.shift();1985if (request) {1986this._onDidChangePendingRequests.fire();1987}1988return request;1989}19901991/**1992* @internal Used by ChatService to clear all pending requests1993*/1994clearPendingRequests(): void {1995if (this._pendingRequests.length > 0) {1996this._pendingRequests.length = 0;1997this._onDidChangePendingRequests.fire();1998}1999}20002001readonly lastRequestObs: IObservable<IChatRequestModel | undefined>;20022003// TODO to be clear, this is not the same as the id from the session object, which belongs to the provider.2004// It's easier to be able to identify this model before its async initialization is complete2005private readonly _sessionId: string;2006/** @deprecated Use {@link sessionResource} instead */2007get sessionId(): string {2008return this._sessionId;2009}20102011private readonly _sessionResource: URI;2012get sessionResource(): URI {2013return this._sessionResource;2014}20152016readonly requestInProgress: IObservable<boolean>;2017readonly requestNeedsInput: IObservable<IChatRequestNeedsInputInfo | undefined>;20182019/** Input model for managing input state */2020readonly inputModel: InputModel;20212022get hasRequests(): boolean {2023return this._requests.length > 0;2024}20252026get lastRequest(): ChatRequestModel | undefined {2027return this._requests.at(-1);2028}20292030private _timestamp: number;2031get timestamp(): number {2032return this._timestamp;2033}20342035get timing(): IChatSessionTiming {2036const lastRequest = this._requests.at(-1);2037const lastResponse = lastRequest?.response;2038const lastRequestStarted = lastRequest?.timestamp;2039const lastRequestEnded = lastResponse?.completedAt ?? lastResponse?.timestamp;2040return {2041created: this._timestamp,2042lastRequestStarted,2043lastRequestEnded,2044};2045}20462047get lastMessageDate(): number {2048return this._requests.at(-1)?.timestamp ?? this._timestamp;2049}20502051private get _defaultAgent() {2052return this.chatAgentService.getDefaultAgent(ChatAgentLocation.Chat, ChatModeKind.Ask);2053}20542055private readonly _initialResponderUsername: string | undefined;2056get responderUsername(): string {2057return this._defaultAgent?.fullName ??2058this._initialResponderUsername ?? '';2059}20602061private _isImported = false;2062get isImported(): boolean {2063return this._isImported;2064}20652066private _customTitle: string | undefined;2067get customTitle(): string | undefined {2068return this._customTitle;2069}20702071get title(): string {2072return this._customTitle || ChatModel.getDefaultTitle(this._requests);2073}20742075get hasCustomTitle(): boolean {2076return this._customTitle !== undefined;2077}20782079private _editingSession: IChatEditingSession | undefined;20802081get editingSession(): IChatEditingSession | undefined {2082return this._editingSession;2083}20842085private readonly _initialLocation: ChatAgentLocation;2086get initialLocation(): ChatAgentLocation {2087return this._initialLocation;2088}20892090private readonly _canUseTools: boolean = true;2091get canUseTools(): boolean {2092return this._canUseTools;2093}20942095private _disableBackgroundKeepAlive: boolean;2096get willKeepAlive(): boolean {2097return !this._disableBackgroundKeepAlive;2098}20992100public dataSerializer?: IChatDataSerializerLog;21012102constructor(2103dataRef: ISerializedChatDataReference | undefined,2104initialModelProps: { initialLocation: ChatAgentLocation; canUseTools: boolean; inputState?: ISerializableChatModelInputState; resource?: URI; sessionId?: string; disableBackgroundKeepAlive?: boolean },2105@ILogService private readonly logService: ILogService,2106@IChatAgentService private readonly chatAgentService: IChatAgentService,2107@IChatEditingService private readonly chatEditingService: IChatEditingService,2108@IChatService private readonly chatService: IChatService,2109) {2110super();21112112const initialData = dataRef?.value;2113const isValidExportedData = isExportableSessionData(initialData);2114const isValidFullData = isValidExportedData && isSerializableSessionData(initialData);2115if (initialData && !isValidExportedData) {2116this.logService.warn(`ChatModel#constructor: Loaded malformed session data: ${JSON.stringify(initialData)}`);2117}21182119this._isImported = !!initialData && isValidExportedData && !isValidFullData;2120this._sessionId = (isValidFullData && initialData.sessionId) || initialModelProps.sessionId || generateUuid();2121this._sessionResource = initialModelProps.resource ?? LocalChatSessionUri.forSession(this._sessionId);2122this._disableBackgroundKeepAlive = initialModelProps.disableBackgroundKeepAlive ?? false;21232124this._requests = initialData ? this._deserialize(initialData) : [];2125this._timestamp = (isValidFullData && initialData.creationDate) || Date.now();2126this._customTitle = isValidFullData ? initialData.customTitle : undefined;21272128// Initialize input model from serialized data (undefined for new chats)2129const serializedInputState = initialModelProps.inputState || (isValidFullData && initialData.inputState ? initialData.inputState : undefined);2130this.inputModel = new InputModel(serializedInputState && {2131attachments: serializedInputState.attachments,2132mode: serializedInputState.mode,2133selectedModel: serializedInputState.selectedModel && {2134identifier: serializedInputState.selectedModel.identifier,2135metadata: serializedInputState.selectedModel.metadata2136},2137contrib: serializedInputState.contrib,2138inputText: serializedInputState.inputText,2139selections: serializedInputState.selections2140});21412142this.dataSerializer = dataRef?.serializer;2143this._initialResponderUsername = initialData?.responderUsername;21442145this._repoData = isValidFullData && initialData.repoData ? initialData.repoData : undefined;21462147// Hydrate pending requests from serialized data2148if (isValidFullData && initialData.pendingRequests) {2149this._pendingRequests = this._deserializePendingRequests(initialData.pendingRequests);2150}21512152this._initialLocation = initialData?.initialLocation ?? initialModelProps.initialLocation;21532154this._canUseTools = initialModelProps.canUseTools;21552156this.lastRequestObs = observableFromEvent(this, this.onDidChange, () => this._requests.at(-1));21572158this._register(autorun(reader => {2159const request = this.lastRequestObs.read(reader);2160if (!request?.response) {2161return;2162}21632164reader.store.add(request.response.onDidChange(async ev => {2165if (!this._editingSession || ev.reason !== 'completedRequest') {2166return;2167}21682169this._onDidChange.fire({ kind: 'completedRequest', request });2170}));2171}));21722173this.requestInProgress = this.lastRequestObs.map((request, r) => {2174return request?.response?.isInProgress.read(r) ?? false;2175});21762177this.requestNeedsInput = this.lastRequestObs.map((request, r) => {2178const pendingInfo = request?.response?.isPendingConfirmation.read(r);2179if (!pendingInfo) {2180return undefined;2181}2182return {2183title: this.title,2184detail: pendingInfo.detail,2185};2186});21872188// Retain a reference to itself when a request is in progress, so the ChatModel stays alive in the background2189// only while running a request. TODO also keep it alive for 5min or so so we don't have to dispose/restore too often?2190if (this.initialLocation === ChatAgentLocation.Chat && !initialModelProps.disableBackgroundKeepAlive) {2191const selfRef = this._register(new MutableDisposable<IChatModelReference>());2192this._register(autorun(r => {2193const inProgress = this.requestInProgress.read(r);2194const needsInput = this.requestNeedsInput.read(r);2195const shouldStayAlive = inProgress || !!needsInput;2196if (shouldStayAlive && !selfRef.value) {2197selfRef.value = chatService.getActiveSessionReference(this._sessionResource);2198} else if (!shouldStayAlive && selfRef.value) {2199selfRef.clear();2200}2201}));2202}2203}22042205startEditingSession(isGlobalEditingSession?: boolean, transferFromSession?: IChatEditingSession): void {2206const session = this._editingSession ??= this._register(2207transferFromSession2208? this.chatEditingService.transferEditingSession(this, transferFromSession)2209: isGlobalEditingSession2210? this.chatEditingService.startOrContinueGlobalEditingSession(this)2211: this.chatEditingService.createEditingSession(this)2212);22132214if (!this._disableBackgroundKeepAlive) {2215// todo@connor4312: hold onto a reference so background sessions don't2216// trigger early disposal. This will be cleaned up with the globalization of edits.2217const selfRef = this._register(new MutableDisposable<IChatModelReference>());2218this._register(autorun(r => {2219const hasModified = session.entries.read(r).some(e => e.state.read(r) === ModifiedFileEntryState.Modified);2220if (hasModified && !selfRef.value) {2221selfRef.value = this.chatService.getActiveSessionReference(this._sessionResource);2222} else if (!hasModified && selfRef.value) {2223selfRef.clear();2224}2225}));2226}22272228this._register(autorun(reader => {2229this._setDisabledRequests(session.requestDisablement.read(reader));2230}));2231}22322233private currentEditedFileEvents = new ResourceMap<IChatAgentEditedFileEvent>();2234notifyEditingAction(action: IChatEditingSessionAction): void {2235const state = action.outcome === 'accepted' ? ChatRequestEditedFileEventKind.Keep :2236action.outcome === 'rejected' ? ChatRequestEditedFileEventKind.Undo :2237action.outcome === 'userModified' ? ChatRequestEditedFileEventKind.UserModification : null;2238if (state === null) {2239return;2240}22412242if (!this.currentEditedFileEvents.has(action.uri) || this.currentEditedFileEvents.get(action.uri)?.eventKind === ChatRequestEditedFileEventKind.Keep) {2243this.currentEditedFileEvents.set(action.uri, { eventKind: state, uri: action.uri });2244}2245}22462247private _deserialize(obj: IExportableChatData | ISerializedChatDataReference): ChatRequestModel[] {2248const requests = hasKey(obj, { serializer: true }) ? obj.value.requests : obj.requests;2249if (!Array.isArray(requests)) {2250this.logService.error(`Ignoring malformed session data: ${JSON.stringify(obj)}`);2251return [];2252}22532254try {2255return requests.map(r => this._deserializeRequest(r));2256} catch (error) {2257this.logService.error('Failed to parse chat data', error);2258return [];2259}2260}22612262private _deserializeRequest(raw: ISerializableChatRequestData): ChatRequestModel {2263const parsedRequest =2264typeof raw.message === 'string'2265? this.getParsedRequestFromString(raw.message)2266: reviveParsedChatRequest(raw.message);22672268// Old messages don't have variableData, or have it in the wrong (non-array) shape2269const variableData: IChatRequestVariableData = this.reviveVariableData(raw.variableData);2270const request = new ChatRequestModel({2271session: this,2272message: parsedRequest,2273variableData,2274timestamp: raw.timestamp ?? -1,2275restoredId: raw.requestId,2276confirmation: raw.confirmation,2277editedFileEvents: raw.editedFileEvents,2278modelId: raw.modelId,2279});2280request.shouldBeRemovedOnSend = raw.isHidden ? { requestId: raw.requestId } : raw.shouldBeRemovedOnSend;2281// eslint-disable-next-line @typescript-eslint/no-explicit-any, local/code-no-any-casts2282if (raw.response || raw.result || (raw as any).responseErrorDetails) {2283const agent = (raw.agent && 'metadata' in raw.agent) ? // Check for the new format, ignore entries in the old format2284reviveSerializedAgent(raw.agent) : undefined;22852286// Port entries from old format2287const result = 'responseErrorDetails' in raw ?2288// eslint-disable-next-line local/code-no-dangerous-type-assertions2289{ errorDetails: raw.responseErrorDetails } as IChatAgentResult : raw.result;2290let modelState = raw.modelState || { value: raw.isCanceled ? ResponseModelState.Cancelled : ResponseModelState.Complete, completedAt: Date.now() };2291if (modelState.value === ResponseModelState.Pending || modelState.value === ResponseModelState.NeedsInput) {2292modelState = { value: ResponseModelState.Cancelled, completedAt: Date.now() };2293}22942295request.response = new ChatResponseModel({2296responseContent: raw.response ?? [new MarkdownString(raw.response)],2297session: this,2298agent,2299slashCommand: raw.slashCommand,2300requestId: request.id,2301modelState,2302vote: raw.vote,2303timestamp: raw.timestamp,2304voteDownReason: raw.voteDownReason,2305result,2306followups: raw.followups,2307restoredId: raw.responseId,2308timeSpentWaiting: raw.timeSpentWaiting,2309shouldBeBlocked: request.shouldBeBlocked.get(),2310codeBlockInfos: raw.responseMarkdownInfo?.map<ICodeBlockInfo>(info => ({ suggestionId: info.suggestionId })),2311});2312request.response.shouldBeRemovedOnSend = raw.isHidden ? { requestId: raw.requestId } : raw.shouldBeRemovedOnSend;2313if (raw.usedContext) { // @ulugbekna: if this's a new vscode sessions, doc versions are incorrect anyway?2314request.response.applyReference(revive(raw.usedContext));2315}23162317raw.contentReferences?.forEach(r => request.response!.applyReference(revive(r)));2318raw.codeCitations?.forEach(c => request.response!.applyCodeCitation(revive(c)));2319}2320return request;2321}23222323private reviveVariableData(raw: IChatRequestVariableData): IChatRequestVariableData {2324const variableData = raw && Array.isArray(raw.variables)2325? raw :2326{ variables: [] };23272328variableData.variables = variableData.variables.map<IChatRequestVariableEntry>(IChatRequestVariableEntry.fromExport);23292330return variableData;2331}23322333private getParsedRequestFromString(message: string): IParsedChatRequest {2334// TODO These offsets won't be used, but chat replies need to go through the parser as well2335const parts = [new ChatRequestTextPart(new OffsetRange(0, message.length), { startColumn: 1, startLineNumber: 1, endColumn: 1, endLineNumber: 1 }, message)];2336return {2337text: message,2338parts2339};2340}23412342/**2343* Hydrates pending requests from serialized data.2344* For each serialized pending request, finds the matching request model and adds it to the pending queue.2345*/2346private _deserializePendingRequests(pendingRequests: ISerializablePendingRequestData[]): IChatPendingRequest[] {2347try {2348return pendingRequests.map(pending => ({2349id: pending.id,2350request: this._deserializeRequest(pending.request),2351kind: pending.kind,2352sendOptions: {2353...pending.sendOptions,2354userSelectedTools: pending.sendOptions.userSelectedTools2355? constObservable(pending.sendOptions.userSelectedTools)2356: undefined,2357}2358}));2359} catch (e) {2360this.logService.error('Failed to parse pending chat requests', e);2361return [];2362}2363}2364236523662367getRequests(): ChatRequestModel[] {2368return this._requests;2369}23702371resetCheckpoint(): void {2372for (const request of this._requests) {2373request.setShouldBeBlocked(false);2374if (request.response) {2375request.response.setBlockedState(false);2376}2377}2378}23792380setCheckpoint(requestId: string | undefined) {2381let checkpoint: ChatRequestModel | undefined;2382let checkpointIndex = -1;2383if (requestId !== undefined) {2384this._requests.forEach((request, index) => {2385if (request.id === requestId) {2386checkpointIndex = index;2387checkpoint = request;2388request.setShouldBeBlocked(true);2389}2390});23912392if (!checkpoint) {2393return; // Invalid request ID2394}2395}23962397for (let i = this._requests.length - 1; i >= 0; i -= 1) {2398const request = this._requests[i];2399if (this._checkpoint && !checkpoint) {2400request.setShouldBeBlocked(false);2401if (request.response) {2402request.response.setBlockedState(false);2403}2404} else if (checkpoint && i >= checkpointIndex) {2405request.setShouldBeBlocked(true);2406if (request.response) {2407request.response.setBlockedState(true);2408}2409} else if (checkpoint && i < checkpointIndex) {2410request.setShouldBeBlocked(false);2411if (request.response) {2412request.response.setBlockedState(false);2413}2414}2415}24162417this._checkpoint = checkpoint;2418}24192420private _checkpoint: ChatRequestModel | undefined = undefined;2421public get checkpoint() {2422return this._checkpoint;2423}24242425private _setDisabledRequests(requestIds: IChatRequestDisablement[]) {2426this._requests.forEach((request) => {2427const shouldBeRemovedOnSend = requestIds.find(r => r.requestId === request.id);2428request.shouldBeRemovedOnSend = shouldBeRemovedOnSend;2429if (request.response) {2430request.response.shouldBeRemovedOnSend = shouldBeRemovedOnSend;2431}2432});24332434this._onDidChange.fire({ kind: 'setHidden' });2435}24362437addRequest(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 {2438const editedFileEvents = [...this.currentEditedFileEvents.values()];2439this.currentEditedFileEvents.clear();2440const request = new ChatRequestModel({2441restoredId: id,2442session: this,2443message,2444variableData,2445timestamp: Date.now(),2446attempt,2447modeInfo,2448confirmation,2449locationData,2450attachedContext: attachments,2451isCompleteAddedRequest,2452modelId,2453editedFileEvents: editedFileEvents.length ? editedFileEvents : undefined,2454userSelectedTools,2455});2456request.response = new ChatResponseModel({2457responseContent: [],2458session: this,2459agent: chatAgent,2460slashCommand,2461requestId: request.id,2462isCompleteAddedRequest,2463codeBlockInfos: undefined,2464});24652466this._requests.push(request);2467this._onDidChange.fire({ kind: 'addRequest', request });2468return request;2469}24702471public setCustomTitle(title: string): void {2472this._customTitle = title;2473this._onDidChange.fire({ kind: 'setCustomTitle', title });2474}24752476updateRequest(request: ChatRequestModel, variableData: IChatRequestVariableData) {2477request.variableData = variableData;2478this._onDidChange.fire({ kind: 'changedRequest', request });2479}24802481adoptRequest(request: ChatRequestModel): void {2482// this doesn't use `removeRequest` because it must not dispose the request object2483const oldOwner = request.session;2484const index = oldOwner._requests.findIndex((candidate: ChatRequestModel) => candidate.id === request.id);24852486if (index === -1) {2487return;2488}24892490oldOwner._requests.splice(index, 1);24912492request.adoptTo(this);2493request.response?.adoptTo(this);2494this._requests.push(request);24952496oldOwner._onDidChange.fire({ kind: 'removeRequest', requestId: request.id, responseId: request.response?.id, reason: ChatRequestRemovalReason.Adoption });2497this._onDidChange.fire({ kind: 'addRequest', request });2498}24992500acceptResponseProgress(request: ChatRequestModel, progress: IChatProgress, quiet?: boolean): void {2501if (!request.response) {2502request.response = new ChatResponseModel({2503responseContent: [],2504session: this,2505requestId: request.id,2506codeBlockInfos: undefined,2507});2508}25092510if (request.response.isComplete) {2511throw new Error('acceptResponseProgress: Adding progress to a completed response');2512}25132514if (progress.kind === 'usedContext' || progress.kind === 'reference') {2515request.response.applyReference(progress);2516} else if (progress.kind === 'codeCitation') {2517request.response.applyCodeCitation(progress);2518} else if (progress.kind === 'move') {2519this._onDidChange.fire({ kind: 'move', target: progress.uri, range: progress.range });2520} else if (progress.kind === 'codeblockUri' && progress.isEdit) {2521request.response.addUndoStop({ id: progress.undoStopId ?? generateUuid(), kind: 'undoStop' });2522request.response.updateContent(progress, quiet);2523} else if (progress.kind === 'progressTaskResult') {2524// Should have been handled upstream, not sent to model2525this.logService.error(`Couldn't handle progress: ${JSON.stringify(progress)}`);2526} else {2527request.response.updateContent(progress, quiet);2528}2529}25302531removeRequest(id: string, reason: ChatRequestRemovalReason = ChatRequestRemovalReason.Removal): void {2532const index = this._requests.findIndex(request => request.id === id);2533const request = this._requests[index];25342535if (index !== -1) {2536this._onDidChange.fire({ kind: 'removeRequest', requestId: request.id, responseId: request.response?.id, reason });2537this._requests.splice(index, 1);2538request.response?.dispose();2539}2540}25412542cancelRequest(request: ChatRequestModel): void {2543if (request.response) {2544request.response.cancel();2545}2546}25472548setResponse(request: ChatRequestModel, result: IChatAgentResult): void {2549if (!request.response) {2550request.response = new ChatResponseModel({2551responseContent: [],2552session: this,2553requestId: request.id,2554codeBlockInfos: undefined,2555});2556}25572558request.response.setResult(result);2559}25602561setFollowups(request: ChatRequestModel, followups: IChatFollowup[] | undefined): void {2562if (!request.response) {2563// Maybe something went wrong?2564return;2565}2566request.response.setFollowups(followups);2567}25682569setResponseModel(request: ChatRequestModel, response: ChatResponseModel): void {2570request.response = response;2571this._onDidChange.fire({ kind: 'addResponse', response });2572}25732574toExport(): IExportableChatData {2575return {2576responderUsername: this.responderUsername,2577initialLocation: this.initialLocation,2578requests: this._requests.map((r): ISerializableChatRequestData => {2579const message = {2580...r.message,2581// eslint-disable-next-line @typescript-eslint/no-explicit-any2582parts: r.message.parts.map((p: any) => p && 'toJSON' in p ? (p.toJSON as Function)() : p)2583};2584const agent = r.response?.agent;2585const agentJson = agent && 'toJSON' in agent ? (agent.toJSON as Function)() :2586agent ? { ...agent } : undefined;2587return {2588requestId: r.id,2589message,2590variableData: IChatRequestVariableData.toExport(r.variableData),2591response: r.response ?2592r.response.entireResponse.value.map(item => {2593// Keeping the shape of the persisted data the same for back compat2594if (item.kind === 'treeData') {2595return item.treeData;2596} else if (item.kind === 'markdownContent') {2597return item.content;2598} else {2599// eslint-disable-next-line local/code-no-any-casts, @typescript-eslint/no-explicit-any2600return item as any; // TODO2601}2602})2603: undefined,2604shouldBeRemovedOnSend: r.shouldBeRemovedOnSend,2605agent: agentJson,2606timestamp: r.timestamp,2607confirmation: r.confirmation,2608editedFileEvents: r.editedFileEvents,2609modelId: r.modelId,2610...r.response?.toJSON(),2611};2612}),2613};2614}26152616toJSON(): ISerializableChatData {2617return {2618version: 3,2619...this.toExport(),2620sessionId: this.sessionId,2621creationDate: this._timestamp,2622customTitle: this._customTitle,2623inputState: this.inputModel.toJSON(),2624repoData: this._repoData,2625};2626}26272628override dispose() {2629this._requests.forEach(r => r.response?.dispose());2630this._onDidDispose.fire();26312632super.dispose();2633}2634}26352636export function updateRanges(variableData: IChatRequestVariableData, diff: number): IChatRequestVariableData {2637return {2638variables: variableData.variables.map(v => ({2639...v,2640range: v.range && {2641start: v.range.start - diff,2642endExclusive: v.range.endExclusive - diff2643}2644}))2645};2646}26472648export function canMergeMarkdownStrings(md1: IMarkdownString, md2: IMarkdownString): boolean {2649if (md1.baseUri && md2.baseUri) {2650const baseUriEquals = md1.baseUri.scheme === md2.baseUri.scheme2651&& md1.baseUri.authority === md2.baseUri.authority2652&& md1.baseUri.path === md2.baseUri.path2653&& md1.baseUri.query === md2.baseUri.query2654&& md1.baseUri.fragment === md2.baseUri.fragment;2655if (!baseUriEquals) {2656return false;2657}2658} else if (md1.baseUri || md2.baseUri) {2659return false;2660}26612662return equals(md1.isTrusted, md2.isTrusted) &&2663md1.supportHtml === md2.supportHtml &&2664md1.supportThemeIcons === md2.supportThemeIcons;2665}26662667export function appendMarkdownString(md1: IMarkdownString, md2: IMarkdownString | string): IMarkdownString {2668const appendedValue = typeof md2 === 'string' ? md2 : md2.value;2669return {2670value: md1.value + appendedValue,2671isTrusted: md1.isTrusted,2672supportThemeIcons: md1.supportThemeIcons,2673supportHtml: md1.supportHtml,2674baseUri: md1.baseUri2675};2676}26772678export function getCodeCitationsMessage(citations: ReadonlyArray<IChatCodeCitation>): string {2679if (citations.length === 0) {2680return '';2681}26822683const licenseTypes = citations.reduce((set, c) => set.add(c.license), new Set<string>());2684const label = licenseTypes.size === 1 ?2685localize('codeCitation', "Similar code found with 1 license type", licenseTypes.size) :2686localize('codeCitations', "Similar code found with {0} license types", licenseTypes.size);2687return label;2688}26892690/**2691* Converts IChatSendRequestOptions to a serializable format by extracting only2692* serializable fields and converting observables to static values.2693*/2694export function serializeSendOptions(options: IChatSendRequestOptions): ISerializableSendOptions {2695return {2696modeInfo: options.modeInfo,2697userSelectedModelId: options.userSelectedModelId,2698userSelectedTools: options.userSelectedTools?.get(),2699location: options.location,2700locationData: options.locationData,2701attempt: options.attempt,2702noCommandDetection: options.noCommandDetection,2703agentId: options.agentId,2704agentIdSilent: options.agentIdSilent,2705slashCommand: options.slashCommand,2706confirmation: options.confirmation,2707};2708}27092710export enum ChatRequestEditedFileEventKind {2711Keep = 1,2712Undo = 2,2713UserModification = 3,2714}27152716export interface IChatAgentEditedFileEvent {2717readonly uri: URI;2718readonly eventKind: ChatRequestEditedFileEventKind;2719}27202721/** URI for a resource embedded in a chat request/response */2722export namespace ChatResponseResource {2723export const scheme = 'vscode-chat-response-resource';27242725export function createUri(sessionResource: URI, toolCallId: string, index: number, basename?: string): URI {2726return URI.from({2727scheme: ChatResponseResource.scheme,2728authority: encodeHex(VSBuffer.fromString(sessionResource.toString())),2729path: `/tool/${toolCallId}/${index}` + (basename ? `/${basename}` : ''),2730});2731}27322733export function parseUri(uri: URI): undefined | { sessionResource: URI; toolCallId: string; index: number } {2734if (uri.scheme !== ChatResponseResource.scheme) {2735return undefined;2736}27372738const parts = uri.path.split('/');2739if (parts.length < 5) {2740return undefined;2741}27422743const [, kind, toolCallId, index] = parts;2744if (kind !== 'tool') {2745return undefined;2746}27472748let sessionResource: URI;2749try {2750sessionResource = URI.parse(decodeHex(uri.authority).toString());2751} catch (e) {2752if (e instanceof SyntaxError) { // pre-1.108 local session ID2753sessionResource = LocalChatSessionUri.forSession(uri.authority);2754} else {2755throw e;2756}2757}27582759return {2760sessionResource,2761toolCallId: toolCallId,2762index: Number(index),2763};2764}2765}276627672768