mirror of
https://github.com/stablyai/orca.git
synced 2026-10-18 16:03:17 +00:00
fix(editor): scope move rekey to the initiating execution host (round 6)
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -11,6 +11,7 @@ import {
|
||||
} from '@/lib/remap-open-editor-tabs-for-path-change'
|
||||
import { verifyLatchedMoveDestinations } from '@/hooks/useEditorExternalWatch'
|
||||
import { notifyHostOfMirroredEditorClose } from '@/runtime/close-mirrored-editor-tab'
|
||||
import { getExecutionHostIdForWorktree } from '@/lib/worktree-runtime-owner'
|
||||
|
||||
let moveOperationCounter = 0
|
||||
|
||||
@@ -33,12 +34,18 @@ 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 is affected — including one open in
|
||||
// a DIFFERENT worktree (e.g. a floating workspace) at the same absolute path,
|
||||
// which the in-place rekey also retargets — so quiesce/suppress/notify them all.
|
||||
const affected = useAppStore
|
||||
.getState()
|
||||
.openFiles.filter((f) => isPathInsideOrEqual(fromPath, f.filePath))
|
||||
// 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.
|
||||
const moveState = useAppStore.getState()
|
||||
const initiatingHostId = getExecutionHostIdForWorktree(moveState, worktreeId)
|
||||
const affected = moveState.openFiles.filter(
|
||||
(f) =>
|
||||
isPathInsideOrEqual(fromPath, f.filePath) &&
|
||||
getExecutionHostIdForWorktree(moveState, f.worktreeId) === initiatingHostId
|
||||
)
|
||||
|
||||
// In-flight source suppression is scoped per (worktree, runtime owner): the
|
||||
// same path can be open and watched under more than one worktree/owner.
|
||||
|
||||
@@ -164,6 +164,59 @@ describe('remapOpenEditorTabsForPathChange', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('leaves a same-path tab on a different execution host untouched', () => {
|
||||
// Two SSH connections both have /repo/a.md open with separate dirty drafts.
|
||||
// Renaming on connA's host must not retarget connB's tab (a distinct file).
|
||||
const state = useAppStore.getState()
|
||||
useAppStore.setState({
|
||||
repos: [
|
||||
{ id: 'repoA', connectionId: 'connA' },
|
||||
{ id: 'repoB', connectionId: 'connB' }
|
||||
] as never,
|
||||
worktreesByRepo: {
|
||||
repoA: [{ id: 'wt-a', repoId: 'repoA' }],
|
||||
repoB: [{ id: 'wt-b', repoId: 'repoB' }]
|
||||
} as never
|
||||
})
|
||||
|
||||
const openDirtyAt = (worktreeId: string, draft: string): string => {
|
||||
state.openFile(
|
||||
{
|
||||
filePath: '/repo/a.md',
|
||||
relativePath: 'a.md',
|
||||
worktreeId,
|
||||
runtimeEnvironmentId: null,
|
||||
language: 'markdown',
|
||||
mode: 'edit'
|
||||
},
|
||||
{ suppressActiveRuntimeFallback: true }
|
||||
)
|
||||
const id = useAppStore.getState().openFiles.find((f) => f.worktreeId === worktreeId)!.id
|
||||
state.setEditorDraft(id, draft)
|
||||
state.markFileDirty(id, true)
|
||||
return id
|
||||
}
|
||||
openDirtyAt('wt-a', 'draft A')
|
||||
const idB = openDirtyAt('wt-b', 'draft B')
|
||||
|
||||
const result = remapOpenEditorTabsForPathChange({
|
||||
fromPath: '/repo/a.md',
|
||||
toPath: '/repo/b.md',
|
||||
worktreePath: '/repo',
|
||||
worktreeId: 'wt-a'
|
||||
})
|
||||
expect(result.ok).toBe(true)
|
||||
|
||||
const files = useAppStore.getState().openFiles
|
||||
// connA's tab moved; connB's tab kept its id, path, AND its dirty draft.
|
||||
expect(files.find((f) => f.worktreeId === 'wt-a')!.filePath).toBe('/repo/b.md')
|
||||
const tabB = files.find((f) => f.worktreeId === 'wt-b')!
|
||||
expect(tabB.id).toBe(idB)
|
||||
expect(tabB.filePath).toBe('/repo/a.md')
|
||||
expect(tabB.isDirty).toBe(true)
|
||||
expect(useAppStore.getState().editorDrafts[idB]).toBe('draft B')
|
||||
})
|
||||
|
||||
it('clears the untitled marker when remapping a renamed new markdown file', () => {
|
||||
const state = useAppStore.getState()
|
||||
const oldPath = '/repo/untitled.md'
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useAppStore } from '@/store'
|
||||
import { detectLanguage } from '@/lib/language-detect'
|
||||
import { basename } from '@/lib/path'
|
||||
import { getExecutionHostIdForWorktree } from '@/lib/worktree-runtime-owner'
|
||||
import {
|
||||
buildDiffEditorFileId,
|
||||
buildOwnedEditorFileId,
|
||||
@@ -147,7 +148,15 @@ export function remapOpenEditorTabsForPathChange({
|
||||
moveOperationId?: string
|
||||
}): RekeyOpenFilesResult {
|
||||
const state = useAppStore.getState()
|
||||
const filesToMove = state.openFiles.filter((file) => isPathInsideOrEqual(fromPath, file.filePath))
|
||||
// The rename only touched the initiating execution host. The same absolute
|
||||
// path can be open on a DIFFERENT host (a second SSH connection, or local vs
|
||||
// runtime) as a distinct physical file — never retarget those tabs.
|
||||
const initiatingHostId = getExecutionHostIdForWorktree(state, worktreeId)
|
||||
const filesToMove = state.openFiles.filter(
|
||||
(file) =>
|
||||
isPathInsideOrEqual(fromPath, file.filePath) &&
|
||||
getExecutionHostIdForWorktree(state, file.worktreeId) === initiatingHostId
|
||||
)
|
||||
if (filesToMove.length === 0) {
|
||||
return { ok: true }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user