diff --git a/src/renderer/src/components/editor/EditorContent.monaco-lifecycle.test.tsx b/src/renderer/src/components/editor/EditorContent.monaco-lifecycle.test.tsx index 0929caaded5..8be800d1ddf 100644 --- a/src/renderer/src/components/editor/EditorContent.monaco-lifecycle.test.tsx +++ b/src/renderer/src/components/editor/EditorContent.monaco-lifecycle.test.tsx @@ -2,10 +2,12 @@ import { cleanup, render } from '@testing-library/react' import { afterEach, describe, expect, it, vi } from 'vitest' import type { OpenFile } from '@/store/slices/editor' +import { buildOwnedEditorFileId } from '@/store/slices/editor/file-ids/editor-file-ids' const lifecycle = vi.hoisted(() => ({ events: [] as string[], diffModelKeys: [] as string[], + viewStateKeys: new Array(), models: new Map(), notebookProps: [] as { fileId: string @@ -70,7 +72,9 @@ vi.mock('@/lib/lazy-with-retry', async () => { readOnly?: boolean liveTail?: boolean viewStateId?: string + viewStateKey: string }) { + lifecycle.viewStateKeys.push(props.viewStateKey) /* oxlint-disable react-hooks/exhaustive-deps -- Mount-only by design: a prop-effect would hide a missing outer React remount. */ React.useEffect(() => { lifecycle.events.push(`mount:${props.filePath}`) @@ -215,6 +219,7 @@ afterEach(() => { cleanup() lifecycle.events.length = 0 lifecycle.diffModelKeys.length = 0 + lifecycle.viewStateKeys.length = 0 lifecycle.models.clear() lifecycle.mountedProps.length = 0 lifecycle.notebookProps.length = 0 @@ -222,6 +227,28 @@ afterEach(() => { }) describe('EditorContent Monaco lifecycle boundary', () => { + it('keeps default-pane view state separate for the same path on different runtime hosts', () => { + const filePath = '/repo/main.ts' + const worktreeId = 'repo::/repo' + const first = file(filePath, { + id: buildOwnedEditorFileId(filePath, worktreeId, 'host-a'), + runtimeEnvironmentId: 'host-a' + }) + const second = file(filePath, { + id: buildOwnedEditorFileId(filePath, worktreeId, 'host-b'), + runtimeEnvironmentId: 'host-b' + }) + const view = render( + + ) + const firstKey = lifecycle.viewStateKeys.at(-1) + expect(firstKey).toBeTypeOf('string') + + view.rerender() + + expect(lifecycle.viewStateKeys.at(-1)).not.toBe(firstKey) + }) + it('unmounts the prior path before reconciling a stale retained target model', () => { const first = file('/repo/first.ts') const second = file('/repo/second.ts') diff --git a/src/renderer/src/components/editor/EditorContent.tsx b/src/renderer/src/components/editor/EditorContent.tsx index 68e56b20c37..cbf3e2dfd34 100644 --- a/src/renderer/src/components/editor/EditorContent.tsx +++ b/src/renderer/src/components/editor/EditorContent.tsx @@ -98,7 +98,7 @@ export function EditorContent({ }): React.JSX.Element { const editorViewStateKey = viewStateScopeId === activeFile.id - ? activeFile.filePath + ? activeFile.id : `${activeFile.filePath}::${viewStateScopeId}` const diffViewStateKey = viewStateScopeId === activeFile.id ? activeFile.id : `${activeFile.id}::${viewStateScopeId}` @@ -109,7 +109,7 @@ export function EditorContent({ // Why: only the single-pane edit path gets PDF scroll memory — diff and conflict review mount several viewers on one path. const pdfViewStateKey = viewStateScopeId === activeFile.id - ? `${activeFile.filePath}:pdf` + ? `${activeFile.id}:pdf` : `${activeFile.filePath}::${viewStateScopeId}:pdf` // Scope durable PDF zoom by owner: identical paths can belong to different worktrees or remote hosts. const pdfPreferenceKey = buildPdfScalePreferenceKey(activeFile)