Path: blob/main/src/vs/workbench/contrib/chat/common/editing/chatEditingService.ts
4780 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 { Location, 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 { IChatAgentResult } from '../participants/chatAgents.js';23import { ChatModel, IChatRequestDisablement, IChatResponseModel } from '../model/chatModel.js';24import { IChatMultiDiffData, IChatProgress } from '../chatService/chatService.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;5051//#region related files5253hasRelatedFilesProviders(): boolean;54registerRelatedFilesProvider(handle: number, provider: IChatRelatedFilesProvider): IDisposable;55getRelatedFiles(chatSessionResource: URI, prompt: string, files: URI[], token: CancellationToken): Promise<{ group: string; files: IChatRelatedFile[] }[] | undefined>;5657//#endregion58}5960export interface IChatRequestDraft {61readonly prompt: string;62readonly files: readonly URI[];63}6465export interface IChatRelatedFileProviderMetadata {66readonly description: string;67}6869export interface IChatRelatedFile {70readonly uri: URI;71readonly description: string;72}7374export interface IChatRelatedFilesProvider {75readonly description: string;76provideRelatedFiles(chatRequest: IChatRequestDraft, token: CancellationToken): Promise<IChatRelatedFile[] | undefined>;77}7879export interface WorkingSetDisplayMetadata {80state: ModifiedFileEntryState;81description?: string;82}8384export interface IStreamingEdits {85pushText(edits: TextEdit[], isLastEdits: boolean): void;86pushNotebookCellText(cell: URI, edits: TextEdit[], isLastEdits: boolean): void;87pushNotebook(edits: ICellEditOperation[], isLastEdits: boolean): void;88/** Marks edits as done, idempotent */89complete(): void;90}9192export interface IModifiedEntryTelemetryInfo {93readonly agentId: string | undefined;94readonly command: string | undefined;95readonly sessionResource: URI;96readonly requestId: string;97readonly result: IChatAgentResult | undefined;98readonly modelId: string | undefined;99readonly modeId: 'ask' | 'edit' | 'agent' | 'custom' | 'applyCodeBlock' | undefined;100readonly applyCodeBlockSuggestionId: EditSuggestionId | undefined;101readonly feature: 'sideBarChat' | 'inlineChat' | undefined;102}103104export interface ISnapshotEntry {105readonly resource: URI;106readonly languageId: string;107readonly snapshotUri: URI;108readonly original: string;109readonly current: string;110readonly state: ModifiedFileEntryState;111telemetryInfo: IModifiedEntryTelemetryInfo;112}113114export interface IChatEditingSession extends IDisposable {115readonly isGlobalEditingSession: boolean;116readonly chatSessionResource: URI;117readonly onDidDispose: Event<void>;118readonly state: IObservable<ChatEditingSessionState>;119readonly entries: IObservable<readonly IModifiedFileEntry[]>;120/** Requests disabled by undo/redo in the session */121readonly requestDisablement: IObservable<IChatRequestDisablement[]>;122123show(previousChanges?: boolean): Promise<void>;124accept(...uris: URI[]): Promise<void>;125reject(...uris: URI[]): Promise<void>;126getEntry(uri: URI): IModifiedFileEntry | undefined;127readEntry(uri: URI, reader: IReader): IModifiedFileEntry | undefined;128129restoreSnapshot(requestId: string, stopId: string | undefined): Promise<void>;130131/**132* Marks all edits to the given resources as agent edits until133* {@link stopExternalEdits} is called with the same ID. This is used for134* agents that make changes on-disk rather than streaming edits through the135* chat session.136*/137startExternalEdits(responseModel: IChatResponseModel, operationId: number, resources: URI[], undoStopId: string): Promise<IChatProgress[]>;138stopExternalEdits(responseModel: IChatResponseModel, operationId: number): Promise<IChatProgress[]>;139140/**141* Gets the snapshot URI of a file at the request and _after_ changes made in the undo stop.142* @param uri File in the workspace143*/144getSnapshotUri(requestId: string, uri: URI, stopId: string | undefined): URI | undefined;145146getSnapshotContents(requestId: string, uri: URI, stopId: string | undefined): Promise<VSBuffer | undefined>;147getSnapshotModel(requestId: string, undoStop: string | undefined, snapshotUri: URI): Promise<ITextModel | null>;148149/**150* Will lead to this object getting disposed151*/152stop(clearState?: boolean): Promise<void>;153154/**155* Starts making edits to the resource.156* @param resource URI that's being edited157* @param responseModel The response model making the edits158* @param inUndoStop The undo stop the edits will be grouped in159*/160startStreamingEdits(resource: URI, responseModel: IChatResponseModel, inUndoStop: string | undefined): IStreamingEdits;161162/**163* Gets the document diff of a change made to a URI between one undo stop and164* the next one.165* @returns The observable or undefined if there is no diff between the stops.166*/167getEntryDiffBetweenStops(uri: URI, requestId: string | undefined, stopId: string | undefined): IObservable<IEditSessionEntryDiff | undefined> | undefined;168169/**170* Gets the document diff of a change made to a URI between one request to another one.171* @returns The observable or undefined if there is no diff between the requests.172*/173getEntryDiffBetweenRequests(uri: URI, startRequestIs: string, stopRequestId: string): IObservable<IEditSessionEntryDiff | undefined>;174175/**176* Gets the diff of each file modified in this session, comparing the initial177* baseline to the current state.178*/179getDiffsForFilesInSession(): IObservable<readonly IEditSessionEntryDiff[]>;180181/**182* Gets the diff of each file modified in the request.183*/184getDiffsForFilesInRequest(requestId: string): IObservable<readonly IEditSessionEntryDiff[]>;185186/**187* Whether there are any edits made in the given request.188*/189hasEditsInRequest(requestId: string, reader?: IReader): boolean;190191/**192* Gets the aggregated diff stats for all files modified in this session.193*/194getDiffForSession(): IObservable<IEditSessionDiffStats>;195196readonly canUndo: IObservable<boolean>;197readonly canRedo: IObservable<boolean>;198undoInteraction(): Promise<void>;199redoInteraction(): Promise<void>;200}201202export function chatEditingSessionIsReady(session: IChatEditingSession): Promise<void> {203return new Promise<void>(resolve => {204autorunSelfDisposable(reader => {205const state = session.state.read(reader);206if (state !== ChatEditingSessionState.Initial) {207reader.dispose();208resolve();209}210});211});212}213214export function editEntriesToMultiDiffData(entriesObs: IObservable<readonly IEditSessionEntryDiff[]>): IChatMultiDiffData {215return {216kind: 'multiDiffData',217collapsed: true,218multiDiffData: entriesObs.map(entries => ({219title: localize('chatMultidiff.autoGenerated', 'Changes to {0} files', entries.length),220resources: entries.map(entry => ({221originalUri: entry.originalURI,222modifiedUri: entry.modifiedURI,223goToFileUri: entry.modifiedURI,224added: entry.added,225removed: entry.removed,226}))227})),228};229}230231export function awaitCompleteChatEditingDiff(diff: IObservable<IEditSessionEntryDiff>, token?: CancellationToken): Promise<IEditSessionEntryDiff>;232export function awaitCompleteChatEditingDiff(diff: IObservable<readonly IEditSessionEntryDiff[]>, token?: CancellationToken): Promise<readonly IEditSessionEntryDiff[]>;233export function awaitCompleteChatEditingDiff(diff: IObservable<readonly IEditSessionEntryDiff[] | IEditSessionEntryDiff>, token?: CancellationToken): Promise<readonly IEditSessionEntryDiff[] | IEditSessionEntryDiff> {234return new Promise<readonly IEditSessionEntryDiff[] | IEditSessionEntryDiff>((resolve, reject) => {235autorunSelfDisposable(reader => {236if (token) {237if (token.isCancellationRequested) {238reader.dispose();239return reject(new CancellationError());240}241reader.store.add(token.onCancellationRequested(() => {242reader.dispose();243reject(new CancellationError());244}));245}246247const current = diff.read(reader);248if (current instanceof Array) {249if (!current.some(c => c.isBusy)) {250reader.dispose();251resolve(current);252}253} else if (!current.isBusy) {254reader.dispose();255resolve(current);256}257});258});259}260261export interface IEditSessionDiffStats {262/** Added data (e.g. line numbers) to show in the UI */263added: number;264/** Removed data (e.g. line numbers) to show in the UI */265removed: number;266}267268export interface IEditSessionEntryDiff extends IEditSessionDiffStats {269/** LHS and RHS of a diff editor, if opened: */270originalURI: URI;271modifiedURI: URI;272273/** Diff state information: */274quitEarly: boolean;275identical: boolean;276277/** True if nothing else will be added to this diff. */278isFinal: boolean;279280/** True if the diff is currently being computed or updated. */281isBusy: boolean;282}283284export function emptySessionEntryDiff(originalURI: URI, modifiedURI: URI): IEditSessionEntryDiff {285return {286originalURI,287modifiedURI,288added: 0,289removed: 0,290quitEarly: false,291identical: false,292isFinal: false,293isBusy: false,294};295}296297export const enum ModifiedFileEntryState {298Modified,299Accepted,300Rejected,301}302303/**304* Represents a part of a change305*/306export interface IModifiedFileEntryChangeHunk {307accept(): Promise<boolean>;308reject(): Promise<boolean>;309}310311export interface IModifiedFileEntryEditorIntegration extends IDisposable {312313/**314* The index of a change315*/316currentIndex: IObservable<number>;317318/**319* Reveal the first (`true`) or last (`false`) change320*/321reveal(firstOrLast: boolean, preserveFocus?: boolean): void;322323/**324* Go to next change and increate `currentIndex`325* @param wrap When at the last, start over again or not326* @returns If it went next327*/328next(wrap: boolean): boolean;329330/**331* @see `next`332*/333previous(wrap: boolean): boolean;334335/**336* Enable the accessible diff viewer for this editor337*/338enableAccessibleDiffView(): void;339340/**341* Accept the change given or the nearest342* @param change An opaque change object343*/344acceptNearestChange(change?: IModifiedFileEntryChangeHunk): Promise<void>;345346/**347* @see `acceptNearestChange`348*/349rejectNearestChange(change?: IModifiedFileEntryChangeHunk): Promise<void>;350351/**352* Toggle between diff-editor and normal editor353* @param change An opaque change object354* @param show Optional boolean to control if the diff should show355*/356toggleDiff(change: IModifiedFileEntryChangeHunk | undefined, show?: boolean): Promise<void>;357}358359export interface IModifiedFileEntry {360readonly entryId: string;361readonly originalURI: URI;362readonly modifiedURI: URI;363364readonly lastModifyingRequestId: string;365366readonly state: IObservable<ModifiedFileEntryState>;367readonly isCurrentlyBeingModifiedBy: IObservable<{ responseModel: IChatResponseModel; undoStopId: string | undefined } | undefined>;368readonly lastModifyingResponse: IObservable<IChatResponseModel | undefined>;369readonly rewriteRatio: IObservable<number>;370371readonly waitsForLastEdits: IObservable<boolean>;372373accept(): Promise<void>;374reject(): Promise<void>;375376reviewMode: IObservable<boolean>;377autoAcceptController: IObservable<{ total: number; remaining: number; cancel(): void } | undefined>;378enableReviewModeUntilSettled(): void;379380/**381* Number of changes for this file382*/383readonly changesCount: IObservable<number>;384385/**386* Diff information for this entry387*/388readonly diffInfo?: IObservable<IDocumentDiff>;389390/**391* Number of lines added in this entry.392*/393readonly linesAdded?: IObservable<number>;394395/**396* Number of lines removed in this entry397*/398readonly linesRemoved?: IObservable<number>;399400getEditorIntegration(editor: IEditorPane): IModifiedFileEntryEditorIntegration;401hasModificationAt(location: Location): boolean;402/**403* Gets the document diff info, waiting for any ongoing promises to flush.404*/405getDiffInfo?(): Promise<IDocumentDiff>;406}407408export interface IChatEditingSessionStream {409textEdits(resource: URI, textEdits: TextEdit[], isLastEdits: boolean, responseModel: IChatResponseModel): void;410notebookEdits(resource: URI, edits: ICellEditOperation[], isLastEdits: boolean, responseModel: IChatResponseModel): void;411}412413export const enum ChatEditingSessionState {414Initial = 0,415StreamingEdits = 1,416Idle = 2,417Disposed = 3418}419420export const CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME = 'chat-editing-multi-diff-source';421422export const chatEditingWidgetFileStateContextKey = new RawContextKey<ModifiedFileEntryState>('chatEditingWidgetFileState', undefined, localize('chatEditingWidgetFileState', "The current state of the file in the chat editing widget"));423export const chatEditingAgentSupportsReadonlyReferencesContextKey = new RawContextKey<boolean>('chatEditingAgentSupportsReadonlyReferences', undefined, localize('chatEditingAgentSupportsReadonlyReferences', "Whether the chat editing agent supports readonly references (temporary)"));424export const decidedChatEditingResourceContextKey = new RawContextKey<string[]>('decidedChatEditingResource', []);425export const chatEditingResourceContextKey = new RawContextKey<string | undefined>('chatEditingResource', undefined);426export const inChatEditingSessionContextKey = new RawContextKey<boolean | undefined>('inChatEditingSession', undefined);427export const hasUndecidedChatEditingResourceContextKey = new RawContextKey<boolean | undefined>('hasUndecidedChatEditingResource', false);428export const hasAppliedChatEditsContextKey = new RawContextKey<boolean | undefined>('hasAppliedChatEdits', false);429export const applyingChatEditsFailedContextKey = new RawContextKey<boolean | undefined>('applyingChatEditsFailed', false);430431export const chatEditingMaxFileAssignmentName = 'chatEditingSessionFileLimit';432export const defaultChatEditingMaxFileLimit = 10;433434export const enum ChatEditKind {435Created,436Modified,437}438439export interface IChatEditingActionContext {440// The chat session that this editing session is associated with441sessionResource: URI;442}443444export function isChatEditingActionContext(thing: unknown): thing is IChatEditingActionContext {445return typeof thing === 'object' && !!thing && hasKey(thing, { sessionResource: true });446}447448export function getMultiDiffSourceUri(session: IChatEditingSession, showPreviousChanges?: boolean): URI {449return URI.from({450scheme: CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME,451authority: encodeHex(VSBuffer.fromString(session.chatSessionResource.toString())),452query: showPreviousChanges ? 'previous' : undefined,453});454}455456export function parseChatMultiDiffUri(uri: URI): { chatSessionResource: URI; showPreviousChanges: boolean } {457const chatSessionResource = URI.parse(decodeHex(uri.authority).toString());458const showPreviousChanges = uri.query === 'previous';459460return { chatSessionResource, showPreviousChanges };461}462463464