customizations editor: hook up dirty state for built-in customization editing (#305300)

* Enhance AI Customization Management Editor with confirmation handling and dirty state tracking

* Improve dialog handling by waiting for keyboard events to propagate before opening confirmation

* Refactor AI Customization Management Editor to replace confirmation handling with save handling

* Add escape key handling to close dialog only if previously pressed

* Address review: guard save() against auto-save, reset editor dirty baseline

- Only run the pick-target save flow on explicit saves (not auto-save
  from focus/window changes)
- Reset _editorContentChanged after successful save so the embedded
  editor stays clean until the next edit (updateEditorActionButton
  propagates this to input.setDirty via updateInputDirtyState)

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Josh Spicer
2026-03-26 22:49:02 +00:00
committed by GitHub
parent b04a4405e9
commit 819666ef49
3 changed files with 93 additions and 2 deletions

View File

@@ -328,9 +328,14 @@ export class Dialog extends Disposable {
// Handle keyboard events globally: Tab, Arrow-Left/Right
const window = getWindow(this.container);
let sawEscapeKeyDown = false;
this._register(addDisposableListener(window, 'keydown', e => {
const evt = new StandardKeyboardEvent(e);
if (evt.equals(KeyCode.Escape)) {
sawEscapeKeyDown = true;
}
if (evt.equals(KeyMod.Alt)) {
evt.preventDefault();
}
@@ -470,7 +475,7 @@ export class Dialog extends Disposable {
EventHelper.stop(e, true);
const evt = new StandardKeyboardEvent(e);
if (!this.options.disableCloseAction && evt.equals(KeyCode.Escape)) {
if (!this.options.disableCloseAction && evt.equals(KeyCode.Escape) && sawEscapeKeyDown) {
close();
}
}, true));

View File

@@ -1202,6 +1202,8 @@ export class AICustomizationManagementEditor extends EditorPane {
this.inEditorContextKey.set(true);
this.sectionContextKey.set(this.selectedSection);
input.setSaveHandler(() => this.handleBuiltinSave());
this.telemetryService.publicLog2<CustomizationEditorOpenedEvent, CustomizationEditorOpenedClassification>('chatCustomizationEditor.opened', {
section: this.selectedSection,
});
@@ -1214,6 +1216,12 @@ export class AICustomizationManagementEditor extends EditorPane {
}
override clearInput(): void {
const input = this.input;
if (input instanceof AICustomizationManagementEditorInput) {
input.setSaveHandler(undefined);
input.setDirty(false);
}
this.inEditorContextKey.set(false);
if (this.viewMode === 'editor') {
this.goBackToList();
@@ -1661,6 +1669,8 @@ export class AICustomizationManagementEditor extends EditorPane {
}
private updateEditorActionButton(): void {
this.updateInputDirtyState();
if (!this.editorActionButton || !this.editorActionButtonIcon) {
return;
}
@@ -1682,6 +1692,49 @@ export class AICustomizationManagementEditor extends EditorPane {
&& (this.currentEditingPromptType === PromptsType.prompt || this.currentEditingPromptType === PromptsType.skill);
}
private updateInputDirtyState(): void {
const input = this.input;
if (input instanceof AICustomizationManagementEditorInput) {
input.setDirty(this.shouldShowBuiltinSaveAction());
}
}
private async handleBuiltinSave(): Promise<boolean> {
if (!this.shouldShowBuiltinSaveAction()) {
return false;
}
const target = await this.pickBuiltinPromptSaveTarget();
if (!target || target.target === 'cancel') {
return false;
}
const saveRequest = this.createBuiltinPromptSaveRequest(target);
if (!saveRequest) {
return false;
}
try {
await this.saveBuiltinPromptCopy(saveRequest);
this.telemetryService.publicLog2<CustomizationEditorSaveItemEvent, CustomizationEditorSaveItemClassification>('chatCustomizationEditor.saveItem', {
promptType: this.currentEditingPromptType ?? '',
storage: String(this.currentEditingStorage ?? ''),
saveTarget: target.target,
});
this._editorContentChanged = false;
this.updateEditorActionButton();
return true;
} catch (error) {
console.error('Failed to save built-in override:', error);
this.notificationService.warn(target.target === 'workspace'
? localize('saveBuiltinCopyFailedWorkspace', "Could not save the override to the workspace.")
: localize('saveBuiltinCopyFailedUser', "Could not save the override to your user folder."));
return false;
}
}
private resetEditorSaveIndicator(): void {
this.editorSaveIndicator.className = 'editor-save-indicator';
this.editorSaveIndicator.title = '';

View File

@@ -6,7 +6,7 @@
import { Codicon } from '../../../../../base/common/codicons.js';
import { ThemeIcon } from '../../../../../base/common/themables.js';
import { localize } from '../../../../../nls.js';
import { IUntypedEditorInput, EditorInputCapabilities } from '../../../../common/editor.js';
import { IUntypedEditorInput, EditorInputCapabilities, GroupIdentifier, ISaveOptions, SaveReason } from '../../../../common/editor.js';
import { EditorInput } from '../../../../common/editor/editorInput.js';
import { AI_CUSTOMIZATION_MANAGEMENT_EDITOR_INPUT_ID } from './aiCustomizationManagement.js';
@@ -20,6 +20,9 @@ export class AICustomizationManagementEditorInput extends EditorInput {
readonly resource = undefined;
private _isDirty = false;
private _saveHandler?: () => Promise<boolean>;
override get capabilities(): EditorInputCapabilities {
return super.capabilities | EditorInputCapabilities.Singleton | EditorInputCapabilities.RequiresModal;
}
@@ -59,4 +62,34 @@ export class AICustomizationManagementEditorInput extends EditorInput {
override async resolve(): Promise<null> {
return null;
}
override isDirty(): boolean {
return this._isDirty;
}
override async save(group: GroupIdentifier, options?: ISaveOptions): Promise<EditorInput | undefined> {
if (options?.reason !== undefined && options.reason !== SaveReason.EXPLICIT) {
return undefined;
}
if (this._saveHandler) {
const saved = await this._saveHandler();
return saved ? this : undefined;
}
return undefined;
}
override async revert(): Promise<void> {
this.setDirty(false);
}
setDirty(dirty: boolean): void {
if (this._isDirty !== dirty) {
this._isDirty = dirty;
this._onDidChangeDirty.fire();
}
}
setSaveHandler(handler: (() => Promise<boolean>) | undefined): void {
this._saveHandler = handler;
}
}