Stop transient file conflict callouts (#2694)

* fix(app): prevent transient file conflict callouts

Watcher versions are candidates until a matching disk read settles them. The editor now classifies the settled bytes once, preserving dirty buffers and keeping reload content paired with its revision.

* fix(app): preserve external BOM changes
This commit is contained in:
Mohamed Boudra
2026-07-31 19:03:11 +08:00
committed by GitHub
parent 91da0bac4d
commit 8e983c5d27
13 changed files with 753 additions and 187 deletions
@@ -76,6 +76,8 @@ export async function installDaemonWebSocketGate(page: Page) {
const clientRequestCounts = new Map<string, number>();
const subscribedFilePaths = new Set<string>();
const fileSubscriptionWaiters = new Map<string, () => void>();
const observedFileUpdates = new Set<string>();
const fileUpdateWaiters = new Map<string, () => void>();
let fileReadPathToHold: string | null = null;
let heldFileReads: Array<() => void> = [];
let resolveHeldFileRead: (() => void) | null = null;
@@ -84,6 +86,19 @@ export async function installDaemonWebSocketGate(page: Page) {
let heldReadyFileUpdate: (() => void) | null = null;
let resolveHeldReadyFileUpdate: (() => void) | null = null;
let heldReadyFileUpdatePromise = Promise.resolve();
const recordFileUpdate = (message: ServerMessage): void => {
if (
message.type !== "fs.file.update" ||
typeof message.payload?.version?.status !== "string" ||
typeof message.payload.version.path !== "string"
) {
return;
}
const key = `${message.payload.version.path}:${message.payload.version.status}`;
observedFileUpdates.add(key);
fileUpdateWaiters.get(key)?.();
fileUpdateWaiters.delete(key);
};
await page.routeWebSocket(daemonWsRoutePattern(), (ws) => {
if (!acceptingConnections) {
@@ -134,6 +149,7 @@ export async function installDaemonWebSocketGate(page: Page) {
fileSubscriptionWaiters.get(path)?.();
fileSubscriptionWaiters.delete(path);
}
if (serverMessage) recordFileUpdate(serverMessage);
if (
serverMessage?.type === "fs.file.update" &&
serverMessage.payload?.version?.status === "ready" &&
@@ -157,6 +173,11 @@ export async function installDaemonWebSocketGate(page: Page) {
if (subscribedFilePaths.has(path)) return Promise.resolve();
return new Promise((resolve) => fileSubscriptionWaiters.set(path, resolve));
},
waitForFileUpdate(path: string, status: "ready" | "missing" | "error"): Promise<void> {
const key = `${path}:${status}`;
if (observedFileUpdates.has(key)) return Promise.resolve();
return new Promise((resolve) => fileUpdateWaiters.set(key, resolve));
},
holdFileReads(path: string): void {
fileReadPathToHold = path;
heldFileReads = [];
+48
View File
@@ -0,0 +1,48 @@
import type { Page } from "@playwright/test";
import { expect } from "../fixtures";
export async function recordFileCallouts(page: Page): Promise<void> {
await page.evaluate(() => {
const callouts: string[] = [];
const recordAlert = (alert: Element) => {
const text = alert.textContent?.trim();
if (text && !callouts.includes(text)) callouts.push(text);
};
const record = (records: MutationRecord[]) => {
for (const mutation of records) {
if (
!(mutation.target instanceof Element) ||
!mutation.target.closest('[data-testid="workspace-file-pane"]')
) {
continue;
}
for (const node of mutation.addedNodes) {
if (!(node instanceof Element)) continue;
if (node.matches('[role="alert"]')) recordAlert(node);
for (const alert of node.querySelectorAll('[role="alert"]')) recordAlert(alert);
}
}
};
const observer = new MutationObserver(record);
observer.observe(document.body, { childList: true, subtree: true, characterData: true });
Object.assign(globalThis, { __PASEO_RECORDED_FILE_CALLOUTS__: callouts });
});
}
export async function expectFileCalloutWasRendered(page: Page, title: string): Promise<void> {
await expect
.poll(() => recordedFileCallouts(page))
.toEqual(expect.arrayContaining([expect.stringContaining(title)]));
}
export async function expectNoFileCalloutWasRendered(page: Page): Promise<void> {
await expect.poll(() => recordedFileCallouts(page)).toEqual([]);
}
function recordedFileCallouts(page: Page): Promise<string[]> {
return page.evaluate(
() =>
(globalThis as typeof globalThis & { __PASEO_RECORDED_FILE_CALLOUTS__?: string[] })
.__PASEO_RECORDED_FILE_CALLOUTS__ ?? [],
);
}