diff --git a/src/renderer/src/components/editor/editor-path-move-inflight.ts b/src/renderer/src/components/editor/editor-path-move-inflight.ts index 0cef962738c..cdba0e02b49 100644 --- a/src/renderer/src/components/editor/editor-path-move-inflight.ts +++ b/src/renderer/src/components/editor/editor-path-move-inflight.ts @@ -1,16 +1,12 @@ import { normalizeAbsolutePathForComparison } from '@/components/right-sidebar/file-explorer-paths' -// Tracks Orca-owned moves that are in flight (rename issued, not yet settled). -// Replaces the old time-bounded self-move registry: an operation is live only -// for the exact duration of its filesystem rename + atomic rekey, so there is -// no TTL to race a slow SSH rename and no cross-operation aliasing. +// Tracks Orca-owned moves in flight (rename issued, not yet settled). Live only +// for the rename + rekey duration — no TTL to race a slow SSH rename. // -// Only the source side needs this: while an operation is live, a watcher delete -// of anything under one of its source ROOTS is the move's own echo, not an -// external delete — don't tombstone. Roots are prefix-matched (not exact) so a -// file opened UNDER a moving directory mid-rename is covered too. The -// destination is handled after commit: the coordinator proactively -// content-verifies every gated moved tab. +// Source side only: while live, a watcher delete under any source ROOT is the +// move's own echo (prefix-matched so a file opened under a moving directory +// mid-rename is covered too), not an external delete — don't tombstone. The +// destination is content-verified by the coordinator after commit. type MoveOperation = { worktreeId: string diff --git a/src/renderer/src/lib/execute-open-editor-path-move.ts b/src/renderer/src/lib/execute-open-editor-path-move.ts index cb10d30d58d..4f8620ac6ca 100644 --- a/src/renderer/src/lib/execute-open-editor-path-move.ts +++ b/src/renderer/src/lib/execute-open-editor-path-move.ts @@ -37,11 +37,8 @@ export async function executeOpenEditorPathMove(args: { const { context, fromPath, toPath, worktreeId, worktreePath } = args const operationId = `editor-move-${(moveOperationCounter += 1)}` - // Every open session under the source path on the SAME execution host is - // affected — including one in a DIFFERENT worktree (e.g. a floating workspace) - // at the same absolute path — so quiesce/suppress/notify them all. A tab at the - // same path on another host (2nd SSH connection, local vs runtime) is a distinct - // file the rename never touched; scoping by host keeps it out of the transaction. + // Host-scoped: the same abs path on another host (2nd SSH connection, local vs + // runtime) is a distinct file the rename never touched — keep it out of the move. const moveState = useAppStore.getState() const initiatingHostId = getExecutionHostIdForWorktree(moveState, worktreeId) const affected = moveState.openFiles.filter( @@ -50,11 +47,9 @@ export async function executeOpenEditorPathMove(args: { getExecutionHostIdForWorktree(moveState, f.worktreeId) === initiatingHostId ) - // In-flight source suppression is scoped per (worktree, runtime owner) and - // registers the move ROOT (fromPath, prefix-matched) — so a file opened UNDER a - // moving directory mid-rename still has its own delete/rename echo suppressed, - // even though it wasn't in `affected`. Always include the initiating scope even - // when no tab is open there yet (an empty-affected directory move). + // Register the move ROOT (prefix-matched) per scope, always including the + // initiating scope, so a file opened UNDER a moving directory mid-rename still has + // its own delete/rename echo suppressed even though it wasn't in `affected`. const ownerSubOps: string[] = [] const scopes = new Map() const addScope = (wtId: string, owner: string | null): void => { @@ -128,9 +123,8 @@ export async function executeOpenEditorPathMove(args: { ) } - // Rekey committed: close the host's old-path tab for any mirrored file. The - // rekey detached it to a local tab, so without this the host snapshot would - // resurrect the old path; the close intent suppresses re-mirroring. + // Rekey detached mirrored tabs to local; close the host's old-path tab so its + // snapshot can't resurrect the old path (the close intent suppresses re-mirroring). for (const file of mirroredAffected) { notifyHostOfMirroredEditorClose(mirrorState, file.worktreeId, file.id) } @@ -140,12 +134,8 @@ export async function executeOpenEditorPathMove(args: { } } - // Drive verification for EVERY tab the rekey gated — not just paths whose - // watcher event was latched. The rename has completed, so disk is authoritative - // now; verifying proactively guarantees the autosave gate resolves even if the - // destination watcher event never arrives (watcher down / dropped / coalesced). - // A move echo reads disk == baseline (suppress); a genuine write differs - // (banner). A later watcher event finds the provenance consumed and no-ops. + // Proactively verify every gated tab so the autosave gate resolves even if the + // destination watcher event never arrives (down / dropped / coalesced). const gatedTabIds = useAppStore .getState() .openFiles.filter((f) => f.pendingSelfMoveEcho?.operationId === operationId) diff --git a/src/renderer/src/lib/rename-file.ts b/src/renderer/src/lib/rename-file.ts index 544f52d9a49..e6ce2a5981a 100644 --- a/src/renderer/src/lib/rename-file.ts +++ b/src/renderer/src/lib/rename-file.ts @@ -2,7 +2,6 @@ import { toast } from 'sonner' import { useAppStore } from '@/store' import { basename, dirname, joinPath } from '@/lib/path' import { getConnectionId } from '@/lib/connection-context' -import { requestEditorSaveQuiesce } from '@/components/editor/editor-autosave' import { commitFileExplorerOp } from '@/components/right-sidebar/fileExplorerUndoRedo' import { executeOpenEditorPathMove } from '@/lib/execute-open-editor-path-move' @@ -32,15 +31,12 @@ type RenameFileArgs = { /** * Rename a file or directory on disk. Handles: * - no-op when the name is unchanged - * - quiescing any in-flight autosave on open tabs under `oldPath` - * (so a trailing write can't recreate the old path post-rename) - * - remapping every affected open editor tab to the new path + * - the open-editor move transaction (quiesce + rename + rekey) via the coordinator * - committing an undo/redo pair via the file-explorer undo stack * - unwrapped toast on IPC failure * * Used by the file-explorer inline rename and by double-click-rename - * from an editor tab. Both entry points should go through here so - * the tab-remap + quiesce behavior stays consistent. + * from an editor tab. Both go through here so the move behavior stays consistent. */ export async function renameFileOnDisk(args: RenameFileArgs): Promise { const { oldPath, newName, worktreeId, worktreePath, refreshDir } = args @@ -55,19 +51,8 @@ export async function renameFileOnDisk(args: RenameFileArgs): Promise { const parentDir = dirname(oldPath) const newPath = joinPath(parentDir, trimmed) const connectionId = getConnectionId(worktreeId) ?? undefined - - // Let any in-flight autosave under `oldPath` finish first — a trailing - // write to the old path after rename would silently recreate it. - const state = useAppStore.getState() - const filesToQuiesce = state.openFiles.filter( - (file) => - file.filePath === oldPath || - file.filePath.startsWith(`${oldPath}/`) || - file.filePath.startsWith(`${oldPath}\\`) - ) - await Promise.all(filesToQuiesce.map((file) => requestEditorSaveQuiesce({ fileId: file.id }))) const fileContext = { - settings: state.settings, + settings: useAppStore.getState().settings, worktreeId, worktreePath, connectionId