mirror of
https://github.com/stablyai/orca.git
synced 2026-10-18 16:03:17 +00:00
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:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user