From 714f54156ffdf48dfe48b2f4b51d39c6128ecc42 Mon Sep 17 00:00:00 2001 From: Rob Lourens Date: Sun, 20 Apr 2025 09:54:07 -0700 Subject: [PATCH] Add editedFileEvents (#246996) * Start plumbing working set events * more * More fixes * Update tests --- .../src/singlefolder-tests/chat.test.ts | 3 +- .../workbench/api/common/extHost.api.impl.ts | 2 + .../api/common/extHostChatAgents2.ts | 12 ++-- .../api/common/extHostTypeConverters.ts | 37 +++++++---- src/vs/workbench/api/common/extHostTypes.ts | 11 +++- .../chatEditingModifiedDocumentEntry.ts | 4 +- .../chatEditingModifiedFileEntry.ts | 5 +- .../contrib/chat/common/chatAgents.ts | 3 +- .../contrib/chat/common/chatModel.ts | 62 ++++++++++++++++++- .../contrib/chat/common/chatService.ts | 2 +- .../contrib/chat/common/chatServiceImpl.ts | 12 +++- .../ChatService_can_deserialize.0.snap | 3 +- ...rvice_can_deserialize_with_response.0.snap | 3 +- .../ChatService_can_serialize.1.snap | 6 +- .../ChatService_sendRequest_fails.0.snap | 3 +- ...scode.proposed.chatParticipantPrivate.d.ts | 61 ++++++++++++++++++ 16 files changed, 194 insertions(+), 35 deletions(-) diff --git a/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts b/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts index 289f0a6a1c0..df90a035401 100644 --- a/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts +++ b/extensions/vscode-api-tests/src/singlefolder-tests/chat.test.ts @@ -5,7 +5,7 @@ import * as assert from 'assert'; import 'mocha'; -import { ChatContext, ChatRequest, ChatResult, Disposable, Event, EventEmitter, chat, commands, lm } from 'vscode'; +import { ChatContext, ChatRequest, ChatRequestTurn, ChatRequestTurn2, ChatResult, Disposable, Event, EventEmitter, chat, commands, lm } from 'vscode'; import { DeferredPromise, asPromise, assertNoRpc, closeAllEditors, delay, disposeAll } from '../utils'; suite('chat', () => { @@ -71,6 +71,7 @@ suite('chat', () => { assert.strictEqual(request.context.history.length, 2); assert.strictEqual(request.context.history[0].participant, 'api-test.participant'); assert.strictEqual(request.context.history[0].command, 'hello'); + assert.ok(request.context.history[0] instanceof ChatRequestTurn && request.context.history[0] instanceof ChatRequestTurn2); deferred.complete(); } } catch (e) { diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index c23e6900be5..51d7797272d 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1800,11 +1800,13 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I ChatResponseExtensionsPart: extHostTypes.ChatResponseExtensionsPart, ChatResponseReferencePartStatusKind: extHostTypes.ChatResponseReferencePartStatusKind, ChatRequestTurn: extHostTypes.ChatRequestTurn, + ChatRequestTurn2: extHostTypes.ChatRequestTurn, ChatResponseTurn: extHostTypes.ChatResponseTurn, ChatLocation: extHostTypes.ChatLocation, ChatRequestEditorData: extHostTypes.ChatRequestEditorData, ChatRequestNotebookData: extHostTypes.ChatRequestNotebookData, ChatReferenceBinaryData: extHostTypes.ChatReferenceBinaryData, + ChatRequestEditedFileEventKind: extHostTypes.ChatRequestEditedFileEventKind, LanguageModelChatMessageRole: extHostTypes.LanguageModelChatMessageRole, LanguageModelChatMessage: extHostTypes.LanguageModelChatMessage, LanguageModelChatMessage2: extHostTypes.LanguageModelChatMessage2, diff --git a/src/vs/workbench/api/common/extHostChatAgents2.ts b/src/vs/workbench/api/common/extHostChatAgents2.ts index d7824ff4f0c..49c7a5feff3 100644 --- a/src/vs/workbench/api/common/extHostChatAgents2.ts +++ b/src/vs/workbench/api/common/extHostChatAgents2.ts @@ -410,8 +410,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS const { request, location, history } = await this._createRequest(requestDto, context, detector.extension); const model = await this.getModelForRequest(request, detector.extension); - const includeInteractionId = isProposedApiEnabled(detector.extension, 'chatParticipantPrivate'); - const extRequest = typeConvert.ChatAgentRequest.to(includeInteractionId ? request : { ...request, requestId: '' }, location, model, this.getDiagnosticsWhenEnabled(detector.extension), this.getToolsForRequest(detector.extension, request)); + const extRequest = typeConvert.ChatAgentRequest.to(request, location, model, this.getDiagnosticsWhenEnabled(detector.extension), this.getToolsForRequest(detector.extension, request), detector.extension); return detector.provider.provideParticipantDetection( extRequest, @@ -495,13 +494,13 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS stream = new ChatAgentResponseStream(agent.extension, request, this._proxy, this._commands.converter, sessionDisposables); const model = await this.getModelForRequest(request, agent.extension); - const includeInteractionId = isProposedApiEnabled(agent.extension, 'chatParticipantPrivate'); const extRequest = typeConvert.ChatAgentRequest.to( - includeInteractionId ? request : { ...request, requestId: '' }, + request, location, model, this.getDiagnosticsWhenEnabled(agent.extension), - this.getToolsForRequest(agent.extension, request) + this.getToolsForRequest(agent.extension, request), + agent.extension ); inFlightRequest = { requestId: requestDto.requestId, extRequest }; this._inFlightRequests.add(inFlightRequest); @@ -585,7 +584,8 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS const toolReferences = h.request.variables.variables .filter(v => v.kind === 'tool') .map(typeConvert.ChatLanguageModelToolReference.to); - const turn = new extHostTypes.ChatRequestTurn(h.request.message, h.request.command, varsWithoutTools, h.request.agentId, toolReferences); + const editedFileEvents = isProposedApiEnabled(extension, 'chatParticipantPrivate') ? h.request.editedFileEvents : undefined; + const turn = new extHostTypes.ChatRequestTurn(h.request.message, h.request.command, varsWithoutTools, h.request.agentId, toolReferences, editedFileEvents); res.push(turn); // RESPONSE turn diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index a9a9ef3b486..3177861cb19 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -34,7 +34,7 @@ import * as languageSelector from '../../../editor/common/languageSelector.js'; import * as languages from '../../../editor/common/languages.js'; import { EndOfLineSequence, TrackedRangeStickiness } from '../../../editor/common/model.js'; import { ITextEditorOptions } from '../../../platform/editor/common/editor.js'; -import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; +import { IExtensionDescription, IRelaxedExtensionDescription } from '../../../platform/extensions/common/extensions.js'; import { IMarkerData, IRelatedInformation, MarkerSeverity, MarkerTag } from '../../../platform/markers/common/markers.js'; import { ProgressLocation as MainProgressLocation } from '../../../platform/progress/common/progress.js'; import { DEFAULT_EDITOR_ASSOCIATION, SaveReason } from '../../common/editor.js'; @@ -55,7 +55,7 @@ import { TestId } from '../../contrib/testing/common/testId.js'; import { CoverageDetails, DetailType, ICoverageCount, IFileCoverage, ISerializedTestResults, ITestErrorMessage, ITestItem, ITestRunProfileReference, ITestTag, TestMessageType, TestResultItem, TestRunProfileBitset, denamespaceTestTag, namespaceTestTag } from '../../contrib/testing/common/testTypes.js'; import { EditorGroupColumn } from '../../services/editor/common/editorGroupColumn.js'; import { ACTIVE_GROUP, SIDE_GROUP } from '../../services/editor/common/editorService.js'; -import { checkProposedApiEnabled } from '../../services/extensions/common/extensions.js'; +import { checkProposedApiEnabled, isProposedApiEnabled } from '../../services/extensions/common/extensions.js'; import { Dto } from '../../services/extensions/common/proxyIdentifier.js'; import * as extHostProtocol from './extHost.protocol.js'; import { CommandsConverter } from './extHostCommands.js'; @@ -2897,10 +2897,11 @@ export namespace ChatResponsePart { } export namespace ChatAgentRequest { - export function to(request: IChatAgentRequest, location2: vscode.ChatRequestEditorData | vscode.ChatRequestNotebookData | undefined, model: vscode.LanguageModelChat, diagnostics: readonly [vscode.Uri, readonly vscode.Diagnostic[]][], tools: vscode.LanguageModelToolInformation[] | undefined): vscode.ChatRequest { + export function to(request: IChatAgentRequest, location2: vscode.ChatRequestEditorData | vscode.ChatRequestNotebookData | undefined, model: vscode.LanguageModelChat, diagnostics: readonly [vscode.Uri, readonly vscode.Diagnostic[]][], tools: vscode.LanguageModelToolInformation[] | undefined, extension: IRelaxedExtensionDescription): vscode.ChatRequest { const toolReferences = request.variables.variables.filter(v => v.kind === 'tool'); const variableReferences = request.variables.variables.filter(v => v.kind !== 'tool'); - const requestWithoutId = { + const requestWithAllProps: vscode.ChatRequest = { + id: request.requestId, prompt: request.message, command: request.command, attempt: request.attempt ?? 0, @@ -2914,16 +2915,28 @@ export namespace ChatAgentRequest { location2, toolInvocationToken: Object.freeze({ sessionId: request.sessionId }) as never, tools, - model + model, + editedFileEvents: request.editedFileEvents, }; - if (request.requestId) { - return { - ...requestWithoutId, - id: request.requestId - }; + + if (!isProposedApiEnabled(extension, 'chatParticipantPrivate')) { + delete (requestWithAllProps as any).id; + delete (requestWithAllProps as any).attempt; + delete (requestWithAllProps as any).enableCommandDetection; + delete (requestWithAllProps as any).isParticipantDetected; + delete (requestWithAllProps as any).location; + delete (requestWithAllProps as any).location2; + delete (requestWithAllProps as any).editedFileEvents; } - // This cast is done to allow sending the stabl version of ChatRequest which does not have an id property - return requestWithoutId as unknown as vscode.ChatRequest; + + if (!isProposedApiEnabled(extension, 'chatParticipantAdditions')) { + delete requestWithAllProps.acceptedConfirmationData; + delete requestWithAllProps.rejectedConfirmationData; + delete (requestWithAllProps as any).tools; + } + + + return requestWithAllProps; } } diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index b245cb4f11d..b8a8c9c2a8d 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -4525,6 +4525,12 @@ export enum ChatEditingSessionActionOutcome { Saved = 3 } +export enum ChatRequestEditedFileEventKind { + Keep = 1, + Undo = 2, + UserModification = 3, +} + //#endregion //#region Interactive Editor @@ -4718,13 +4724,14 @@ export class ChatResponseNotebookEditPart implements vscode.ChatResponseNotebook } } -export class ChatRequestTurn implements vscode.ChatRequestTurn { +export class ChatRequestTurn implements vscode.ChatRequestTurn2 { constructor( readonly prompt: string, readonly command: string | undefined, readonly references: vscode.ChatPromptReference[], readonly participant: string, - readonly toolReferences: vscode.ChatLanguageModelToolReference[] + readonly toolReferences: vscode.ChatLanguageModelToolReference[], + readonly editedFileEvents?: vscode.ChatRequestEditedFileEvent[] ) { } } diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedDocumentEntry.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedDocumentEntry.ts index 3868469ddc4..e6d83e24c38 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedDocumentEntry.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedDocumentEntry.ts @@ -236,7 +236,6 @@ export class ChatEditingModifiedDocumentEntry extends AbstractChatEditingModifie const e_sum = this._edit; const e_ai = edit; this._edit = e_sum.compose(e_ai); - } else { // e_ai @@ -269,6 +268,7 @@ export class ChatEditingModifiedDocumentEntry extends AbstractChatEditingModifie } this._allEditsAreFromUs = false; + this._userEditScheduler.schedule(); this._updateDiffInfoSeq(); const didResetToOriginalContent = this.modifiedModel.getValue() === this.initialContent; @@ -348,6 +348,7 @@ export class ChatEditingModifiedDocumentEntry extends AbstractChatEditingModifie await this._updateDiffInfoSeq(); if (this._diffInfo.get().identical) { this._stateObs.set(ModifiedFileEntryState.Accepted, undefined); + this._notifyAction('accepted'); } this._accessibilitySignalService.playSignal(AccessibilitySignal.editsKept, { allowManyInParallel: true }); return true; @@ -366,6 +367,7 @@ export class ChatEditingModifiedDocumentEntry extends AbstractChatEditingModifie await this._updateDiffInfoSeq(); if (this._diffInfo.get().identical) { this._stateObs.set(ModifiedFileEntryState.Rejected, undefined); + this._notifyAction('rejected'); } this._accessibilitySignalService.playSignal(AccessibilitySignal.editsUndone, { allowManyInParallel: true }); return true; diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts index 44cfbfa8293..67a0a3e60f9 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingModifiedFileEntry.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { RunOnceScheduler } from '../../../../../base/common/async.js'; import { Emitter } from '../../../../../base/common/event.js'; import { Disposable, DisposableMap, MutableDisposable } from '../../../../../base/common/lifecycle.js'; import { Schemas } from '../../../../../base/common/network.js'; @@ -81,6 +82,8 @@ export abstract class AbstractChatEditingModifiedFileEntry extends Disposable im readonly abstract originalURI: URI; + protected readonly _userEditScheduler = this._register(new RunOnceScheduler(() => this._notifyAction('userModified'), 1000)); + constructor( readonly modifiedURI: URI, protected _telemetryInfo: IModifiedEntryTelemetryInfo, @@ -189,7 +192,7 @@ export abstract class AbstractChatEditingModifiedFileEntry extends Disposable im protected abstract _doReject(tx: ITransaction | undefined): Promise; - private _notifyAction(outcome: 'accepted' | 'rejected') { + protected _notifyAction(outcome: 'accepted' | 'rejected' | 'userModified') { this._chatService.notifyUserAction({ action: { kind: 'chatEditingSessionAction', uri: this.modifiedURI, hasRemainingEdits: false, outcome }, agentId: this._telemetryInfo.agentId, diff --git a/src/vs/workbench/contrib/chat/common/chatAgents.ts b/src/vs/workbench/contrib/chat/common/chatAgents.ts index d0a47a245b4..9fe6e516395 100644 --- a/src/vs/workbench/contrib/chat/common/chatAgents.ts +++ b/src/vs/workbench/contrib/chat/common/chatAgents.ts @@ -24,7 +24,7 @@ import { IProductService } from '../../../../platform/product/common/productServ import { asJson, IRequestService } from '../../../../platform/request/common/request.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { ChatContextKeys } from './chatContextKeys.js'; -import { IChatProgressHistoryResponseContent, IChatRequestVariableData, ISerializableChatAgentData } from './chatModel.js'; +import { IChatAgentEditedFileEvent, IChatProgressHistoryResponseContent, IChatRequestVariableData, ISerializableChatAgentData } from './chatModel.js'; import { IRawChatCommandContribution } from './chatParticipantContribTypes.js'; import { IChatFollowup, IChatLocationData, IChatProgress, IChatResponseErrorDetails, IChatTaskDto } from './chatService.js'; import { ChatAgentLocation, ChatMode } from './constants.js'; @@ -138,6 +138,7 @@ export interface IChatAgentRequest { rejectedConfirmationData?: any[]; userSelectedModelId?: string; userSelectedTools?: string[]; + editedFileEvents?: IChatAgentEditedFileEvent[]; } export interface IChatQuestion { diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index dcd5b116acd..7aedf0ee4c6 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -9,6 +9,7 @@ import { Codicon } from '../../../../base/common/codicons.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { IMarkdownString, MarkdownString, isMarkdownString } from '../../../../base/common/htmlContent.js'; import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js'; +import { ResourceMap } from '../../../../base/common/map.js'; import { revive } from '../../../../base/common/marshalling.js'; import { Schemas } from '../../../../base/common/network.js'; import { equals } from '../../../../base/common/objects.js'; @@ -27,7 +28,7 @@ import { CellUri, ICellEditOperation } from '../../notebook/common/notebookCommo import { IChatAgentCommand, IChatAgentData, IChatAgentResult, IChatAgentService, reviveSerializedAgent } from './chatAgents.js'; import { IChatEditingService, IChatEditingSession } from './chatEditingService.js'; import { ChatRequestTextPart, IParsedChatRequest, reviveParsedChatRequest } from './chatParserTypes.js'; -import { ChatAgentVoteDirection, ChatAgentVoteDownReason, IChatAgentMarkdownContentWithVulnerability, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatExtensionsContent, IChatFollowup, IChatLocationData, IChatMarkdownContent, IChatNotebookEdit, IChatProgress, IChatProgressMessage, IChatResponseCodeblockUriPart, IChatResponseProgressFileTreeData, IChatTask, IChatTextEdit, IChatToolInvocation, IChatToolInvocationSerialized, IChatTreeData, IChatUndoStop, IChatUsedContext, IChatWarningMessage, isIUsedContext } from './chatService.js'; +import { ChatAgentVoteDirection, ChatAgentVoteDownReason, IChatAgentMarkdownContentWithVulnerability, IChatCodeCitation, IChatCommandButton, IChatConfirmation, IChatContentInlineReference, IChatContentReference, IChatEditingSessionAction, IChatExtensionsContent, IChatFollowup, IChatLocationData, IChatMarkdownContent, IChatNotebookEdit, IChatProgress, IChatProgressMessage, IChatResponseCodeblockUriPart, IChatResponseProgressFileTreeData, IChatTask, IChatTextEdit, IChatToolInvocation, IChatToolInvocationSerialized, IChatTreeData, IChatUndoStop, IChatUsedContext, IChatWarningMessage, isIUsedContext } from './chatService.js'; import { IChatRequestVariableValue } from './chatVariables.js'; import { ChatAgentLocation, ChatMode } from './constants.js'; @@ -248,6 +249,7 @@ export interface IChatRequestModel { readonly attachedContext?: IChatRequestVariableEntry[]; readonly isCompleteAddedRequest: boolean; readonly response?: IChatResponseModel; + readonly editedFileEvents?: IChatAgentEditedFileEvent[]; shouldBeRemovedOnSend: IChatRequestDisablement | undefined; } @@ -389,6 +391,7 @@ export interface IChatRequestModelParameters { isCompleteAddedRequest?: boolean; modelId?: string; restoredId?: string; + editedFileEvents?: IChatAgentEditedFileEvent[]; } export class ChatRequestModel implements IChatRequestModel { @@ -406,6 +409,7 @@ export class ChatRequestModel implements IChatRequestModel { private readonly _confirmation?: string; private readonly _locationData?: IChatLocationData; private readonly _attachedContext?: IChatRequestVariableEntry[]; + private readonly _editedFileEvents?: IChatAgentEditedFileEvent[]; public get session(): ChatModel { return this._session; @@ -443,6 +447,10 @@ export class ChatRequestModel implements IChatRequestModel { return this._attachedContext; } + public get editedFileEvents(): IChatAgentEditedFileEvent[] | undefined { + return this._editedFileEvents; + } + constructor(params: IChatRequestModelParameters) { this._session = params.session; this.message = params.message; @@ -455,6 +463,7 @@ export class ChatRequestModel implements IChatRequestModel { this.isCompleteAddedRequest = params.isCompleteAddedRequest ?? false; this.modelId = params.modelId; this.id = params.restoredId ?? 'request_' + generateUuid(); + this._editedFileEvents = params.editedFileEvents; } adoptTo(session: ChatModel) { @@ -1082,6 +1091,7 @@ export interface ISerializableChatRequestData { codeCitations?: ReadonlyArray; timestamp?: number; confirmation?: string; + editedFileEvents?: IChatAgentEditedFileEvent[]; } export interface IExportableChatData { @@ -1437,7 +1447,37 @@ export class ChatModel extends Disposable implements IChatModel { this.chatEditingService.startOrContinueGlobalEditingSession(this) : this.chatEditingService.createEditingSession(this); this._editingSession = new ObservablePromise(editingSessionPromise); - this._editingSession.promise.then(editingSession => this._store.isDisposed ? editingSession.dispose() : this._register(editingSession)); + this._editingSession.promise.then(editingSession => { + this._store.isDisposed ? editingSession.dispose() : this._register(editingSession); + + // const currentStates = new ResourceMap(); + // this._register(autorun(r => { + // editingSession.entries.read(r).forEach(entry => { + // const state = entry.state.read(r); + // if (state !== currentStates.get(entry.modifiedURI)) { + // currentStates.set(entry.modifiedURI, state); + // if (state === ModifiedFileEntryState.Rejected) { + // this.currentWorkingSetEntries.push({ + // uri: entry.modifiedURI, + // state: ChatAgentWorkingSetEntryState.Rejected + // }); + // } + // } + // }); + // })); + }); + } + + private currentEditedFileEvents = new ResourceMap(); + notifyEditingAction(action: IChatEditingSessionAction): void { + const state = action.outcome === 'accepted' ? ChatRequestEditedFileEventKind.Keep : + action.outcome === 'rejected' ? ChatRequestEditedFileEventKind.Undo : + action.outcome === 'userModified' ? ChatRequestEditedFileEventKind.UserModification : null; + if (state === null) { + return; + } + + this.currentEditedFileEvents.set(action.uri, { eventKind: state, uri: action.uri }); } private _deserialize(obj: IExportableChatData): ChatRequestModel[] { @@ -1463,6 +1503,7 @@ export class ChatModel extends Disposable implements IChatModel { timestamp: raw.timestamp ?? -1, restoredId: raw.requestId, confirmation: raw.confirmation, + editedFileEvents: raw.editedFileEvents, }); request.shouldBeRemovedOnSend = raw.isHidden ? { requestId: raw.requestId } : raw.shouldBeRemovedOnSend; if (raw.response || raw.result || (raw as any).responseErrorDetails) { @@ -1610,6 +1651,8 @@ export class ChatModel extends Disposable implements IChatModel { } addRequest(message: IParsedChatRequest, variableData: IChatRequestVariableData, attempt: number, chatAgent?: IChatAgentData, slashCommand?: IChatAgentCommand, confirmation?: string, locationData?: IChatLocationData, attachments?: IChatRequestVariableEntry[], isCompleteAddedRequest?: boolean, modelId?: string): ChatRequestModel { + const editedFileEvents = [...this.currentEditedFileEvents.values()]; + this.currentEditedFileEvents.clear(); const request = new ChatRequestModel({ session: this, message, @@ -1620,7 +1663,8 @@ export class ChatModel extends Disposable implements IChatModel { locationData, attachedContext: attachments, isCompleteAddedRequest, - modelId + modelId, + editedFileEvents: editedFileEvents.length ? editedFileEvents : undefined, }); request.response = new ChatResponseModel({ responseContent: [], @@ -1804,6 +1848,7 @@ export class ChatModel extends Disposable implements IChatModel { codeCitations: r.response?.codeCitations, timestamp: r.timestamp, confirmation: r.confirmation, + editedFileEvents: r.editedFileEvents, }; }), }; @@ -1882,3 +1927,14 @@ export function getCodeCitationsMessage(citations: ReadonlyArray { @@ -761,7 +767,8 @@ export class ChatService extends Disposable implements IChatService { acceptedConfirmationData: options?.acceptedConfirmationData, rejectedConfirmationData: options?.rejectedConfirmationData, userSelectedModelId: options?.userSelectedModelId, - userSelectedTools: options?.userSelectedTools + userSelectedTools: options?.userSelectedTools, + editedFileEvents: request.editedFileEvents } satisfies IChatAgentRequest; }; @@ -998,7 +1005,8 @@ export class ChatService extends Disposable implements IChatService { message: promptTextResult.message, command: request.response.slashCommand?.name, variables: updateRanges(request.variableData, promptTextResult.diff), // TODO bit of a hack - location: ChatAgentLocation.Panel + location: ChatAgentLocation.Panel, + editedFileEvents: request.editedFileEvents, }; history.push({ request: historyRequest, response: toChatHistoryContent(request.response.response.value), result: request.response.result ?? {} }); } diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize.0.snap index 7a565b05fd7..954291a13fb 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize.0.snap @@ -101,7 +101,8 @@ contentReferences: [ ], codeCitations: [ ], timestamp: undefined, - confirmation: undefined + confirmation: undefined, + editedFileEvents: undefined } ] } \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize_with_response.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize_with_response.0.snap index 5207cb81433..eac347edaac 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize_with_response.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize_with_response.0.snap @@ -85,7 +85,8 @@ contentReferences: [ ], codeCitations: [ ], timestamp: undefined, - confirmation: undefined + confirmation: undefined, + editedFileEvents: undefined } ] } \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.1.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.1.snap index b8b5830eb3f..6c53d43dcbf 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.1.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.1.snap @@ -108,7 +108,8 @@ contentReferences: [ ], codeCitations: [ ], timestamp: undefined, - confirmation: undefined + confirmation: undefined, + editedFileEvents: undefined }, { requestId: undefined, @@ -162,7 +163,8 @@ contentReferences: [ ], codeCitations: [ ], timestamp: undefined, - confirmation: undefined + confirmation: undefined, + editedFileEvents: undefined } ] } \ No newline at end of file diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_sendRequest_fails.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_sendRequest_fails.0.snap index 0f10279e625..f6a351338de 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_sendRequest_fails.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_sendRequest_fails.0.snap @@ -85,7 +85,8 @@ contentReferences: [ ], codeCitations: [ ], timestamp: undefined, - confirmation: undefined + confirmation: undefined, + editedFileEvents: undefined } ] } \ No newline at end of file diff --git a/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts b/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts index d056a6f0711..43010b7300c 100644 --- a/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts +++ b/src/vscode-dts/vscode.proposed.chatParticipantPrivate.d.ts @@ -77,6 +77,67 @@ declare module 'vscode' { * or terminal. Will be `undefined` for the chat panel. */ readonly location2: ChatRequestEditorData | ChatRequestNotebookData | undefined; + + /** + * Events for edited files in this session collected since the last request. + */ + readonly editedFileEvents?: ChatRequestEditedFileEvent[]; + } + + export enum ChatRequestEditedFileEventKind { + Keep = 1, + Undo = 2, + UserModification = 3, + } + + export interface ChatRequestEditedFileEvent { + readonly uri: Uri; + readonly eventKind: ChatRequestEditedFileEventKind; + } + + /** + * ChatRequestTurn + private additions. Note- at runtime this is the SAME as ChatRequestTurn and instanceof is safe. + */ + export class ChatRequestTurn2 { + /** + * The prompt as entered by the user. + * + * Information about references used in this request is stored in {@link ChatRequestTurn.references}. + * + * *Note* that the {@link ChatParticipant.name name} of the participant and the {@link ChatCommand.name command} + * are not part of the prompt. + */ + readonly prompt: string; + + /** + * The id of the chat participant to which this request was directed. + */ + readonly participant: string; + + /** + * The name of the {@link ChatCommand command} that was selected for this request. + */ + readonly command?: string; + + /** + * The references that were used in this message. + */ + readonly references: ChatPromptReference[]; + + /** + * The list of tools were attached to this request. + */ + readonly toolReferences: readonly ChatLanguageModelToolReference[]; + + /** + * Events for edited files in this session collected between the previous request and this one. + */ + readonly editedFileEvents?: ChatRequestEditedFileEvent[]; + + /** + * @hidden + */ + private constructor(prompt: string, command: string | undefined, references: ChatPromptReference[], participant: string, toolReferences: ChatLanguageModelToolReference[], editedFileEvents: ChatRequestEditedFileEvent[] | undefined); } export interface ChatParticipant {