Add editedFileEvents (#246996)

* Start plumbing working set events

* more

* More fixes

* Update tests
This commit is contained in:
Rob Lourens
2025-04-20 09:54:07 -07:00
committed by GitHub
parent 676fadad06
commit 714f54156f
16 changed files with 194 additions and 35 deletions

View File

@@ -5,7 +5,7 @@
import * as assert from 'assert'; import * as assert from 'assert';
import 'mocha'; 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'; import { DeferredPromise, asPromise, assertNoRpc, closeAllEditors, delay, disposeAll } from '../utils';
suite('chat', () => { suite('chat', () => {
@@ -71,6 +71,7 @@ suite('chat', () => {
assert.strictEqual(request.context.history.length, 2); assert.strictEqual(request.context.history.length, 2);
assert.strictEqual(request.context.history[0].participant, 'api-test.participant'); assert.strictEqual(request.context.history[0].participant, 'api-test.participant');
assert.strictEqual(request.context.history[0].command, 'hello'); assert.strictEqual(request.context.history[0].command, 'hello');
assert.ok(request.context.history[0] instanceof ChatRequestTurn && request.context.history[0] instanceof ChatRequestTurn2);
deferred.complete(); deferred.complete();
} }
} catch (e) { } catch (e) {

View File

@@ -1800,11 +1800,13 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
ChatResponseExtensionsPart: extHostTypes.ChatResponseExtensionsPart, ChatResponseExtensionsPart: extHostTypes.ChatResponseExtensionsPart,
ChatResponseReferencePartStatusKind: extHostTypes.ChatResponseReferencePartStatusKind, ChatResponseReferencePartStatusKind: extHostTypes.ChatResponseReferencePartStatusKind,
ChatRequestTurn: extHostTypes.ChatRequestTurn, ChatRequestTurn: extHostTypes.ChatRequestTurn,
ChatRequestTurn2: extHostTypes.ChatRequestTurn,
ChatResponseTurn: extHostTypes.ChatResponseTurn, ChatResponseTurn: extHostTypes.ChatResponseTurn,
ChatLocation: extHostTypes.ChatLocation, ChatLocation: extHostTypes.ChatLocation,
ChatRequestEditorData: extHostTypes.ChatRequestEditorData, ChatRequestEditorData: extHostTypes.ChatRequestEditorData,
ChatRequestNotebookData: extHostTypes.ChatRequestNotebookData, ChatRequestNotebookData: extHostTypes.ChatRequestNotebookData,
ChatReferenceBinaryData: extHostTypes.ChatReferenceBinaryData, ChatReferenceBinaryData: extHostTypes.ChatReferenceBinaryData,
ChatRequestEditedFileEventKind: extHostTypes.ChatRequestEditedFileEventKind,
LanguageModelChatMessageRole: extHostTypes.LanguageModelChatMessageRole, LanguageModelChatMessageRole: extHostTypes.LanguageModelChatMessageRole,
LanguageModelChatMessage: extHostTypes.LanguageModelChatMessage, LanguageModelChatMessage: extHostTypes.LanguageModelChatMessage,
LanguageModelChatMessage2: extHostTypes.LanguageModelChatMessage2, LanguageModelChatMessage2: extHostTypes.LanguageModelChatMessage2,

View File

@@ -410,8 +410,7 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS
const { request, location, history } = await this._createRequest(requestDto, context, detector.extension); const { request, location, history } = await this._createRequest(requestDto, context, detector.extension);
const model = await this.getModelForRequest(request, detector.extension); const model = await this.getModelForRequest(request, detector.extension);
const includeInteractionId = isProposedApiEnabled(detector.extension, 'chatParticipantPrivate'); const extRequest = typeConvert.ChatAgentRequest.to(request, location, model, this.getDiagnosticsWhenEnabled(detector.extension), this.getToolsForRequest(detector.extension, request), detector.extension);
const extRequest = typeConvert.ChatAgentRequest.to(includeInteractionId ? request : { ...request, requestId: '' }, location, model, this.getDiagnosticsWhenEnabled(detector.extension), this.getToolsForRequest(detector.extension, request));
return detector.provider.provideParticipantDetection( return detector.provider.provideParticipantDetection(
extRequest, extRequest,
@@ -495,13 +494,13 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS
stream = new ChatAgentResponseStream(agent.extension, request, this._proxy, this._commands.converter, sessionDisposables); stream = new ChatAgentResponseStream(agent.extension, request, this._proxy, this._commands.converter, sessionDisposables);
const model = await this.getModelForRequest(request, agent.extension); const model = await this.getModelForRequest(request, agent.extension);
const includeInteractionId = isProposedApiEnabled(agent.extension, 'chatParticipantPrivate');
const extRequest = typeConvert.ChatAgentRequest.to( const extRequest = typeConvert.ChatAgentRequest.to(
includeInteractionId ? request : { ...request, requestId: '' }, request,
location, location,
model, model,
this.getDiagnosticsWhenEnabled(agent.extension), this.getDiagnosticsWhenEnabled(agent.extension),
this.getToolsForRequest(agent.extension, request) this.getToolsForRequest(agent.extension, request),
agent.extension
); );
inFlightRequest = { requestId: requestDto.requestId, extRequest }; inFlightRequest = { requestId: requestDto.requestId, extRequest };
this._inFlightRequests.add(inFlightRequest); this._inFlightRequests.add(inFlightRequest);
@@ -585,7 +584,8 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS
const toolReferences = h.request.variables.variables const toolReferences = h.request.variables.variables
.filter(v => v.kind === 'tool') .filter(v => v.kind === 'tool')
.map(typeConvert.ChatLanguageModelToolReference.to); .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); res.push(turn);
// RESPONSE turn // RESPONSE turn

View File

@@ -34,7 +34,7 @@ import * as languageSelector from '../../../editor/common/languageSelector.js';
import * as languages from '../../../editor/common/languages.js'; import * as languages from '../../../editor/common/languages.js';
import { EndOfLineSequence, TrackedRangeStickiness } from '../../../editor/common/model.js'; import { EndOfLineSequence, TrackedRangeStickiness } from '../../../editor/common/model.js';
import { ITextEditorOptions } from '../../../platform/editor/common/editor.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 { IMarkerData, IRelatedInformation, MarkerSeverity, MarkerTag } from '../../../platform/markers/common/markers.js';
import { ProgressLocation as MainProgressLocation } from '../../../platform/progress/common/progress.js'; import { ProgressLocation as MainProgressLocation } from '../../../platform/progress/common/progress.js';
import { DEFAULT_EDITOR_ASSOCIATION, SaveReason } from '../../common/editor.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 { 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 { EditorGroupColumn } from '../../services/editor/common/editorGroupColumn.js';
import { ACTIVE_GROUP, SIDE_GROUP } from '../../services/editor/common/editorService.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 { Dto } from '../../services/extensions/common/proxyIdentifier.js';
import * as extHostProtocol from './extHost.protocol.js'; import * as extHostProtocol from './extHost.protocol.js';
import { CommandsConverter } from './extHostCommands.js'; import { CommandsConverter } from './extHostCommands.js';
@@ -2897,10 +2897,11 @@ export namespace ChatResponsePart {
} }
export namespace ChatAgentRequest { 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 toolReferences = request.variables.variables.filter(v => v.kind === 'tool');
const variableReferences = 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, prompt: request.message,
command: request.command, command: request.command,
attempt: request.attempt ?? 0, attempt: request.attempt ?? 0,
@@ -2914,16 +2915,28 @@ export namespace ChatAgentRequest {
location2, location2,
toolInvocationToken: Object.freeze({ sessionId: request.sessionId }) as never, toolInvocationToken: Object.freeze({ sessionId: request.sessionId }) as never,
tools, tools,
model model,
editedFileEvents: request.editedFileEvents,
}; };
if (request.requestId) {
return { if (!isProposedApiEnabled(extension, 'chatParticipantPrivate')) {
...requestWithoutId, delete (requestWithAllProps as any).id;
id: request.requestId 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;
} }
} }

View File

@@ -4525,6 +4525,12 @@ export enum ChatEditingSessionActionOutcome {
Saved = 3 Saved = 3
} }
export enum ChatRequestEditedFileEventKind {
Keep = 1,
Undo = 2,
UserModification = 3,
}
//#endregion //#endregion
//#region Interactive Editor //#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( constructor(
readonly prompt: string, readonly prompt: string,
readonly command: string | undefined, readonly command: string | undefined,
readonly references: vscode.ChatPromptReference[], readonly references: vscode.ChatPromptReference[],
readonly participant: string, readonly participant: string,
readonly toolReferences: vscode.ChatLanguageModelToolReference[] readonly toolReferences: vscode.ChatLanguageModelToolReference[],
readonly editedFileEvents?: vscode.ChatRequestEditedFileEvent[]
) { } ) { }
} }

View File

@@ -236,7 +236,6 @@ export class ChatEditingModifiedDocumentEntry extends AbstractChatEditingModifie
const e_sum = this._edit; const e_sum = this._edit;
const e_ai = edit; const e_ai = edit;
this._edit = e_sum.compose(e_ai); this._edit = e_sum.compose(e_ai);
} else { } else {
// e_ai // e_ai
@@ -269,6 +268,7 @@ export class ChatEditingModifiedDocumentEntry extends AbstractChatEditingModifie
} }
this._allEditsAreFromUs = false; this._allEditsAreFromUs = false;
this._userEditScheduler.schedule();
this._updateDiffInfoSeq(); this._updateDiffInfoSeq();
const didResetToOriginalContent = this.modifiedModel.getValue() === this.initialContent; const didResetToOriginalContent = this.modifiedModel.getValue() === this.initialContent;
@@ -348,6 +348,7 @@ export class ChatEditingModifiedDocumentEntry extends AbstractChatEditingModifie
await this._updateDiffInfoSeq(); await this._updateDiffInfoSeq();
if (this._diffInfo.get().identical) { if (this._diffInfo.get().identical) {
this._stateObs.set(ModifiedFileEntryState.Accepted, undefined); this._stateObs.set(ModifiedFileEntryState.Accepted, undefined);
this._notifyAction('accepted');
} }
this._accessibilitySignalService.playSignal(AccessibilitySignal.editsKept, { allowManyInParallel: true }); this._accessibilitySignalService.playSignal(AccessibilitySignal.editsKept, { allowManyInParallel: true });
return true; return true;
@@ -366,6 +367,7 @@ export class ChatEditingModifiedDocumentEntry extends AbstractChatEditingModifie
await this._updateDiffInfoSeq(); await this._updateDiffInfoSeq();
if (this._diffInfo.get().identical) { if (this._diffInfo.get().identical) {
this._stateObs.set(ModifiedFileEntryState.Rejected, undefined); this._stateObs.set(ModifiedFileEntryState.Rejected, undefined);
this._notifyAction('rejected');
} }
this._accessibilitySignalService.playSignal(AccessibilitySignal.editsUndone, { allowManyInParallel: true }); this._accessibilitySignalService.playSignal(AccessibilitySignal.editsUndone, { allowManyInParallel: true });
return true; return true;

View File

@@ -3,6 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information. * 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 { Emitter } from '../../../../../base/common/event.js';
import { Disposable, DisposableMap, MutableDisposable } from '../../../../../base/common/lifecycle.js'; import { Disposable, DisposableMap, MutableDisposable } from '../../../../../base/common/lifecycle.js';
import { Schemas } from '../../../../../base/common/network.js'; import { Schemas } from '../../../../../base/common/network.js';
@@ -81,6 +82,8 @@ export abstract class AbstractChatEditingModifiedFileEntry extends Disposable im
readonly abstract originalURI: URI; readonly abstract originalURI: URI;
protected readonly _userEditScheduler = this._register(new RunOnceScheduler(() => this._notifyAction('userModified'), 1000));
constructor( constructor(
readonly modifiedURI: URI, readonly modifiedURI: URI,
protected _telemetryInfo: IModifiedEntryTelemetryInfo, protected _telemetryInfo: IModifiedEntryTelemetryInfo,
@@ -189,7 +192,7 @@ export abstract class AbstractChatEditingModifiedFileEntry extends Disposable im
protected abstract _doReject(tx: ITransaction | undefined): Promise<void>; protected abstract _doReject(tx: ITransaction | undefined): Promise<void>;
private _notifyAction(outcome: 'accepted' | 'rejected') { protected _notifyAction(outcome: 'accepted' | 'rejected' | 'userModified') {
this._chatService.notifyUserAction({ this._chatService.notifyUserAction({
action: { kind: 'chatEditingSessionAction', uri: this.modifiedURI, hasRemainingEdits: false, outcome }, action: { kind: 'chatEditingSessionAction', uri: this.modifiedURI, hasRemainingEdits: false, outcome },
agentId: this._telemetryInfo.agentId, agentId: this._telemetryInfo.agentId,

View File

@@ -24,7 +24,7 @@ import { IProductService } from '../../../../platform/product/common/productServ
import { asJson, IRequestService } from '../../../../platform/request/common/request.js'; import { asJson, IRequestService } from '../../../../platform/request/common/request.js';
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
import { ChatContextKeys } from './chatContextKeys.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 { IRawChatCommandContribution } from './chatParticipantContribTypes.js';
import { IChatFollowup, IChatLocationData, IChatProgress, IChatResponseErrorDetails, IChatTaskDto } from './chatService.js'; import { IChatFollowup, IChatLocationData, IChatProgress, IChatResponseErrorDetails, IChatTaskDto } from './chatService.js';
import { ChatAgentLocation, ChatMode } from './constants.js'; import { ChatAgentLocation, ChatMode } from './constants.js';
@@ -138,6 +138,7 @@ export interface IChatAgentRequest {
rejectedConfirmationData?: any[]; rejectedConfirmationData?: any[];
userSelectedModelId?: string; userSelectedModelId?: string;
userSelectedTools?: string[]; userSelectedTools?: string[];
editedFileEvents?: IChatAgentEditedFileEvent[];
} }
export interface IChatQuestion { export interface IChatQuestion {

View File

@@ -9,6 +9,7 @@ import { Codicon } from '../../../../base/common/codicons.js';
import { Emitter, Event } from '../../../../base/common/event.js'; import { Emitter, Event } from '../../../../base/common/event.js';
import { IMarkdownString, MarkdownString, isMarkdownString } from '../../../../base/common/htmlContent.js'; import { IMarkdownString, MarkdownString, isMarkdownString } from '../../../../base/common/htmlContent.js';
import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js'; import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js';
import { ResourceMap } from '../../../../base/common/map.js';
import { revive } from '../../../../base/common/marshalling.js'; import { revive } from '../../../../base/common/marshalling.js';
import { Schemas } from '../../../../base/common/network.js'; import { Schemas } from '../../../../base/common/network.js';
import { equals } from '../../../../base/common/objects.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 { IChatAgentCommand, IChatAgentData, IChatAgentResult, IChatAgentService, reviveSerializedAgent } from './chatAgents.js';
import { IChatEditingService, IChatEditingSession } from './chatEditingService.js'; import { IChatEditingService, IChatEditingSession } from './chatEditingService.js';
import { ChatRequestTextPart, IParsedChatRequest, reviveParsedChatRequest } from './chatParserTypes.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 { IChatRequestVariableValue } from './chatVariables.js';
import { ChatAgentLocation, ChatMode } from './constants.js'; import { ChatAgentLocation, ChatMode } from './constants.js';
@@ -248,6 +249,7 @@ export interface IChatRequestModel {
readonly attachedContext?: IChatRequestVariableEntry[]; readonly attachedContext?: IChatRequestVariableEntry[];
readonly isCompleteAddedRequest: boolean; readonly isCompleteAddedRequest: boolean;
readonly response?: IChatResponseModel; readonly response?: IChatResponseModel;
readonly editedFileEvents?: IChatAgentEditedFileEvent[];
shouldBeRemovedOnSend: IChatRequestDisablement | undefined; shouldBeRemovedOnSend: IChatRequestDisablement | undefined;
} }
@@ -389,6 +391,7 @@ export interface IChatRequestModelParameters {
isCompleteAddedRequest?: boolean; isCompleteAddedRequest?: boolean;
modelId?: string; modelId?: string;
restoredId?: string; restoredId?: string;
editedFileEvents?: IChatAgentEditedFileEvent[];
} }
export class ChatRequestModel implements IChatRequestModel { export class ChatRequestModel implements IChatRequestModel {
@@ -406,6 +409,7 @@ export class ChatRequestModel implements IChatRequestModel {
private readonly _confirmation?: string; private readonly _confirmation?: string;
private readonly _locationData?: IChatLocationData; private readonly _locationData?: IChatLocationData;
private readonly _attachedContext?: IChatRequestVariableEntry[]; private readonly _attachedContext?: IChatRequestVariableEntry[];
private readonly _editedFileEvents?: IChatAgentEditedFileEvent[];
public get session(): ChatModel { public get session(): ChatModel {
return this._session; return this._session;
@@ -443,6 +447,10 @@ export class ChatRequestModel implements IChatRequestModel {
return this._attachedContext; return this._attachedContext;
} }
public get editedFileEvents(): IChatAgentEditedFileEvent[] | undefined {
return this._editedFileEvents;
}
constructor(params: IChatRequestModelParameters) { constructor(params: IChatRequestModelParameters) {
this._session = params.session; this._session = params.session;
this.message = params.message; this.message = params.message;
@@ -455,6 +463,7 @@ export class ChatRequestModel implements IChatRequestModel {
this.isCompleteAddedRequest = params.isCompleteAddedRequest ?? false; this.isCompleteAddedRequest = params.isCompleteAddedRequest ?? false;
this.modelId = params.modelId; this.modelId = params.modelId;
this.id = params.restoredId ?? 'request_' + generateUuid(); this.id = params.restoredId ?? 'request_' + generateUuid();
this._editedFileEvents = params.editedFileEvents;
} }
adoptTo(session: ChatModel) { adoptTo(session: ChatModel) {
@@ -1082,6 +1091,7 @@ export interface ISerializableChatRequestData {
codeCitations?: ReadonlyArray<IChatCodeCitation>; codeCitations?: ReadonlyArray<IChatCodeCitation>;
timestamp?: number; timestamp?: number;
confirmation?: string; confirmation?: string;
editedFileEvents?: IChatAgentEditedFileEvent[];
} }
export interface IExportableChatData { export interface IExportableChatData {
@@ -1437,7 +1447,37 @@ export class ChatModel extends Disposable implements IChatModel {
this.chatEditingService.startOrContinueGlobalEditingSession(this) : this.chatEditingService.startOrContinueGlobalEditingSession(this) :
this.chatEditingService.createEditingSession(this); this.chatEditingService.createEditingSession(this);
this._editingSession = new ObservablePromise(editingSessionPromise); 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<ModifiedFileEntryState>();
// 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<IChatAgentEditedFileEvent>();
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[] { private _deserialize(obj: IExportableChatData): ChatRequestModel[] {
@@ -1463,6 +1503,7 @@ export class ChatModel extends Disposable implements IChatModel {
timestamp: raw.timestamp ?? -1, timestamp: raw.timestamp ?? -1,
restoredId: raw.requestId, restoredId: raw.requestId,
confirmation: raw.confirmation, confirmation: raw.confirmation,
editedFileEvents: raw.editedFileEvents,
}); });
request.shouldBeRemovedOnSend = raw.isHidden ? { requestId: raw.requestId } : raw.shouldBeRemovedOnSend; request.shouldBeRemovedOnSend = raw.isHidden ? { requestId: raw.requestId } : raw.shouldBeRemovedOnSend;
if (raw.response || raw.result || (raw as any).responseErrorDetails) { 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 { 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({ const request = new ChatRequestModel({
session: this, session: this,
message, message,
@@ -1620,7 +1663,8 @@ export class ChatModel extends Disposable implements IChatModel {
locationData, locationData,
attachedContext: attachments, attachedContext: attachments,
isCompleteAddedRequest, isCompleteAddedRequest,
modelId modelId,
editedFileEvents: editedFileEvents.length ? editedFileEvents : undefined,
}); });
request.response = new ChatResponseModel({ request.response = new ChatResponseModel({
responseContent: [], responseContent: [],
@@ -1804,6 +1848,7 @@ export class ChatModel extends Disposable implements IChatModel {
codeCitations: r.response?.codeCitations, codeCitations: r.response?.codeCitations,
timestamp: r.timestamp, timestamp: r.timestamp,
confirmation: r.confirmation, confirmation: r.confirmation,
editedFileEvents: r.editedFileEvents,
}; };
}), }),
}; };
@@ -1882,3 +1927,14 @@ export function getCodeCitationsMessage(citations: ReadonlyArray<IChatCodeCitati
localize('codeCitations', "Similar code found with {0} license types", licenseTypes.size); localize('codeCitations', "Similar code found with {0} license types", licenseTypes.size);
return label; return label;
} }
export enum ChatRequestEditedFileEventKind {
Keep = 1,
Undo = 2,
UserModification = 3,
}
export interface IChatAgentEditedFileEvent {
readonly uri: URI;
readonly eventKind: ChatRequestEditedFileEventKind;
}

View File

@@ -381,7 +381,7 @@ export interface IChatEditingSessionAction {
kind: 'chatEditingSessionAction'; kind: 'chatEditingSessionAction';
uri: URI; uri: URI;
hasRemainingEdits: boolean; hasRemainingEdits: boolean;
outcome: 'accepted' | 'rejected' | 'saved'; outcome: 'accepted' | 'rejected' | 'userModified';
} }
export type ChatUserAction = IChatVoteAction | IChatCopyAction | IChatInsertAction | IChatApplyAction | IChatTerminalAction | IChatCommandAction | IChatFollowupAction | IChatBugReportAction | IChatInlineChatCodeAction | IChatEditingSessionAction; export type ChatUserAction = IChatVoteAction | IChatCopyAction | IChatInsertAction | IChatApplyAction | IChatTerminalAction | IChatCommandAction | IChatFollowupAction | IChatBugReportAction | IChatInlineChatCodeAction | IChatEditingSessionAction;

View File

@@ -288,6 +288,12 @@ export class ChatService extends Disposable implements IChatService {
notifyUserAction(action: IChatUserActionEvent): void { notifyUserAction(action: IChatUserActionEvent): void {
this._chatServiceTelemetry.notifyUserAction(action); this._chatServiceTelemetry.notifyUserAction(action);
this._onDidPerformUserAction.fire(action); this._onDidPerformUserAction.fire(action);
if (action.action.kind === 'chatEditingSessionAction') {
const model = this._sessionModels.get(action.sessionId);
if (model) {
model.notifyEditingAction(action.action);
}
}
} }
async setChatSessionTitle(sessionId: string, title: string): Promise<void> { async setChatSessionTitle(sessionId: string, title: string): Promise<void> {
@@ -761,7 +767,8 @@ export class ChatService extends Disposable implements IChatService {
acceptedConfirmationData: options?.acceptedConfirmationData, acceptedConfirmationData: options?.acceptedConfirmationData,
rejectedConfirmationData: options?.rejectedConfirmationData, rejectedConfirmationData: options?.rejectedConfirmationData,
userSelectedModelId: options?.userSelectedModelId, userSelectedModelId: options?.userSelectedModelId,
userSelectedTools: options?.userSelectedTools userSelectedTools: options?.userSelectedTools,
editedFileEvents: request.editedFileEvents
} satisfies IChatAgentRequest; } satisfies IChatAgentRequest;
}; };
@@ -998,7 +1005,8 @@ export class ChatService extends Disposable implements IChatService {
message: promptTextResult.message, message: promptTextResult.message,
command: request.response.slashCommand?.name, command: request.response.slashCommand?.name,
variables: updateRanges(request.variableData, promptTextResult.diff), // TODO bit of a hack 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 ?? {} }); history.push({ request: historyRequest, response: toChatHistoryContent(request.response.response.value), result: request.response.result ?? {} });
} }

View File

@@ -101,7 +101,8 @@
contentReferences: [ ], contentReferences: [ ],
codeCitations: [ ], codeCitations: [ ],
timestamp: undefined, timestamp: undefined,
confirmation: undefined confirmation: undefined,
editedFileEvents: undefined
} }
] ]
} }

View File

@@ -85,7 +85,8 @@
contentReferences: [ ], contentReferences: [ ],
codeCitations: [ ], codeCitations: [ ],
timestamp: undefined, timestamp: undefined,
confirmation: undefined confirmation: undefined,
editedFileEvents: undefined
} }
] ]
} }

View File

@@ -108,7 +108,8 @@
contentReferences: [ ], contentReferences: [ ],
codeCitations: [ ], codeCitations: [ ],
timestamp: undefined, timestamp: undefined,
confirmation: undefined confirmation: undefined,
editedFileEvents: undefined
}, },
{ {
requestId: undefined, requestId: undefined,
@@ -162,7 +163,8 @@
contentReferences: [ ], contentReferences: [ ],
codeCitations: [ ], codeCitations: [ ],
timestamp: undefined, timestamp: undefined,
confirmation: undefined confirmation: undefined,
editedFileEvents: undefined
} }
] ]
} }

View File

@@ -85,7 +85,8 @@
contentReferences: [ ], contentReferences: [ ],
codeCitations: [ ], codeCitations: [ ],
timestamp: undefined, timestamp: undefined,
confirmation: undefined confirmation: undefined,
editedFileEvents: undefined
} }
] ]
} }

View File

@@ -77,6 +77,67 @@ declare module 'vscode' {
* or terminal. Will be `undefined` for the chat panel. * or terminal. Will be `undefined` for the chat panel.
*/ */
readonly location2: ChatRequestEditorData | ChatRequestNotebookData | undefined; 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 { export interface ChatParticipant {