chore(editor): trim move-fix comments to the why; drop redundant rename quiesce

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Neil
2026-07-20 23:25:27 -07:00
co-authored by Orca
parent 13f83e894d
commit 2a80df44ea
3 changed files with 18 additions and 47 deletions
@@ -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
@@ -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<string, { worktreeId: string; owner: string | null }>()
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)
+3 -18
View File
@@ -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<void> {
const { oldPath, newName, worktreeId, worktreePath, refreshDir } = args
@@ -55,19 +51,8 @@ export async function renameFileOnDisk(args: RenameFileArgs): Promise<void> {
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