Clarify changed and deleted files in the file viewer (#2670)
* fix(app): distinguish changed and deleted files Successful reads now reconcile transient missing events without letting stale cached data mask a deletion. File notices expose only actions valid for the current buffer state. * fix(app): preserve file state ordering * fix(app): allow retrying file checks * fix(app): clear unchanged file conflicts * fix(app): keep file conflict recovery consistent Own disk observation and read ordering in one model so stale reads and failed subscriptions cannot produce contradictory actions. * fix(app): allow retrying file preview errors Preserve daemon read errors and expose retry for initial and non-editable file loads.
This commit is contained in:
@@ -16,6 +16,21 @@ interface ClientRequest {
|
||||
type?: unknown;
|
||||
subscribe?: unknown;
|
||||
page?: { cursor?: unknown };
|
||||
mode?: unknown;
|
||||
path?: unknown;
|
||||
}
|
||||
|
||||
interface ServerMessage {
|
||||
type?: unknown;
|
||||
payload?: {
|
||||
initial?: {
|
||||
path?: unknown;
|
||||
};
|
||||
version?: {
|
||||
status?: unknown;
|
||||
path?: unknown;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
function readClientRequest(message: string | Buffer): ClientRequest | null {
|
||||
@@ -31,6 +46,18 @@ function readClientRequest(message: string | Buffer): ClientRequest | null {
|
||||
}
|
||||
}
|
||||
|
||||
function readServerMessage(message: string | Buffer): ServerMessage | null {
|
||||
try {
|
||||
const envelope = JSON.parse(typeof message === "string" ? message : message.toString()) as {
|
||||
type?: unknown;
|
||||
message?: ServerMessage;
|
||||
};
|
||||
return envelope.type === "session" ? (envelope.message ?? null) : envelope;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function directoryForRequest(request: ClientRequest): keyof DirectoryBootstrapCounts | null {
|
||||
if (request.page?.cursor) return null;
|
||||
if (request.type === "fetch_agents_request") return "agents";
|
||||
@@ -47,6 +74,16 @@ export async function installDaemonWebSocketGate(page: Page) {
|
||||
total: { agents: 0, workspaces: 0 },
|
||||
};
|
||||
const clientRequestCounts = new Map<string, number>();
|
||||
const subscribedFilePaths = new Set<string>();
|
||||
const fileSubscriptionWaiters = new Map<string, () => void>();
|
||||
let fileReadPathToHold: string | null = null;
|
||||
let heldFileReads: Array<() => void> = [];
|
||||
let resolveHeldFileRead: (() => void) | null = null;
|
||||
let heldFileReadPromise = Promise.resolve();
|
||||
let readyFileUpdatePathToHold: string | null = null;
|
||||
let heldReadyFileUpdate: (() => void) | null = null;
|
||||
let resolveHeldReadyFileUpdate: (() => void) | null = null;
|
||||
let heldReadyFileUpdatePromise = Promise.resolve();
|
||||
|
||||
await page.routeWebSocket(daemonWsRoutePattern(), (ws) => {
|
||||
if (!acceptingConnections) {
|
||||
@@ -69,6 +106,15 @@ export async function installDaemonWebSocketGate(page: Page) {
|
||||
directoryStarts.total[directory] += 1;
|
||||
}
|
||||
}
|
||||
if (
|
||||
request?.type === "file_explorer_request" &&
|
||||
request.mode === "file" &&
|
||||
request.path === fileReadPathToHold
|
||||
) {
|
||||
heldFileReads.push(() => server.send(message));
|
||||
resolveHeldFileRead?.();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
server.send(message);
|
||||
} catch {
|
||||
@@ -78,6 +124,26 @@ export async function installDaemonWebSocketGate(page: Page) {
|
||||
|
||||
server.onMessage((message) => {
|
||||
if (!acceptingConnections) return;
|
||||
const serverMessage = readServerMessage(message);
|
||||
if (
|
||||
serverMessage?.type === "fs.file.subscribe.response" &&
|
||||
typeof serverMessage.payload?.initial?.path === "string"
|
||||
) {
|
||||
const path = serverMessage.payload.initial.path;
|
||||
subscribedFilePaths.add(path);
|
||||
fileSubscriptionWaiters.get(path)?.();
|
||||
fileSubscriptionWaiters.delete(path);
|
||||
}
|
||||
if (
|
||||
serverMessage?.type === "fs.file.update" &&
|
||||
serverMessage.payload?.version?.status === "ready" &&
|
||||
serverMessage.payload.version.path === readyFileUpdatePathToHold &&
|
||||
!heldReadyFileUpdate
|
||||
) {
|
||||
heldReadyFileUpdate = () => ws.send(message);
|
||||
resolveHeldReadyFileUpdate?.();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
ws.send(message);
|
||||
} catch {
|
||||
@@ -87,6 +153,44 @@ export async function installDaemonWebSocketGate(page: Page) {
|
||||
});
|
||||
|
||||
return {
|
||||
waitForFileSubscription(path: string): Promise<void> {
|
||||
if (subscribedFilePaths.has(path)) return Promise.resolve();
|
||||
return new Promise((resolve) => fileSubscriptionWaiters.set(path, resolve));
|
||||
},
|
||||
holdFileReads(path: string): void {
|
||||
fileReadPathToHold = path;
|
||||
heldFileReads = [];
|
||||
heldFileReadPromise = new Promise((resolve) => {
|
||||
resolveHeldFileRead = resolve;
|
||||
});
|
||||
},
|
||||
waitForHeldFileRead(): Promise<void> {
|
||||
return heldFileReadPromise;
|
||||
},
|
||||
releaseHeldFileRead(): void {
|
||||
const forwards = heldFileReads;
|
||||
heldFileReads = [];
|
||||
fileReadPathToHold = null;
|
||||
resolveHeldFileRead = null;
|
||||
for (const forward of forwards) forward();
|
||||
},
|
||||
holdNextReadyFileUpdate(path: string): void {
|
||||
readyFileUpdatePathToHold = path;
|
||||
heldReadyFileUpdate = null;
|
||||
heldReadyFileUpdatePromise = new Promise((resolve) => {
|
||||
resolveHeldReadyFileUpdate = resolve;
|
||||
});
|
||||
},
|
||||
waitForHeldReadyFileUpdate(): Promise<void> {
|
||||
return heldReadyFileUpdatePromise;
|
||||
},
|
||||
releaseHeldReadyFileUpdate(): void {
|
||||
const forward = heldReadyFileUpdate;
|
||||
heldReadyFileUpdate = null;
|
||||
readyFileUpdatePathToHold = null;
|
||||
resolveHeldReadyFileUpdate = null;
|
||||
forward?.();
|
||||
},
|
||||
async drop(): Promise<void> {
|
||||
acceptingConnections = false;
|
||||
const sockets = Array.from(activeSockets);
|
||||
|
||||
Reference in New Issue
Block a user