Path: blob/main/src/vs/workbench/contrib/chat/common/editing/chatEditingService.ts
5258 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 { decodeHex, encodeHex, VSBuffer } from '../../../../../base/common/buffer.js';6import { CancellationToken } from '../../../../../base/common/cancellation.js';7import { CancellationError } from '../../../../../base/common/errors.js';8import { Event } from '../../../../../base/common/event.js';9import { IDisposable } from '../../../../../base/common/lifecycle.js';10import { autorunSelfDisposable, IObservable, IReader } from '../../../../../base/common/observable.js';11import { hasKey } from '../../../../../base/common/types.js';12import { URI } from '../../../../../base/common/uri.js';13import { IDocumentDiff } from '../../../../../editor/common/diff/documentDiffProvider.js';14import { TextEdit } from '../../../../../editor/common/languages.js';15import { ITextModel } from '../../../../../editor/common/model.js';16import { EditSuggestionId } from '../../../../../editor/common/textModelEditSource.js';17import { localize } from '../../../../../nls.js';18import { RawContextKey } from '../../../../../platform/contextkey/common/contextkey.js';19import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js';20import { IEditorPane } from '../../../../common/editor.js';21import { ICellEditOperation } from '../../../notebook/common/notebookCommon.js';22import { IChatMultiDiffData, IChatMultiDiffDataSerialized, IChatProgress, IChatWorkspaceEdit } from '../chatService/chatService.js';23import { ChatModel, IChatRequestDisablement, IChatResponseModel } from '../model/chatModel.js';24import { IChatAgentResult } from '../participants/chatAgents.js';2526export const IChatEditingService = createDecorator<IChatEditingService>('chatEditingService');2728export interface IChatEditingService {2930_serviceBrand: undefined;3132startOrContinueGlobalEditingSession(chatModel: ChatModel): IChatEditingSession;3334getEditingSession(chatSessionResource: URI): IChatEditingSession | undefined;3536/**37* All editing sessions, sorted by recency, e.g the last created session comes first.38*/39readonly editingSessionsObs: IObservable<readonly IChatEditingSession[]>;4041/**42* Creates a new short lived editing session43*/44createEditingSession(chatModel: ChatModel): IChatEditingSession;4546/**47* Creates an editing session with state transferred from the provided session.48*/49transferEditingSession(chatModel: ChatModel, session: IChatEditingSession): IChatEditingSession;50}5152export interface WorkingSetDisplayMetadata {53state: ModifiedFileEntryState;54description?: string;55}5657export interface IStreamingEdits {58pushText(edits: TextEdit[], isLastEdits: boolean): void;59pushNotebookCellText(cell: URI, edits: TextEdit[], isLastEdits: boolean): void;60pushNotebook(edits: ICellEditOperation[], isLastEdits: boolean): void;61/** Marks edits as done, idempotent */62complete(): void;63}6465export interface IModifiedEntryTelemetryInfo {66readonly agentId: string | undefined;67readonly command: string | undefined;68readonly sessionResource: URI;69readonly requestId: string;70readonly result: IChatAgentResult | undefined;71readonly modelId: string | undefined;72readonly modeId: 'ask' | 'edit' | 'agent' | 'custom' | 'applyCodeBlock' | undefined;73readonly applyCodeBlockSuggestionId: EditSuggestionId | undefined;74readonly feature: 'sideBarChat' | 'inlineChat' | undefined;75}7677export interface ISnapshotEntry {78readonly resource: URI;79readonly languageId: string;80readonly snapshotUri: URI;81readonly original: string;82readonly current: string;83readonly state: ModifiedFileEntryState;84telemetryInfo: IModifiedEntryTelemetryInfo;85/** True if this entry represents a deleted file */86readonly isDeleted?: boolean;87}8889export interface IChatEditingSession extends IDisposable {90readonly isGlobalEditingSession: boolean;91readonly chatSessionResource: URI;92readonly onDidDispose: Event<void>;93readonly state: IObservable<ChatEditingSessionState>;94readonly entries: IObservable<readonly IModifiedFileEntry[]>;95/** Requests disabled by undo/redo in the session */96readonly requestDisablement: IObservable<IChatRequestDisablement[]>;9798show(previousChanges?: boolean): Promise<void>;99accept(...uris: URI[]): Promise<void>;100reject(...uris: URI[]): Promise<void>;101getEntry(uri: URI): IModifiedFileEntry | undefined;102readEntry(uri: URI, reader: IReader): IModifiedFileEntry | undefined;103104restoreSnapshot(requestId: string, stopId: string | undefined): Promise<void>;105106/**107* Marks all edits to the given resources as agent edits until108* {@link stopExternalEdits} is called with the same ID. This is used for109* agents that make changes on-disk rather than streaming edits through the110* chat session.111*/112startExternalEdits(responseModel: IChatResponseModel, operationId: number, resources: URI[], undoStopId: string): Promise<IChatProgress[]>;113stopExternalEdits(responseModel: IChatResponseModel, operationId: number): Promise<IChatProgress[]>;114115/**116* Gets the snapshot URI of a file at the request and _after_ changes made in the undo stop.117* @param uri File in the workspace118*/119getSnapshotUri(requestId: string, uri: URI, stopId: string | undefined): URI | undefined;120121getSnapshotContents(requestId: string, uri: URI, stopId: string | undefined): Promise<VSBuffer | undefined>;122getSnapshotModel(requestId: string, undoStop: string | undefined, snapshotUri: URI): Promise<ITextModel | null>;123124/**125* Will lead to this object getting disposed126*/127stop(clearState?: boolean): Promise<void>;128129/**130* Starts making edits to the resource.131* @param resource URI that's being edited132* @param responseModel The response model making the edits133* @param inUndoStop The undo stop the edits will be grouped in134*/135startStreamingEdits(resource: URI, responseModel: IChatResponseModel, inUndoStop: string | undefined): IStreamingEdits;136137/**138* Applies a workspace edit (file deletions, creations, renames).139* @param edit The workspace edit containing file operations140* @param responseModel The response model making the edit141* @param undoStopId The undo stop ID for this edit142*/143applyWorkspaceEdit(edit: IChatWorkspaceEdit, responseModel: IChatResponseModel, undoStopId: string): void;144145/**146* Gets the document diff of a change made to a URI between one undo stop and147* the next one.148* @returns The observable or undefined if there is no diff between the stops.149*/150getEntryDiffBetweenStops(uri: URI, requestId: string | undefined, stopId: string | undefined): IObservable<IEditSessionEntryDiff | undefined> | undefined;151152/**153* Gets the document diff of a change made to a URI between one request to another one.154* @returns The observable or undefined if there is no diff between the requests.155*/156getEntryDiffBetweenRequests(uri: URI, startRequestIs: string, stopRequestId: string): IObservable<IEditSessionEntryDiff | undefined>;157158/**159* Gets the diff of each file modified in this session, comparing the initial160* baseline to the current state.161*/162getDiffsForFilesInSession(): IObservable<readonly IEditSessionEntryDiff[]>;163164/**165* Gets the diff of each file modified in the request.166*/167getDiffsForFilesInRequest(requestId: string): IObservable<readonly IEditSessionEntryDiff[]>;168169/**170* Whether there are any edits made in the given request.171*/172hasEditsInRequest(requestId: string, reader?: IReader): boolean;173174/**175* Gets the aggregated diff stats for all files modified in this session.176*/177getDiffForSession(): IObservable<IEditSessionDiffStats>;178179readonly canUndo: IObservable<boolean>;180readonly canRedo: IObservable<boolean>;181undoInteraction(): Promise<void>;182redoInteraction(): Promise<void>;183184/**185* Triggers generation of explanations for all modified files in the session.186*/187triggerExplanationGeneration(): Promise<void>;188189/**190* Clears any active explanation generation.191*/192clearExplanations(): void;193194/**195* Whether explanations are currently being generated or displayed.196*/197hasExplanations(): boolean;198}199200export function chatEditingSessionIsReady(session: IChatEditingSession): Promise<void> {201return new Promise<void>(resolve => {202autorunSelfDisposable(reader => {203const state = session.state.read(reader);204if (state !== ChatEditingSessionState.Initial) {205reader.dispose();206resolve();207}208});209});210}211212export function editEntriesToMultiDiffData(entriesObs: IObservable<readonly IEditSessionEntryDiff[]>): IChatMultiDiffData {213const multiDiffData = entriesObs.map(entries => ({214title: localize('chatMultidiff.autoGenerated', 'Changes to {0} files', entries.length),215resources: entries.map(entry => ({216originalUri: entry.originalURI,217modifiedUri: entry.modifiedURI,218goToFileUri: entry.modifiedURI,219added: entry.added,220removed: entry.removed,221}))222}));223224return {225kind: 'multiDiffData',226collapsed: true,227multiDiffData,228toJSON(): IChatMultiDiffDataSerialized {229return {230kind: 'multiDiffData',231collapsed: this.collapsed,232multiDiffData: multiDiffData.get(),233};234}235};236}237238export function awaitCompleteChatEditingDiff(diff: IObservable<IEditSessionEntryDiff>, token?: CancellationToken): Promise<IEditSessionEntryDiff>;239export function awaitCompleteChatEditingDiff(diff: IObservable<readonly IEditSessionEntryDiff[]>, token?: CancellationToken): Promise<readonly IEditSessionEntryDiff[]>;240export function awaitCompleteChatEditingDiff(diff: IObservable<readonly IEditSessionEntryDiff[] | IEditSessionEntryDiff>, token?: CancellationToken): Promise<readonly IEditSessionEntryDiff[] | IEditSessionEntryDiff> {241return new Promise<readonly IEditSessionEntryDiff[] | IEditSessionEntryDiff>((resolve, reject) => {242autorunSelfDisposable(reader => {243if (token) {244if (token.isCancellationRequested) {245reader.dispose();246return reject(new CancellationError());247}248reader.store.add(token.onCancellationRequested(() => {249reader.dispose();250reject(new CancellationError());251}));252}253254const current = diff.read(reader);255if (current instanceof Array) {256if (!current.some(c => c.isBusy)) {257reader.dispose();258resolve(current);259}260} else if (!current.isBusy) {261reader.dispose();262resolve(current);263}264});265});266}267268export interface IEditSessionDiffStats {269/** Added data (e.g. line numbers) to show in the UI */270added: number;271/** Removed data (e.g. line numbers) to show in the UI */272removed: number;273}274275export interface IEditSessionEntryDiff extends IEditSessionDiffStats {276/** LHS and RHS of a diff editor, if opened: */277originalURI: URI;278modifiedURI: URI;279280/** Diff state information: */281quitEarly: boolean;282identical: boolean;283284/** True if nothing else will be added to this diff. */285isFinal: boolean;286287/** True if the diff is currently being computed or updated. */288isBusy: boolean;289}290291export function emptySessionEntryDiff(originalURI: URI, modifiedURI: URI): IEditSessionEntryDiff {292return {293originalURI,294modifiedURI,295added: 0,296removed: 0,297quitEarly: false,298identical: false,299isFinal: false,300isBusy: false,301};302}303304export const enum ModifiedFileEntryState {305Modified,306Accepted,307Rejected,308}309310/**311* Represents a part of a change312*/313export interface IModifiedFileEntryChangeHunk {314accept(): Promise<boolean>;315reject(): Promise<boolean>;316}317318export interface IModifiedFileEntryEditorIntegration extends IDisposable {319320/**321* The index of a change322*/323currentIndex: IObservable<number>;324325/**326* Reveal the first (`true`) or last (`false`) change327*/328reveal(firstOrLast: boolean, preserveFocus?: boolean): void;329330/**331* Go to next change and increate `currentIndex`332* @param wrap When at the last, start over again or not333* @returns If it went next334*/335next(wrap: boolean): boolean;336337/**338* @see `next`339*/340previous(wrap: boolean): boolean;341342/**343* Enable the accessible diff viewer for this editor344*/345enableAccessibleDiffView(): void;346347/**348* Accept the change given or the nearest349* @param change An opaque change object350*/351acceptNearestChange(change?: IModifiedFileEntryChangeHunk): Promise<void>;352353/**354* @see `acceptNearestChange`355*/356rejectNearestChange(change?: IModifiedFileEntryChangeHunk): Promise<void>;357358/**359* Toggle between diff-editor and normal editor360* @param change An opaque change object361* @param show Optional boolean to control if the diff should show362*/363toggleDiff(change: IModifiedFileEntryChangeHunk | undefined, show?: boolean): Promise<void>;364}365366export interface IModifiedFileEntry {367readonly entryId: string;368readonly originalURI: URI;369readonly modifiedURI: URI;370readonly isDeletion?: boolean;371372readonly lastModifyingRequestId: string;373374readonly state: IObservable<ModifiedFileEntryState>;375readonly isCurrentlyBeingModifiedBy: IObservable<{ responseModel: IChatResponseModel; undoStopId: string | undefined } | undefined>;376readonly lastModifyingResponse: IObservable<IChatResponseModel | undefined>;377readonly rewriteRatio: IObservable<number>;378379readonly waitsForLastEdits: IObservable<boolean>;380381accept(): Promise<void>;382reject(): Promise<void>;383384reviewMode: IObservable<boolean>;385autoAcceptController: IObservable<{ total: number; remaining: number; cancel(): void } | undefined>;386enableReviewModeUntilSettled(): void;387388/**389* Number of changes for this file390*/391readonly changesCount: IObservable<number>;392393/**394* Diff information for this entry395*/396readonly diffInfo?: IObservable<IDocumentDiff>;397398/**399* Number of lines added in this entry.400*/401readonly linesAdded?: IObservable<number>;402403/**404* Number of lines removed in this entry405*/406readonly linesRemoved?: IObservable<number>;407408getEditorIntegration(editor: IEditorPane): IModifiedFileEntryEditorIntegration;409/**410* Gets the document diff info, waiting for any ongoing promises to flush.411*/412getDiffInfo?(): Promise<IDocumentDiff>;413}414415export interface IChatEditingSessionStream {416textEdits(resource: URI, textEdits: TextEdit[], isLastEdits: boolean, responseModel: IChatResponseModel): void;417notebookEdits(resource: URI, edits: ICellEditOperation[], isLastEdits: boolean, responseModel: IChatResponseModel): void;418}419420export const enum ChatEditingSessionState {421Initial = 0,422StreamingEdits = 1,423Idle = 2,424Disposed = 3425}426427export const CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME = 'chat-editing-multi-diff-source';428429export const chatEditingWidgetFileStateContextKey = new RawContextKey<ModifiedFileEntryState>('chatEditingWidgetFileState', undefined, localize('chatEditingWidgetFileState', "The current state of the file in the chat editing widget"));430export const chatEditingAgentSupportsReadonlyReferencesContextKey = new RawContextKey<boolean>('chatEditingAgentSupportsReadonlyReferences', undefined, localize('chatEditingAgentSupportsReadonlyReferences', "Whether the chat editing agent supports readonly references (temporary)"));431export const decidedChatEditingResourceContextKey = new RawContextKey<string[]>('decidedChatEditingResource', []);432export const chatEditingResourceContextKey = new RawContextKey<string | undefined>('chatEditingResource', undefined);433export const inChatEditingSessionContextKey = new RawContextKey<boolean | undefined>('inChatEditingSession', undefined);434export const hasUndecidedChatEditingResourceContextKey = new RawContextKey<boolean | undefined>('hasUndecidedChatEditingResource', false);435export const hasAppliedChatEditsContextKey = new RawContextKey<boolean | undefined>('hasAppliedChatEdits', false);436export const applyingChatEditsFailedContextKey = new RawContextKey<boolean | undefined>('applyingChatEditsFailed', false);437438export const chatEditingMaxFileAssignmentName = 'chatEditingSessionFileLimit';439export const defaultChatEditingMaxFileLimit = 10;440441export const enum ChatEditKind {442Created,443Modified,444Deleted,445}446447export interface IChatEditingActionContext {448// The chat session that this editing session is associated with449sessionResource: URI;450}451452export function isChatEditingActionContext(thing: unknown): thing is IChatEditingActionContext {453return typeof thing === 'object' && !!thing && hasKey(thing, { sessionResource: true });454}455456export function getMultiDiffSourceUri(session: IChatEditingSession, showPreviousChanges?: boolean): URI {457return URI.from({458scheme: CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME,459authority: encodeHex(VSBuffer.fromString(session.chatSessionResource.toString())),460query: showPreviousChanges ? 'previous' : undefined,461});462}463464export function parseChatMultiDiffUri(uri: URI): { chatSessionResource: URI; showPreviousChanges: boolean } {465const chatSessionResource = URI.parse(decodeHex(uri.authority).toString());466const showPreviousChanges = uri.query === 'previous';467468return { chatSessionResource, showPreviousChanges };469}470471472