fix(editor): a rename retargets only editors whose own owner is the renaming host (#31091)

* fix(editor): a rename retargets only editors whose own owner is the renaming host

Each open tab is matched to the move by the owner it captured when it opened (its
server and SSH host), not by looking its worktree id up again, so a same-id twin's
editor on another host keeps its path and its unsaved draft.

* fix(editor): a server route that names no machine never strands the renaming host's own tab

* fix(editor): only an SSH host a tab names excludes it from a rename; a local route can be this app's view of one

* fix(editor): a rename's fallback host check reads both sides at once, never a stale pre-rename host

* fix(editor): a rename matches tabs by the rows' owners, never the Active Server focus

* fix(editor): a tab from another workspace moves only when that workspace is on the host the rename went to

* fix(editor): a rename's cross-workspace check uses the File Explorer's own host resolver on both sides
This commit is contained in:
Neil
2026-10-20 03:58:23 -07:00
committed by GitHub
parent 4e998861e1
commit d9a89dcb0c
7 changed files with 385 additions and 55 deletions
@@ -84,6 +84,63 @@ describe('executeOpenEditorPathMove', () => {
expect(__activeEditorPathMoveCountForTests()).toBe(0)
})
it('never retargets the same path open from a same-id workspace on a server', async () => {
const localId = openDirtyTab()
const state = useAppStore.getState()
state.openFile({
filePath: '/repo/a.md',
relativePath: 'a.md',
worktreeId: 'wt-1',
runtimeEnvironmentId: 'env-server',
language: 'markdown',
mode: 'edit'
})
const remote = useAppStore
.getState()
.openFiles.find((file) => file.runtimeEnvironmentId === 'env-server')!
state.setEditorDraft(remote.id, 'server draft')
state.markFileDirty(remote.id, true)
await executeOpenEditorPathMove({
context: { ...CONTEXT, expectedExecutionHostId: 'local' },
fromPath: '/repo/a.md',
toPath: '/repo/b.md',
worktreeId: 'wt-1',
worktreePath: '/repo'
})
const files = useAppStore.getState().openFiles
expect(files.find((file) => file.runtimeEnvironmentId !== 'env-server')!.filePath).toBe(
'/repo/b.md'
)
expect(files.find((file) => file.id === remote.id)).toMatchObject({
filePath: '/repo/a.md',
isDirty: true
})
expect(useAppStore.getState().editorDrafts[remote.id]).toBe('server draft')
expect(useAppStore.getState().editorDrafts[localId]).toBeUndefined()
})
it('still moves its own tab when the workspace host focus changes while the rename runs', async () => {
openDirtyTab()
mocks.renameRuntimePath.mockImplementation(async () => {
useAppStore.setState({
activeWorktreeId: 'wt-1',
activeWorkspaceExecutionHostId: 'runtime:env-other'
})
})
await executeOpenEditorPathMove({
context: { ...CONTEXT, expectedExecutionHostId: 'local' },
fromPath: '/repo/a.md',
toPath: '/repo/b.md',
worktreeId: 'wt-1',
worktreePath: '/repo'
})
expect(useAppStore.getState().openFiles[0]!.filePath).toBe('/repo/b.md')
})
it('declares the moved file as the document for a document-scoped rename', async () => {
openDirtyTab()
@@ -12,10 +12,8 @@ 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,
runtimeEnvironmentIdWithLegacyFallbackUnlessTwin
} from '@/lib/worktree-runtime-owner'
import { runtimeEnvironmentIdWithLegacyFallbackUnlessTwin } from '@/lib/worktree-runtime-owner'
import { editorMoveOwnerFromContext, isOpenFileOnMoveOwner } from '@/lib/open-editor-move-owner'
import { documentFolderAccess } from '@/lib/local-file-access'
let moveOperationCounter = 0
@@ -42,11 +40,9 @@ export async function executeOpenEditorPathMove(args: {
// 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 owner = editorMoveOwnerFromContext(context, worktreeId)
const affected = moveState.openFiles.filter(
(f) =>
isPathInsideOrEqual(fromPath, f.filePath) &&
getExecutionHostIdForWorktree(moveState, f.worktreeId) === initiatingHostId
(f) => isPathInsideOrEqual(fromPath, f.filePath) && isOpenFileOnMoveOwner(moveState, f, owner)
)
// Suppress the move ROOT (prefix-matched) per scope, always including the
@@ -108,6 +104,7 @@ export async function executeOpenEditorPathMove(args: {
toPath,
worktreePath,
worktreeId,
owner,
moveOperationId: operationId
})
if (!rekeyResult.ok) {
@@ -0,0 +1,115 @@
import { beforeEach, describe, expect, it } from 'vitest'
import { useAppStore } from '@/store'
import type { EditorFileOperationProvenance } from '@/lib/editor-file-operation-owner'
import type { WorktreeOperationRoute } from '@/lib/worktree-operation-route'
import { makeWorktree, TEST_REPO } from '@/store/slices/worktrees-slice-test-fixtures'
import { editorMoveOwnerFromContext, isOpenFileOnMoveOwner } from './open-editor-move-owner'
function captured(route: WorktreeOperationRoute): EditorFileOperationProvenance {
return {
ownershipProjection: 'explicit',
generation: {
route,
runtimeConnectionGeneration: null,
runtimePairingRevision: undefined,
runtimeSshGeneration: null,
nestedSshGeneration: null,
directSshGeneration: null
}
}
}
// As openFile records it: the tab's server matches the one its route names.
const tab = (operationProvenance?: EditorFileOperationProvenance) => ({
worktreeId: 'wt-1',
runtimeEnvironmentId: operationProvenance?.generation.route.runtimeEnvironmentId ?? null,
...(operationProvenance ? { operationProvenance } : {})
})
describe('isOpenFileOnMoveOwner', () => {
beforeEach(() => {
useAppStore.setState(useAppStore.getInitialState(), true)
})
it('never moves a direct-SSH tab for a rename on this app, and moves it for its own host', () => {
const state = useAppStore.getState()
const local = tab(captured({ executionHostId: 'local', runtimeEnvironmentId: null }))
const ssh = tab(captured({ executionHostId: 'ssh:box', runtimeEnvironmentId: null }))
const fromLocal = editorMoveOwnerFromContext(
{ target: { kind: 'local' }, expectedExecutionHostId: 'local' },
'wt-1'
)
const fromSsh = editorMoveOwnerFromContext(
{ target: { kind: 'local' }, expectedExecutionHostId: 'ssh:box' },
'wt-1'
)
expect([local, ssh].map((file) => isOpenFileOnMoveOwner(state, file, fromLocal))).toEqual([
true,
false
])
expect(isOpenFileOnMoveOwner(state, ssh, fromSsh)).toBe(true)
})
it('moves a tab recorded as local when the File Explorer resolves its folder to an SSH host', () => {
const state = useAppStore.getState()
// An unstamped folder of SSH repos: the tab's route says this app, its rename goes to the host.
const folderTab = tab(captured({ executionHostId: 'local', runtimeEnvironmentId: null }))
const owner = editorMoveOwnerFromContext(
{ target: { kind: 'local' }, expectedExecutionHostId: 'ssh:box' },
'wt-1'
)
expect(isOpenFileOnMoveOwner(state, folderTab, owner)).toBe(true)
})
it('moves a server tab whose route names only the server when the rename ran on its SSH host', () => {
const state = useAppStore.getState()
const serverTab = tab(
captured({ executionHostId: 'runtime:env-a', runtimeEnvironmentId: 'env-a' })
)
const owner = editorMoveOwnerFromContext(
{
target: { kind: 'environment', environmentId: 'env-a' },
expectedExecutionHostId: 'ssh:box'
},
'wt-1'
)
expect(isOpenFileOnMoveOwner(state, serverTab, owner)).toBe(true)
})
it('never moves another server’s tab for the same workspace id', () => {
const state = useAppStore.getState()
const owner = editorMoveOwnerFromContext(
{ target: { kind: 'environment', environmentId: 'env-a' }, expectedExecutionHostId: 'local' },
'wt-1'
)
expect(
isOpenFileOnMoveOwner(
state,
tab(captured({ executionHostId: 'runtime:env-b', runtimeEnvironmentId: 'env-b' })),
owner
)
).toBe(false)
expect(isOpenFileOnMoveOwner(state, tab(), owner)).toBe(false)
})
it('moves a tab from another workspace only when that workspace is on the renaming host', () => {
useAppStore.setState({
repos: [
{ ...TEST_REPO, id: 'repo-ssh', connectionId: 'box' },
{ ...TEST_REPO, id: 'repo-local' }
],
worktreesByRepo: {
'repo-ssh': [makeWorktree({ id: 'wt-ssh', repoId: 'repo-ssh' })],
'repo-local': [makeWorktree({ id: 'wt-local', repoId: 'repo-local' })]
}
})
const state = useAppStore.getState()
const owner = editorMoveOwnerFromContext(
{ target: { kind: 'local' }, expectedExecutionHostId: 'ssh:box' },
'wt-1'
)
const inWorkspace = (worktreeId: string) => ({ worktreeId, runtimeEnvironmentId: null })
expect(isOpenFileOnMoveOwner(state, inWorkspace('wt-ssh'), owner)).toBe(true)
expect(isOpenFileOnMoveOwner(state, inWorkspace('wt-local'), owner)).toBe(false)
})
})
@@ -0,0 +1,138 @@
import type { OpenFile } from '@/store/slices/editor'
import type { RuntimeFileOperationArgs } from '@/runtime/runtime-file-client-types'
import { parseExecutionHostId, toSshExecutionHostId } from '../../../shared/execution-host'
import {
getFileExplorerOperationOwnerFromState,
type FileExplorerOwnerState
} from '@/components/right-sidebar/file-explorer-operation-owner'
import { resolveOwner } from './resolve-owner'
/**
* Who a file lives on: the paired server (null for this app) and, when known, the SSH host behind
* it (null for that runtime's own machine, undefined when nothing decisive recorded it).
*/
type FileOwner = { runtimeEnvironmentId: string | null; sshHostId: string | null | undefined }
/** The host a move ran on, from the route its rename was sent through. */
export type EditorMoveOwner = FileOwner & {
/** The renamed workspace, for tabs that recorded no decisive owner of their own. */
worktreeId: string | undefined
}
export function editorMoveOwnerFromContext(
context: Pick<RuntimeFileOperationArgs, 'target' | 'expectedExecutionHostId'>,
worktreeId: string | undefined
): EditorMoveOwner {
const expected = parseExecutionHostId(context.expectedExecutionHostId)
return {
runtimeEnvironmentId:
context.target.kind === 'environment' ? context.target.environmentId : null,
sshHostId:
expected?.kind === 'ssh' ? expected.id : expected?.kind === 'local' ? null : undefined,
worktreeId
}
}
/**
* The tab's own owner, recorded when it opened; never re-derived from its worktree id alone. Only an
* SSH host it names is decisive: a `local` or server route can be this app's view of a host the
* File Explorer resolves further (an unstamped folder of SSH repos, a server's nested SSH target).
*/
function openFileOwner(
state: FileExplorerOwnerState,
file: Pick<
OpenFile,
'externalSshTargetId' | 'operationProvenance' | 'runtimeEnvironmentId' | 'worktreeId'
>
): FileOwner {
const captured = file.operationProvenance?.generation.route
const capturedHost = parseExecutionHostId(captured?.executionHostId)
const runtimeEnvironmentId =
file.runtimeEnvironmentId !== undefined
? file.runtimeEnvironmentId?.trim() || null
: captured
? captured.runtimeEnvironmentId
: ownerEnvironmentId(state, file.worktreeId)
return {
runtimeEnvironmentId,
sshHostId:
capturedHost?.kind === 'ssh'
? capturedHost.id
: file.externalSshTargetId
? toSshExecutionHostId(file.externalSshTargetId)
: undefined
}
}
/**
* Whether an open tab is the file the move renamed. Why its own owner: the same path open from a
* same-id workspace on another host is a different file, and its draft must stay where it is.
*/
export function isOpenFileOnMoveOwner(
state: FileExplorerOwnerState,
file: Pick<
OpenFile,
'externalSshTargetId' | 'operationProvenance' | 'runtimeEnvironmentId' | 'worktreeId'
>,
owner: EditorMoveOwner
): boolean {
const own = openFileOwner(state, file)
if (own.runtimeEnvironmentId !== owner.runtimeEnvironmentId) {
return false
}
if (own.sshHostId !== undefined && owner.sshHostId !== undefined) {
return own.sshHostId === owner.sshHostId
}
// Why read now, from the rows: a lookup from before the rename, or one following focus, can disagree.
if (file.worktreeId === owner.worktreeId) {
return true
}
const tabHost = workspaceHost(state, file.worktreeId)
const moveHost =
owner.sshHostId === undefined
? workspaceHost(state, owner.worktreeId)
: { runtimeEnvironmentId: owner.runtimeEnvironmentId, sshHostId: owner.sshHostId }
return (
tabHost !== null &&
moveHost !== null &&
tabHost.runtimeEnvironmentId === moveHost.runtimeEnvironmentId &&
tabHost.sshHostId === moveHost.sshHostId
)
}
/**
* Where the File Explorer sends a workspace's file operations, the resolver its rename used, so a
* folder of SSH repos reads as that host on both sides. Null for a twin.
*/
function workspaceHost(
state: FileExplorerOwnerState,
workspaceId: string | undefined
): FileOwner | null {
const owner = getFileExplorerOperationOwnerFromState(state, workspaceId)
switch (owner.kind) {
case 'local':
return { runtimeEnvironmentId: null, sshHostId: null }
case 'ssh':
return { runtimeEnvironmentId: null, sshHostId: toSshExecutionHostId(owner.connectionId) }
case 'runtime': {
const host = parseExecutionHostId(owner.executionHostId)
return {
runtimeEnvironmentId: owner.environmentId,
sshHostId: host?.kind === 'ssh' ? host.id : null
}
}
case 'unresolved':
// A workspace with no rows is this app's, as everywhere; a twin's host can't be told.
return workspaceId && resolveOwner(state, { workspaceId }).kind === 'missing'
? { runtimeEnvironmentId: null, sshHostId: null }
: null
}
}
/** The paired server a workspace's rows name, or null for this app, a twin or no row. */
function ownerEnvironmentId(state: FileExplorerOwnerState, workspaceId: string): string | null {
const match = resolveOwner(state, { workspaceId })
return match.kind === 'resolved' && match.owner.endpoint.kind === 'environment'
? match.owner.endpoint.environmentId
: null
}
@@ -2,6 +2,7 @@ import { beforeEach, describe, expect, it } from 'vitest'
import { useAppStore } from '@/store'
import { remapOpenEditorTabsForPathChange } from './remap-open-editor-tabs-for-path-change'
import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../shared/constants'
import { editorMoveOwnerFromContext, type EditorMoveOwner } from '@/lib/open-editor-move-owner'
function ownedEditorFileId(
filePath: string,
@@ -12,12 +13,16 @@ function ownedEditorFileId(
return `editor:${encodeURIComponent(worktreeId)}:${encodeURIComponent(runtimeKey)}:${encodeURIComponent(filePath)}`
}
function localMoveOwner(worktreeId: string | undefined): EditorMoveOwner {
return editorMoveOwnerFromContext({ target: { kind: 'local' } }, worktreeId)
}
describe('remapOpenEditorTabsForPathChange', () => {
beforeEach(() => {
useAppStore.setState(useAppStore.getInitialState(), true)
})
it('preserves runtime owners, drafts, dirty state, and markdown preview sources', () => {
it('moves only the renaming host’s editor of a same-id twin, with its draft and preview', () => {
const state = useAppStore.getState()
const worktreeId = 'wt-1'
const worktreePath = '/repo'
@@ -75,46 +80,40 @@ describe('remapOpenEditorTabsForPathChange', () => {
fromPath: '/repo/docs',
toPath: '/repo/notes',
worktreePath,
worktreeId
worktreeId,
owner: editorMoveOwnerFromContext(
{
target: { kind: 'environment', environmentId: 'env-remote' },
expectedExecutionHostId: 'local'
},
worktreeId
)
})
const nextState = useAppStore.getState()
expect(nextState.openFiles.some((file) => file.filePath === oldPath)).toBe(false)
const localRemapped = nextState.openFiles.find(
(file) =>
file.filePath === newPath && file.mode === 'edit' && file.runtimeEnvironmentId === null
)
const remoteRemapped = nextState.openFiles.find(
(file) =>
file.filePath === newPath &&
file.mode === 'edit' &&
file.runtimeEnvironmentId === 'env-remote'
)
expect(localRemapped).toMatchObject({
relativePath: 'notes/readme.md',
isDirty: true,
runtimeEnvironmentId: null
})
expect(remoteRemapped).toMatchObject({
relativePath: 'notes/readme.md',
isDirty: true,
runtimeEnvironmentId: 'env-remote'
})
expect(nextState.editorDrafts[localRemapped!.id]).toBe('local draft')
expect(remoteRemapped).toMatchObject({ relativePath: 'notes/readme.md', isDirty: true })
expect(nextState.editorDrafts[remoteRemapped!.id]).toBe('remote draft')
expect(nextState.editorDrafts[localEditId!]).toBeUndefined()
expect(nextState.editorDrafts[remoteEdit!.id]).toBeUndefined()
const remotePreview = nextState.openFiles.find(
(file) => file.mode === 'markdown-preview' && file.runtimeEnvironmentId === 'env-remote'
)
expect(remotePreview).toMatchObject({
expect(
nextState.openFiles.find(
(file) => file.mode === 'markdown-preview' && file.runtimeEnvironmentId === 'env-remote'
)
).toMatchObject({
filePath: newPath,
relativePath: 'notes/readme.md',
markdownPreviewAnchor: 'heading',
markdownPreviewSourceFileId: remoteRemapped!.id
})
// The local twin's file never moved, so its tab and unsaved draft stay where they were.
const local = nextState.openFiles.find((file) => file.id === localEditId)
expect(local).toMatchObject({ filePath: oldPath, isDirty: true, runtimeEnvironmentId: null })
expect(nextState.editorDrafts[localEditId!]).toBe('local draft')
})
it('retargets preview-only markdown source ids to the moved owner path', () => {
@@ -149,7 +148,8 @@ describe('remapOpenEditorTabsForPathChange', () => {
fromPath: '/repo/docs',
toPath: '/repo/notes',
worktreePath,
worktreeId: 'wt-1'
worktreeId: 'wt-1',
owner: localMoveOwner('wt-1')
})
const floatingPreview = useAppStore
@@ -203,7 +203,8 @@ describe('remapOpenEditorTabsForPathChange', () => {
fromPath: '/repo/a.md',
toPath: '/repo/b.md',
worktreePath: '/repo',
worktreeId: 'wt-a'
worktreeId: 'wt-a',
owner: localMoveOwner('wt-a')
})
expect(result.ok).toBe(true)
@@ -240,7 +241,8 @@ describe('remapOpenEditorTabsForPathChange', () => {
fromPath: 'c:\\repo\\src',
toPath: 'c:\\repo\\dst',
worktreePath: 'c:\\repo',
worktreeId: 'wt-1'
worktreeId: 'wt-1',
owner: localMoveOwner('wt-1')
})
expect(result.ok).toBe(true)
@@ -273,7 +275,8 @@ describe('remapOpenEditorTabsForPathChange', () => {
fromPath: '\\\\wsl$\\Ubuntu\\repo\\src',
toPath: '\\\\wsl$\\Ubuntu\\repo\\dst',
worktreePath: '\\\\wsl$\\Ubuntu\\repo',
worktreeId: 'wt-1'
worktreeId: 'wt-1',
owner: localMoveOwner('wt-1')
})
expect(result.ok).toBe(true)
@@ -301,7 +304,8 @@ describe('remapOpenEditorTabsForPathChange', () => {
fromPath: '/repo/src',
toPath: '/repo/dst',
worktreePath: '/repo',
worktreeId: 'wt-1'
worktreeId: 'wt-1',
owner: localMoveOwner('wt-1')
})
expect(useAppStore.getState().openFiles[0]!.filePath).toBe('/repo/dst/a\\b.txt')
@@ -338,7 +342,8 @@ describe('remapOpenEditorTabsForPathChange', () => {
fromPath: '/repo/src',
toPath: '/repo/dst',
worktreePath: '/repo',
worktreeId: 'wt-1'
worktreeId: 'wt-1',
owner: localMoveOwner('wt-1')
})
const floating = useAppStore
@@ -369,7 +374,8 @@ describe('remapOpenEditorTabsForPathChange', () => {
fromPath: '/repo/src',
toPath: '/repo/dst\\',
worktreePath: '/repo',
worktreeId: 'wt-1'
worktreeId: 'wt-1',
owner: localMoveOwner('wt-1')
})
expect(useAppStore.getState().openFiles[0]!.filePath).toBe('/repo/dst\\/a.ts')
@@ -392,7 +398,8 @@ describe('remapOpenEditorTabsForPathChange', () => {
fromPath: '/srv/team\\repo/src',
toPath: '/srv/team\\repo/dst',
worktreePath: '/srv/team\\repo',
worktreeId: 'wt-1'
worktreeId: 'wt-1',
owner: localMoveOwner('wt-1')
})
// POSIX flavor: the separator stays `/`; the ancestor backslash is untouched.
@@ -416,7 +423,8 @@ describe('remapOpenEditorTabsForPathChange', () => {
fromPath: 'C:\\Repo\\\\Src',
toPath: 'C:\\Repo\\Dst',
worktreePath: 'C:\\Repo',
worktreeId: 'wt-1'
worktreeId: 'wt-1',
owner: localMoveOwner('wt-1')
})
// Nested suffix preserved, no dropped or doubled separator.
@@ -441,7 +449,8 @@ describe('remapOpenEditorTabsForPathChange', () => {
fromPath: oldPath,
toPath: newPath,
worktreePath: '/repo',
worktreeId: 'wt-1'
worktreeId: 'wt-1',
owner: localMoveOwner('wt-1')
})
expect(useAppStore.getState().openFiles).toHaveLength(1)
@@ -473,7 +482,12 @@ describe('remapOpenEditorTabsForPathChange', () => {
state.setLastKnownDiskSignature(oldId, 'sig-abc')
state.setExternalMutation(oldId, 'changed')
remapOpenEditorTabsForPathChange({ fromPath: oldPath, toPath: newPath, worktreePath: '/repo' })
remapOpenEditorTabsForPathChange({
fromPath: oldPath,
toPath: newPath,
worktreePath: '/repo',
owner: localMoveOwner(undefined)
})
const moved = useAppStore.getState().openFiles.find((f) => f.filePath === newPath)
expect(moved).toBeTruthy()
@@ -503,7 +517,8 @@ describe('remapOpenEditorTabsForPathChange', () => {
fromPath: '/repo/docs',
toPath: '/repo/notes',
worktreePath: '/repo',
worktreeId: 'wt-1'
worktreeId: 'wt-1',
owner: localMoveOwner('wt-1')
})
const moved = useAppStore.getState().openFiles.find((f) => f.mode === 'diff')
@@ -1,7 +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 { isOpenFileOnMoveOwner, type EditorMoveOwner } from '@/lib/open-editor-move-owner'
import {
buildDiffEditorFileId,
buildOwnedEditorFileId,
@@ -192,25 +192,25 @@ export function remapOpenEditorTabsForPathChange({
toPath,
worktreePath,
worktreeId,
owner,
moveOperationId
}: {
fromPath: string
toPath: string
worktreePath: string
worktreeId?: string
/** The host the rename ran on; only tabs whose own owner it is are retargeted. */
owner: EditorMoveOwner
/** Passed by the move coordinator so dirty destinations get a content-verify
* gate + provenance installed atomically with the re-home. */
moveOperationId?: string
}): RekeyOpenFilesResult {
const state = useAppStore.getState()
// 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)
// The rename only touched its own host. The same absolute path can be open on another host (a
// second SSH connection, local vs runtime, or a same-id twin) as a distinct file.
const filesToMove = state.openFiles.filter(
(file) =>
isPathInsideOrEqual(fromPath, file.filePath) &&
getExecutionHostIdForWorktree(state, file.worktreeId) === initiatingHostId
isPathInsideOrEqual(fromPath, file.filePath) && isOpenFileOnMoveOwner(state, file, owner)
)
if (filesToMove.length === 0) {
return { ok: true }
@@ -12,6 +12,7 @@ import type { LayoutCommand } from '../../../../shared/workspace-layout/workspac
import type { WorkspaceLayoutModel } from '../../../../shared/workspace-layout/workspace-layout-model'
import { publishWorkspaceLayout } from '../../../../shared/workspace-layout/workspace-layout-published'
import type { WorkspaceLayoutChangeFrame } from '../../../../shared/workspace-layout/workspace-layout-stream-frames'
import { editorMoveOwnerFromContext, type EditorMoveOwner } from '@/lib/open-editor-move-owner'
const mocks = vi.hoisted(() => ({
layoutOwnerIsMain: true,
@@ -171,6 +172,10 @@ function openEdit(worktreeId: string, filePath: string, relativePath: string): s
)
}
function localMoveOwner(worktreeId: string | undefined): EditorMoveOwner {
return editorMoveOwnerFromContext({ target: { kind: 'local' } }, worktreeId)
}
describe('editor records under frames, layout-owner build', () => {
it('keeps the file record of every editor tab main keeps', async () => {
const state = useAppStore.getState()
@@ -253,7 +258,8 @@ describe('open files renamed on disk, layout-owner build', () => {
fromPath: '/repo-a/src',
toPath: '/repo-a/lib',
worktreePath: '/repo-a',
worktreeId: WT
worktreeId: WT,
owner: localMoveOwner(WT)
})
expect(result).toEqual({ ok: true })
@@ -284,7 +290,8 @@ describe('open files renamed on disk, layout-owner build', () => {
fromPath: '/repo-a/a.ts',
toPath: '/repo-a/b.ts',
worktreePath: '/repo-a',
worktreeId: WT
worktreeId: WT,
owner: localMoveOwner(WT)
})
).toEqual({ ok: false, reason: 'collision' })
expect(applySent()).toEqual([])
@@ -295,7 +302,8 @@ describe('open files renamed on disk, layout-owner build', () => {
fromPath: '/repo-a/a.ts',
toPath: '/repo-a/c.ts',
worktreePath: '/repo-a',
worktreeId: WT
worktreeId: WT,
owner: localMoveOwner(WT)
})
).toEqual({ ok: true })
expect(applySent()).toEqual([])