mirror of
https://github.com/vadimmelnicuk/meo.git
synced 2026-05-03 12:40:38 +00:00
fix: improve auto save stability
This commit is contained in:
@@ -328,25 +328,56 @@ export function createPanelSessionController(params: PanelSessionControllerParam
|
||||
return applyQueue;
|
||||
};
|
||||
|
||||
const applyPendingDraftIfNeeded = async (): Promise<void> => {
|
||||
const reportBackgroundError = (contextLabel: string, error: unknown): void => {
|
||||
if (disposed) {
|
||||
return;
|
||||
}
|
||||
console.error(`[MEO panelSession] ${contextLabel}`, error);
|
||||
};
|
||||
|
||||
const runBackground = (promise: Promise<unknown>, contextLabel: string): void => {
|
||||
void promise.catch((error) => {
|
||||
reportBackgroundError(contextLabel, error);
|
||||
});
|
||||
};
|
||||
|
||||
const postToWebview = async (message: Record<string, unknown>): Promise<boolean> => {
|
||||
if (disposed) {
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
return await panel.webview.postMessage(message);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const saveDocumentIfAutoSaveEnabled = async (): Promise<void> => {
|
||||
if (!getAutoSaveEnabled(context) || !document.isDirty) {
|
||||
return;
|
||||
}
|
||||
await document.save();
|
||||
};
|
||||
|
||||
const applyPendingDraftIfNeeded = async (): Promise<boolean> => {
|
||||
const draftText = pendingDraftText;
|
||||
pendingDraftText = null;
|
||||
if (draftText === null) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
const currentText = document.getText();
|
||||
const normalizedCurrent = currentText.replace(/\r\n/g, '\n');
|
||||
const normalizedDraft = draftText.replace(/\r\n/g, '\n');
|
||||
if (normalizedCurrent === normalizedDraft) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
const edit = new vscode.WorkspaceEdit();
|
||||
const fullRange = new vscode.Range(document.positionAt(0), document.positionAt(currentText.length));
|
||||
agentReviewHandoff.noteRecentMEOOwnedFileChangeForUri(document.uri);
|
||||
edit.replace(document.uri, fullRange, draftText);
|
||||
await vscode.workspace.applyEdit(edit);
|
||||
return vscode.workspace.applyEdit(edit);
|
||||
};
|
||||
|
||||
const sendInit = async (): Promise<boolean> => {
|
||||
@@ -365,7 +396,7 @@ export function createPanelSessionController(params: PanelSessionControllerParam
|
||||
outlineVisible: getOutlineVisible(context),
|
||||
theme: getThemeSettings()
|
||||
};
|
||||
return panel.webview.postMessage(message);
|
||||
return postToWebview(message);
|
||||
};
|
||||
|
||||
const sendDocChanged = async (): Promise<boolean> => {
|
||||
@@ -374,7 +405,7 @@ export function createPanelSessionController(params: PanelSessionControllerParam
|
||||
text: document.getText(),
|
||||
version: document.version
|
||||
};
|
||||
return panel.webview.postMessage(message);
|
||||
return postToWebview(message);
|
||||
};
|
||||
|
||||
const sendApplied = async (version: number): Promise<boolean> => {
|
||||
@@ -382,7 +413,7 @@ export function createPanelSessionController(params: PanelSessionControllerParam
|
||||
type: 'applied',
|
||||
version
|
||||
};
|
||||
return panel.webview.postMessage(message);
|
||||
return postToWebview(message);
|
||||
};
|
||||
|
||||
const sendGitBaselineChanged = async (options: RefreshGitBaselineOptions = {}): Promise<boolean> => {
|
||||
@@ -405,7 +436,7 @@ export function createPanelSessionController(params: PanelSessionControllerParam
|
||||
version: document.version,
|
||||
payload
|
||||
};
|
||||
const posted = await panel.webview.postMessage(message);
|
||||
const posted = await postToWebview(message);
|
||||
if (posted) {
|
||||
gitDocumentState.setLastSentBaselineHash(payloadHash);
|
||||
}
|
||||
@@ -437,7 +468,7 @@ export function createPanelSessionController(params: PanelSessionControllerParam
|
||||
};
|
||||
|
||||
const ensureInitDelivered = async (): Promise<void> => {
|
||||
if (initDelivered) {
|
||||
if (disposed || initDelivered) {
|
||||
return;
|
||||
}
|
||||
const posted = await sendInit();
|
||||
@@ -448,6 +479,9 @@ export function createPanelSessionController(params: PanelSessionControllerParam
|
||||
|
||||
const requestExportSnapshot = async (): Promise<{ text: string; environment?: ExportStyleEnvironment }> => {
|
||||
await ensureInitDelivered();
|
||||
if (disposed) {
|
||||
throw new Error('The editor was closed before export completed.');
|
||||
}
|
||||
const requestId = `export-${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
||||
|
||||
const response = new Promise<{ text: string; environment?: ExportStyleEnvironment }>((resolve, reject) => {
|
||||
@@ -463,7 +497,7 @@ export function createPanelSessionController(params: PanelSessionControllerParam
|
||||
type: 'requestExportSnapshot',
|
||||
requestId
|
||||
};
|
||||
const posted = await panel.webview.postMessage(message);
|
||||
const posted = await postToWebview(message);
|
||||
if (!posted) {
|
||||
rejectPendingExportSnapshot(requestId, new Error('The editor webview is not ready to export.'));
|
||||
}
|
||||
@@ -524,7 +558,7 @@ export function createPanelSessionController(params: PanelSessionControllerParam
|
||||
} finally {
|
||||
gitRefreshRunning = false;
|
||||
if (gitRefreshPending) {
|
||||
void runPendingGitRefreshes();
|
||||
runBackground(runPendingGitRefreshes(), 'runPendingGitRefreshes.retry');
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -536,7 +570,7 @@ export function createPanelSessionController(params: PanelSessionControllerParam
|
||||
gitRefreshPending = true;
|
||||
gitRefreshPendingForcePost = gitRefreshPendingForcePost || options.forcePost === true;
|
||||
gitRefreshPendingForceReload = gitRefreshPendingForceReload || options.forceReload === true;
|
||||
void runPendingGitRefreshes();
|
||||
runBackground(runPendingGitRefreshes(), 'runPendingGitRefreshes');
|
||||
};
|
||||
|
||||
const isTextEditorForDocument = (textEditor: vscode.TextEditor | undefined): textEditor is vscode.TextEditor => {
|
||||
@@ -563,7 +597,7 @@ export function createPanelSessionController(params: PanelSessionControllerParam
|
||||
anchor: selection.anchor,
|
||||
head: selection.head
|
||||
};
|
||||
const posted = await panel.webview.postMessage(message);
|
||||
const posted = await postToWebview(message);
|
||||
if (posted) {
|
||||
lastSentRevealSelectionKey = getRevealSelectionKey(selection);
|
||||
pendingRevealSelection = null;
|
||||
@@ -582,7 +616,7 @@ export function createPanelSessionController(params: PanelSessionControllerParam
|
||||
const postFocusEditor = async (): Promise<void> => {
|
||||
await ensureInitDelivered();
|
||||
const message: FocusEditorMessage = { type: 'focusEditor' };
|
||||
await panel.webview.postMessage(message);
|
||||
await postToWebview(message);
|
||||
};
|
||||
|
||||
const sendRevealSelectionForEditor = async (textEditor: vscode.TextEditor | undefined): Promise<void> => {
|
||||
@@ -627,6 +661,9 @@ export function createPanelSessionController(params: PanelSessionControllerParam
|
||||
};
|
||||
|
||||
const handleMessage = async (raw: WebviewMessage): Promise<void> => {
|
||||
if (disposed) {
|
||||
return;
|
||||
}
|
||||
switch (raw.type) {
|
||||
case 'ready':
|
||||
await ensureInitDelivered();
|
||||
@@ -685,7 +722,7 @@ export function createPanelSessionController(params: PanelSessionControllerParam
|
||||
requestId: raw.requestId,
|
||||
resolvedUrl: resolveWebviewImageSrc(raw.url, documentUri, panel.webview)
|
||||
};
|
||||
await panel.webview.postMessage(response);
|
||||
await postToWebview(response);
|
||||
return;
|
||||
}
|
||||
case 'resolveWikiLinks': {
|
||||
@@ -694,7 +731,7 @@ export function createPanelSessionController(params: PanelSessionControllerParam
|
||||
requestId: raw.requestId,
|
||||
results: await resolveWikiLinkTargets(raw.targets, documentUri)
|
||||
};
|
||||
await panel.webview.postMessage(response);
|
||||
await postToWebview(response);
|
||||
return;
|
||||
}
|
||||
case 'exportSnapshot':
|
||||
@@ -715,7 +752,7 @@ export function createPanelSessionController(params: PanelSessionControllerParam
|
||||
localEditGeneration: raw.localEditGeneration,
|
||||
result: resolved.result
|
||||
};
|
||||
await panel.webview.postMessage(response);
|
||||
await postToWebview(response);
|
||||
return;
|
||||
}
|
||||
case 'openGitRevisionForLine':
|
||||
@@ -728,7 +765,10 @@ export function createPanelSessionController(params: PanelSessionControllerParam
|
||||
agentReviewHandoff.noteRecentMEOOwnedFileChangeForUri(document.uri);
|
||||
isApplyingOwnChange = true;
|
||||
try {
|
||||
await enqueue(() => applyDocumentChanges(document, raw, sendDocChanged, sendApplied));
|
||||
await enqueue(async () => {
|
||||
await applyDocumentChanges(document, raw, sendDocChanged, sendApplied);
|
||||
await saveDocumentIfAutoSaveEnabled();
|
||||
});
|
||||
} finally {
|
||||
isApplyingOwnChange = false;
|
||||
}
|
||||
@@ -748,14 +788,14 @@ export function createPanelSessionController(params: PanelSessionControllerParam
|
||||
return;
|
||||
case 'saveImageFromClipboard': {
|
||||
const response = await handleSaveImageFromClipboard(raw, documentUri);
|
||||
await panel.webview.postMessage(response);
|
||||
await postToWebview(response);
|
||||
return;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const messageSubscription = panel.webview.onDidReceiveMessage((raw: WebviewMessage) => {
|
||||
void handleMessage(raw);
|
||||
runBackground(handleMessage(raw), 'handleMessage');
|
||||
});
|
||||
|
||||
const documentChangeSubscription = vscode.workspace.onDidChangeTextDocument((event) => {
|
||||
@@ -772,9 +812,9 @@ export function createPanelSessionController(params: PanelSessionControllerParam
|
||||
return;
|
||||
}
|
||||
|
||||
void enqueue(async () => {
|
||||
runBackground(enqueue(async () => {
|
||||
await sendDocChanged();
|
||||
});
|
||||
}), 'sendDocChanged');
|
||||
});
|
||||
|
||||
const documentSaveSubscription = vscode.workspace.onDidSaveTextDocument((savedDocument) => {
|
||||
@@ -788,27 +828,27 @@ export function createPanelSessionController(params: PanelSessionControllerParam
|
||||
if (!isTextEditorForDocument(event.textEditor)) {
|
||||
return;
|
||||
}
|
||||
void sendRevealSelectionForEditor(event.textEditor);
|
||||
runBackground(sendRevealSelectionForEditor(event.textEditor), 'sendRevealSelectionForEditor.selection');
|
||||
});
|
||||
|
||||
const activeTextEditorSubscription = vscode.window.onDidChangeActiveTextEditor((textEditor) => {
|
||||
if (!isTextEditorForDocument(textEditor)) {
|
||||
return;
|
||||
}
|
||||
void sendRevealSelectionForEditor(textEditor);
|
||||
runBackground(sendRevealSelectionForEditor(textEditor), 'sendRevealSelectionForEditor.activeEditor');
|
||||
});
|
||||
|
||||
const visibleTextEditorsSubscription = vscode.window.onDidChangeVisibleTextEditors(() => {
|
||||
void sendRevealSelectionForEditor(findEditorForDocumentReveal());
|
||||
runBackground(sendRevealSelectionForEditor(findEditorForDocumentReveal()), 'sendRevealSelectionForEditor.visibleEditors');
|
||||
});
|
||||
|
||||
const viewStateSubscription = panel.onDidChangeViewState((event) => {
|
||||
if (event.webviewPanel.active) {
|
||||
onPanelActivated(event.webviewPanel);
|
||||
refreshGitBaseline({ forcePost: true });
|
||||
void flushPendingRevealSelection();
|
||||
void sendRevealSelectionForEditor(findEditorForDocumentReveal());
|
||||
void postFocusEditor();
|
||||
runBackground(flushPendingRevealSelection(), 'flushPendingRevealSelection');
|
||||
runBackground(sendRevealSelectionForEditor(findEditorForDocumentReveal()), 'sendRevealSelectionForEditor.viewState');
|
||||
runBackground(postFocusEditor(), 'postFocusEditor');
|
||||
}
|
||||
onPanelViewStateChanged();
|
||||
});
|
||||
@@ -817,9 +857,9 @@ export function createPanelSessionController(params: PanelSessionControllerParam
|
||||
dispose();
|
||||
});
|
||||
|
||||
void ensureInitDelivered();
|
||||
runBackground(ensureInitDelivered(), 'ensureInitDelivered.startup');
|
||||
refreshGitBaseline({ forcePost: true });
|
||||
void sendRevealSelectionForEditor(findEditorForDocumentReveal());
|
||||
runBackground(sendRevealSelectionForEditor(findEditorForDocumentReveal()), 'sendRevealSelectionForEditor.startup');
|
||||
|
||||
const dispose = (): void => {
|
||||
if (disposed) {
|
||||
@@ -827,14 +867,17 @@ export function createPanelSessionController(params: PanelSessionControllerParam
|
||||
}
|
||||
disposed = true;
|
||||
|
||||
void enqueue(async () => {
|
||||
runBackground(enqueue(async () => {
|
||||
try {
|
||||
// Best-effort recovery for edits that never made it through the debounce/apply round-trip.
|
||||
await applyPendingDraftIfNeeded();
|
||||
const recovered = await applyPendingDraftIfNeeded();
|
||||
if (recovered) {
|
||||
await saveDocumentIfAutoSaveEnabled();
|
||||
}
|
||||
} catch {
|
||||
// Ignore dispose-time recovery failures to avoid surfacing noisy teardown errors.
|
||||
}
|
||||
});
|
||||
}), 'disposeDraftRecovery');
|
||||
|
||||
rejectPendingExportSnapshots(new Error('The editor was closed before export completed.'));
|
||||
messageSubscription.dispose();
|
||||
|
||||
+29
-25
@@ -417,7 +417,13 @@ const findPanelController = createFindPanelController(findPanelElements, () => e
|
||||
const selectionMenuElements = createSelectionMenu();
|
||||
const selectionMenuController = createSelectionMenuController(selectionMenuElements, () => editor);
|
||||
|
||||
toolbar.replaceChildren(formatGroup, rightGroup, modeGroup, findPanelElements.panel);
|
||||
const editorNoticeBanner = document.createElement('div');
|
||||
editorNoticeBanner.className = 'editor-notice';
|
||||
editorNoticeBanner.setAttribute('role', 'status');
|
||||
editorNoticeBanner.setAttribute('aria-live', 'polite');
|
||||
editorNoticeBanner.hidden = true;
|
||||
|
||||
toolbar.replaceChildren(formatGroup, rightGroup, modeGroup, findPanelElements.panel, editorNoticeBanner);
|
||||
|
||||
const existingEditorWrapper = root.querySelector('.editor-wrapper');
|
||||
const editorWrapper = existingEditorWrapper instanceof HTMLElement ? existingEditorWrapper : document.createElement('div');
|
||||
@@ -461,9 +467,28 @@ let hasSentDraftText = false;
|
||||
let initialEditorMountInFlight = false;
|
||||
let createEditorFactoryPromise: Promise<CreateEditorFactory> | null = null;
|
||||
|
||||
const setEditorNotice = (message: string, kind = 'info') => {
|
||||
const normalizedMessage = `${message ?? ''}`.trim();
|
||||
if (!normalizedMessage) {
|
||||
clearEditorNotice();
|
||||
return;
|
||||
}
|
||||
editorNoticeBanner.textContent = normalizedMessage;
|
||||
editorNoticeBanner.dataset.kind = kind;
|
||||
editorNoticeBanner.hidden = false;
|
||||
editorNoticeBanner.classList.add('is-visible');
|
||||
};
|
||||
|
||||
const clearEditorNotice = () => {
|
||||
editorNoticeBanner.textContent = '';
|
||||
delete editorNoticeBanner.dataset.kind;
|
||||
editorNoticeBanner.hidden = true;
|
||||
editorNoticeBanner.classList.remove('is-visible');
|
||||
};
|
||||
|
||||
const editorNotice: EditorNotice = {
|
||||
setEditorNotice: (_message, _kind = 'info') => {},
|
||||
clearEditorNotice: () => {}
|
||||
setEditorNotice,
|
||||
clearEditorNotice
|
||||
};
|
||||
|
||||
const failureNotice = createFailureNoticeManager(editorNotice);
|
||||
@@ -1127,9 +1152,6 @@ window.addEventListener('message', (event) => {
|
||||
flushChanges();
|
||||
maybeSaveAfterSync();
|
||||
syncPendingDraftState();
|
||||
if (autoSaveEnabled && !inFlight && pendingText !== null && normalizeEol(pendingText) === syncedText) {
|
||||
vscode.postMessage({ type: 'saveDocument' });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1245,25 +1267,7 @@ window.addEventListener('visibilitychange', () => {
|
||||
});
|
||||
|
||||
const forceFlushChanges = () => {
|
||||
if (!editor || pendingText === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const nextText = pendingText;
|
||||
const message: WebviewMessage = {
|
||||
type: 'applyChanges',
|
||||
baseVersion: documentVersion,
|
||||
changes: [
|
||||
{
|
||||
from: 0,
|
||||
to: syncedText.length,
|
||||
insert: nextText
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
documentVersion++;
|
||||
vscode.postMessage(message);
|
||||
flushChanges();
|
||||
};
|
||||
|
||||
window.addEventListener('beforeunload', () => {
|
||||
|
||||
@@ -107,6 +107,42 @@ body {
|
||||
background-color: var(--vscode-sideBar-background);
|
||||
}
|
||||
|
||||
.editor-notice {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: calc(100% + 4px);
|
||||
left: 14px;
|
||||
right: 14px;
|
||||
padding: 6px 10px;
|
||||
border: 1px solid var(--vscode-inputValidation-infoBorder, var(--vscode-panel-border));
|
||||
border-radius: 6px;
|
||||
color: var(--vscode-inputValidation-infoForeground, var(--vscode-editor-foreground));
|
||||
background: var(
|
||||
--vscode-inputValidation-infoBackground,
|
||||
var(--vscode-editorWidget-background, var(--vscode-sideBar-background))
|
||||
);
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
z-index: 275;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.editor-notice.is-visible {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.editor-notice[data-kind='warning'] {
|
||||
border-color: var(--vscode-inputValidation-warningBorder, #b89500);
|
||||
color: var(--vscode-inputValidation-warningForeground, var(--vscode-editor-foreground));
|
||||
background: var(--vscode-inputValidation-warningBackground, rgba(184, 149, 0, 0.18));
|
||||
}
|
||||
|
||||
.editor-notice[data-kind='error'] {
|
||||
border-color: var(--vscode-inputValidation-errorBorder, #be1100);
|
||||
color: var(--vscode-inputValidation-errorForeground, #ffffff);
|
||||
background: var(--vscode-inputValidation-errorBackground, rgba(190, 17, 0, 0.3));
|
||||
}
|
||||
|
||||
.find-panel {
|
||||
display: none;
|
||||
position: absolute;
|
||||
|
||||
Reference in New Issue
Block a user