Merge branch 'main' into joh/inlineChatEdits

This commit is contained in:
Johannes
2025-01-22 09:55:48 +01:00
12 changed files with 22 additions and 689 deletions

View File

@@ -13,9 +13,7 @@ import { Registry } from '../../../../platform/registry/common/platform.js';
import { LifecyclePhase } from '../../../services/lifecycle/common/lifecycle.js';
import { InlineChatNotebookContribution } from './inlineChatNotebook.js';
import { IWorkbenchContributionsRegistry, registerWorkbenchContribution2, Extensions as WorkbenchExtensions, WorkbenchPhase } from '../../../common/contributions.js';
import { InlineChatSavingServiceImpl } from './inlineChatSavingServiceImpl.js';
import { InlineChatAccessibleView } from './inlineChatAccessibleView.js';
import { IInlineChatSavingService } from './inlineChatSavingService.js';
import { IInlineChatSessionService } from './inlineChatSessionService.js';
import { InlineChatEnabler, InlineChatSessionServiceImpl } from './inlineChatSessionServiceImpl.js';
import { AccessibleViewRegistry } from '../../../../platform/accessibility/browser/accessibleViewRegistry.js';
@@ -34,7 +32,6 @@ registerAction2(StopSessionAction2);
// --- browser
registerSingleton(IInlineChatSessionService, InlineChatSessionServiceImpl, InstantiationType.Delayed);
registerSingleton(IInlineChatSavingService, InlineChatSavingServiceImpl, InstantiationType.Delayed);
registerEditorContribution(INLINE_CHAT_ID, InlineChatController, EditorContributionInstantiation.Eager); // EAGER because of notebook dispose/create of editors

View File

@@ -46,7 +46,6 @@ import { ChatModel, ChatRequestRemovalReason, IChatRequestModel, IChatTextEditGr
import { IChatService } from '../../chat/common/chatService.js';
import { INotebookEditorService } from '../../notebook/browser/services/notebookEditorService.js';
import { CTX_INLINE_CHAT_EDITING, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, CTX_INLINE_CHAT_RESPONSE_TYPE, CTX_INLINE_CHAT_USER_DID_EDIT, CTX_INLINE_CHAT_VISIBLE, INLINE_CHAT_ID, InlineChatConfigKeys, InlineChatResponseType } from '../common/inlineChat.js';
import { IInlineChatSavingService } from './inlineChatSavingService.js';
import { HunkInformation, Session, StashedSession } from './inlineChatSession.js';
import { IInlineChatSessionService } from './inlineChatSessionService.js';
import { InlineChatError } from './inlineChatSessionServiceImpl.js';
@@ -139,7 +138,6 @@ export class InlineChatController implements IEditorContribution {
private readonly _editor: ICodeEditor,
@IInstantiationService private readonly _instaService: IInstantiationService,
@IInlineChatSessionService private readonly _inlineChatSessionService: IInlineChatSessionService,
@IInlineChatSavingService private readonly _inlineChatSavingService: IInlineChatSavingService,
@IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService,
@ILogService private readonly _logService: ILogService,
@IConfigurationService private readonly _configurationService: IConfigurationService,
@@ -964,7 +962,6 @@ export class InlineChatController implements IEditorContribution {
stop: () => this._session!.hunkData.ignoreTextModelNChanges = false,
};
this._inlineChatSavingService.markChanged(this._session);
if (opts) {
await this._strategy.makeProgressiveChanges(editOperations, editsObserver, opts, undoStopBefore);
} else {
@@ -1127,9 +1124,6 @@ export class InlineChatController implements IEditorContribution {
unstashLastSession(): Session | undefined {
const result = this._stashedSession.value?.unstash();
if (result) {
this._inlineChatSavingService.markChanged(result);
}
return result;
}

View File

@@ -1,16 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { Session } from './inlineChatSession.js';
export const IInlineChatSavingService = createDecorator<IInlineChatSavingService>('IInlineChatSavingService ');
export interface IInlineChatSavingService {
_serviceBrand: undefined;
markChanged(session: Session): void;
}

View File

@@ -1,197 +0,0 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Queue } from '../../../../base/common/async.js';
import { CancellationToken } from '../../../../base/common/cancellation.js';
import { DisposableStore, MutableDisposable, combinedDisposable, dispose } from '../../../../base/common/lifecycle.js';
import { localize } from '../../../../nls.js';
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
import { IProgress, IProgressStep } from '../../../../platform/progress/common/progress.js';
import { SaveReason } from '../../../common/editor.js';
import { Session } from './inlineChatSession.js';
import { IInlineChatSessionService } from './inlineChatSessionService.js';
import { InlineChatConfigKeys } from '../common/inlineChat.js';
import { IEditorGroup, IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js';
import { IFilesConfigurationService } from '../../../services/filesConfiguration/common/filesConfigurationService.js';
import { ITextFileService } from '../../../services/textfile/common/textfiles.js';
import { IInlineChatSavingService } from './inlineChatSavingService.js';
import { Iterable } from '../../../../base/common/iterator.js';
import { Schemas } from '../../../../base/common/network.js';
import { CellUri } from '../../notebook/common/notebookCommon.js';
import { IWorkingCopyFileService } from '../../../services/workingCopy/common/workingCopyFileService.js';
import { URI } from '../../../../base/common/uri.js';
import { Event } from '../../../../base/common/event.js';
import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';
import { ILabelService } from '../../../../platform/label/common/label.js';
import { CancellationError } from '../../../../base/common/errors.js';
interface SessionData {
readonly resourceUri: URI;
readonly dispose: () => void;
readonly session: Session;
readonly groupCandidate: IEditorGroup;
}
// TODO@jrieken this duplicates a config key
const key = 'chat.editing.alwaysSaveWithGeneratedChanges';
export class InlineChatSavingServiceImpl implements IInlineChatSavingService {
declare readonly _serviceBrand: undefined;
private readonly _store = new DisposableStore();
private readonly _saveParticipant = this._store.add(new MutableDisposable());
private readonly _sessionData = new Map<Session, SessionData>();
constructor(
@IFilesConfigurationService private readonly _fileConfigService: IFilesConfigurationService,
@IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService,
@ITextFileService private readonly _textFileService: ITextFileService,
@IInlineChatSessionService _inlineChatSessionService: IInlineChatSessionService,
@IConfigurationService private readonly _configService: IConfigurationService,
@IWorkingCopyFileService private readonly _workingCopyFileService: IWorkingCopyFileService,
@IDialogService private readonly _dialogService: IDialogService,
@ILabelService private readonly _labelService: ILabelService,
) {
this._store.add(Event.any(_inlineChatSessionService.onDidEndSession, _inlineChatSessionService.onDidStashSession)(e => {
this._sessionData.get(e.session)?.dispose();
}));
this._store.add(_configService.onDidChangeConfiguration(e => {
if (!e.affectsConfiguration(key) && !e.affectsConfiguration(InlineChatConfigKeys.AcceptedOrDiscardBeforeSave)) {
return;
}
if (this._isDisabled()) {
dispose(this._sessionData.values());
this._sessionData.clear();
}
}));
}
dispose(): void {
this._store.dispose();
dispose(this._sessionData.values());
}
markChanged(session: Session): void {
if (this._isDisabled()) {
return;
}
if (!this._sessionData.has(session)) {
let uri = session.targetUri;
// notebooks: use the notebook-uri because saving happens on the notebook-level
if (uri.scheme === Schemas.vscodeNotebookCell) {
const data = CellUri.parse(uri);
if (!data) {
return;
}
uri = data?.notebook;
}
if (this._sessionData.size === 0) {
this._installSaveParticpant();
}
const saveConfigOverride = this._fileConfigService.disableAutoSave(uri);
this._sessionData.set(session, {
resourceUri: uri,
groupCandidate: this._editorGroupService.activeGroup,
session,
dispose: () => {
saveConfigOverride.dispose();
this._sessionData.delete(session);
if (this._sessionData.size === 0) {
this._saveParticipant.clear();
}
}
});
}
}
private _installSaveParticpant(): void {
const queue = new Queue<void>();
const d1 = this._textFileService.files.addSaveParticipant({
participate: (model, ctx, progress, token) => {
return queue.queue(() => this._participate(ctx.savedFrom ?? model.textEditorModel?.uri, ctx.reason, progress, token));
}
});
const d2 = this._workingCopyFileService.addSaveParticipant({
participate: (workingCopy, ctx, progress, token) => {
return queue.queue(() => this._participate(ctx.savedFrom ?? workingCopy.resource, ctx.reason, progress, token));
}
});
this._saveParticipant.value = combinedDisposable(d1, d2, queue);
}
private async _participate(uri: URI | undefined, reason: SaveReason, progress: IProgress<IProgressStep>, token: CancellationToken): Promise<void> {
if (reason !== SaveReason.EXPLICIT) {
// all saves that we are concerned about are explicit
// because we have disabled auto-save for them
return;
}
if (this._isDisabled()) {
// disabled
return;
}
const sessions = new Map<Session, SessionData>();
for (const [session, data] of this._sessionData) {
if (uri?.toString() === data.resourceUri.toString()) {
sessions.set(session, data);
}
}
if (sessions.size === 0) {
return;
}
let message: string;
if (sessions.size === 1) {
const session = Iterable.first(sessions.values())!.session;
const agentName = session.agent.fullName;
const filelabel = this._labelService.getUriBasenameLabel(session.textModelN.uri);
message = localize('message.1', "Do you want to save the changes {0} made in {1}?", agentName, filelabel);
} else {
const labels = Array.from(Iterable.map(sessions.values(), i => this._labelService.getUriBasenameLabel(i.session.textModelN.uri)));
message = localize('message.2', "Do you want to save the changes inline chat made in {0}?", labels.join(', '));
}
const result = await this._dialogService.confirm({
message,
detail: localize('detail', "AI-generated changes may be incorrect and should be reviewed before saving."),
primaryButton: localize('save', "Save"),
cancelButton: localize('discard', "Cancel"),
checkbox: {
label: localize('config', "Always save with AI-generated changes without asking"),
checked: false
}
});
if (!result.confirmed) {
// cancel the save
throw new CancellationError();
}
if (result.checkboxChecked) {
// remember choice
this._configService.updateValue(key, true);
}
}
private _isDisabled() {
return this._configService.getValue<boolean>(InlineChatConfigKeys.AcceptedOrDiscardBeforeSave) === true || this._configService.getValue(key);
}
}