diff --git a/src/renderer/src/store/slices/editor.test.ts b/src/renderer/src/store/slices/editor.test.ts index a3ec3f471..0b570c41f 100644 --- a/src/renderer/src/store/slices/editor.test.ts +++ b/src/renderer/src/store/slices/editor.test.ts @@ -456,6 +456,39 @@ describe('createEditorSlice openDiff', () => { ]) }) + it('derives a runtime owner for source-control diffs from the worktree host', () => { + const store = createEditorStore() + store.setState({ + repos: [{ id: 'repo-1', executionHostId: 'runtime:env-1' }] as unknown as AppState['repos'], + worktreesByRepo: { + 'repo-1': [ + { + id: 'repo-1::/srv/repo/worktree', + repoId: 'repo-1', + hostId: 'runtime:env-1' + } + ] + } as unknown as AppState['worktreesByRepo'] + }) + + store + .getState() + .openDiff( + 'repo-1::/srv/repo/worktree', + '/srv/repo/worktree/src/file.ts', + 'src/file.ts', + 'typescript', + false + ) + + expect(store.getState().openFiles[0]).toEqual( + expect.objectContaining({ + id: 'editor-diff:repo-1%3A%3A%2Fsrv%2Frepo%2Fworktree:env-1:unstaged:src%2Ffile.ts', + runtimeEnvironmentId: 'env-1' + }) + ) + }) + it('repairs an existing diff tab entry to the correct mode and staged state', () => { const store = createEditorStore() @@ -2769,6 +2802,46 @@ describe('createEditorSlice combined diff exclusions', () => { }) }) +describe('createEditorSlice openBranchDiff', () => { + it('derives a runtime owner for branch diffs from the worktree host', () => { + const store = createEditorStore() + const worktreeId = 'repo-1::/srv/repo/worktree' + const branchSummary: GitBranchCompareSummary = { + baseRef: 'main', + baseOid: 'base-oid', + compareRef: 'HEAD', + headOid: 'head-oid', + mergeBase: 'merge-base-oid', + changedFiles: 1, + status: 'ready' + } + store.setState({ + repos: [{ id: 'repo-1', executionHostId: 'runtime:env-1' }] as unknown as AppState['repos'], + worktreesByRepo: { + 'repo-1': [{ id: worktreeId, repoId: 'repo-1', hostId: 'runtime:env-1' }] + } as unknown as AppState['worktreesByRepo'] + }) + + store + .getState() + .openBranchDiff( + worktreeId, + '/srv/repo/worktree', + { path: 'src/file.ts', status: 'modified' }, + branchSummary, + 'typescript' + ) + + expect(store.getState().openFiles[0]).toEqual( + expect.objectContaining({ + diffSource: 'branch', + filePath: '/srv/repo/worktree/src/file.ts', + runtimeEnvironmentId: 'env-1' + }) + ) + }) +}) + describe('createEditorSlice remote branch actions', () => { const gitStatusMock = vi.fn() const gitUpstreamStatusMock = vi.fn() diff --git a/src/renderer/src/store/slices/editor.ts b/src/renderer/src/store/slices/editor.ts index 9aac34436..54013b30a 100644 --- a/src/renderer/src/store/slices/editor.ts +++ b/src/renderer/src/store/slices/editor.ts @@ -61,6 +61,7 @@ import { import { settingsForRuntimeOwner } from '@/runtime/runtime-rpc-client' import { notifyHostOfMirroredEditorClose } from '@/runtime/close-mirrored-editor-tab' import { findWorktreeById, getRepoIdFromWorktreeId } from './worktree-helpers' +import { getRuntimeEnvironmentIdForWorktree } from '@/lib/worktree-runtime-owner' import { createUntitledMarkdownFileWithTemplateSelection } from '@/lib/create-untitled-markdown' import { extractIpcErrorMessage } from '@/lib/ipc-error' import { translate } from '@/i18n/i18n' @@ -299,6 +300,19 @@ type GitRuntimeOperationOptions = { applyUpstreamStatus?: boolean } +function resolveDiffRuntimeEnvironmentId( + state: AppState, + worktreeId: string, + explicitRuntimeEnvironmentId: string | null | undefined +): string | null | undefined { + if (explicitRuntimeEnvironmentId !== undefined) { + return explicitRuntimeEnvironmentId + } + // Why: Source Control callers often know only the worktree. Runtime-host + // diffs still need their owner stamped so content loads through runtime RPC. + return getRuntimeEnvironmentIdForWorktree(state, worktreeId) ?? undefined +} + export type PendingEditorReveal = { filePath: string fileId?: string @@ -2415,10 +2429,14 @@ export const createEditorSlice: StateCreator = (s openDiff: (worktreeId, filePath, relativePath, language, staged, options) => { const isPreview = options?.preview ?? false - const runtimeEnvironmentId = options?.runtimeEnvironmentId let editorItemTargetGroupId = options?.targetGroupId let editorItemFileId = '' set((s) => { + const runtimeEnvironmentId = resolveDiffRuntimeEnvironmentId( + s, + worktreeId, + options?.runtimeEnvironmentId + ) const diffSource: DiffSource = staged ? 'staged' : 'unstaged' const id = buildDiffEditorFileId(worktreeId, diffSource, relativePath, runtimeEnvironmentId) editorItemFileId = id @@ -2459,7 +2477,7 @@ export const createEditorSlice: StateCreator = (s skippedConflicts: undefined, conflictReview: undefined, isPreview: isPreview || undefined, - runtimeEnvironmentId: options?.runtimeEnvironmentId + runtimeEnvironmentId } if (isPreview) { const replaceablePreviewId = getReplaceablePreviewFileId(s, worktreeId, targetGroupId) @@ -2507,6 +2525,11 @@ export const createEditorSlice: StateCreator = (s const targetGroupId = resolveEditorOpenTargetGroupId(s, worktreeId, options?.targetGroupId) ?? undefined editorItemTargetGroupId = targetGroupId + const runtimeEnvironmentId = resolveDiffRuntimeEnvironmentId( + s, + worktreeId, + options?.runtimeEnvironmentId + ) const existing = s.openFiles.find((f) => f.id === id) if (existing) { const updatedPreview = isPreview ? existing.isPreview : false @@ -2519,7 +2542,8 @@ export const createEditorSlice: StateCreator = (s conflict: undefined, skippedConflicts: undefined, conflictReview: undefined, - isPreview: updatedPreview + isPreview: updatedPreview, + runtimeEnvironmentId }) return { openFiles: s.openFiles.map((f) => (f.id === id ? reopenedDiff : f)), @@ -2543,7 +2567,8 @@ export const createEditorSlice: StateCreator = (s conflict: undefined, skippedConflicts: undefined, conflictReview: undefined, - isPreview: isPreview || undefined + isPreview: isPreview || undefined, + runtimeEnvironmentId } if (isPreview) { const replaceablePreviewId = getReplaceablePreviewFileId(s, worktreeId, targetGroupId) @@ -2591,6 +2616,11 @@ export const createEditorSlice: StateCreator = (s const targetGroupId = resolveEditorOpenTargetGroupId(s, worktreeId, options?.targetGroupId) ?? undefined editorItemTargetGroupId = targetGroupId + const runtimeEnvironmentId = resolveDiffRuntimeEnvironmentId( + s, + worktreeId, + options?.runtimeEnvironmentId + ) const existing = s.openFiles.find((f) => f.id === id) if (existing) { const updatedPreview = isPreview ? existing.isPreview : false @@ -2603,7 +2633,8 @@ export const createEditorSlice: StateCreator = (s conflict: undefined, skippedConflicts: undefined, conflictReview: undefined, - isPreview: updatedPreview + isPreview: updatedPreview, + runtimeEnvironmentId }) return { openFiles: s.openFiles.map((f) => (f.id === id ? reopenedDiff : f)), @@ -2627,7 +2658,8 @@ export const createEditorSlice: StateCreator = (s conflict: undefined, skippedConflicts: undefined, conflictReview: undefined, - isPreview: isPreview || undefined + isPreview: isPreview || undefined, + runtimeEnvironmentId } if (isPreview) { const replaceablePreviewId = getReplaceablePreviewFileId(s, worktreeId, targetGroupId) @@ -2708,6 +2740,7 @@ export const createEditorSlice: StateCreator = (s areaFilter ] ?? 'All Changes') : 'All Changes' + const runtimeEnvironmentId = resolveDiffRuntimeEnvironmentId(s, worktreeId, undefined) const existing = s.openFiles.find((f) => f.id === id) if (existing) { return { @@ -2723,7 +2756,8 @@ export const createEditorSlice: StateCreator = (s combinedAreaFilter: areaFilter, skippedConflicts, conflictReview: undefined, - conflict: undefined + conflict: undefined, + runtimeEnvironmentId } : f ), @@ -2749,7 +2783,8 @@ export const createEditorSlice: StateCreator = (s combinedAreaFilter: areaFilter, skippedConflicts, conflictReview: undefined, - conflict: undefined + conflict: undefined, + runtimeEnvironmentId } return { openFiles: [...s.openFiles, newFile], @@ -3181,6 +3216,7 @@ export const createEditorSlice: StateCreator = (s const branchCompare = toBranchCompareSnapshot(compare) const id = `${worktreeId}::all-diffs::branch::${compare.baseRef}::${branchCompare.compareVersion}` set((s) => { + const runtimeEnvironmentId = resolveDiffRuntimeEnvironmentId(s, worktreeId, undefined) const branchEntriesSnapshot = s.gitBranchChangesByWorktree[worktreeId] ?? [] const existing = s.openFiles.find((f) => f.id === id) if (existing) { @@ -3194,7 +3230,8 @@ export const createEditorSlice: StateCreator = (s combinedAlternate: alternate, conflict: undefined, skippedConflicts: undefined, - conflictReview: undefined + conflictReview: undefined, + runtimeEnvironmentId } : f ), @@ -3218,7 +3255,8 @@ export const createEditorSlice: StateCreator = (s combinedAlternate: alternate, conflict: undefined, skippedConflicts: undefined, - conflictReview: undefined + conflictReview: undefined, + runtimeEnvironmentId } return { openFiles: [...s.openFiles, newFile], @@ -3244,6 +3282,7 @@ export const createEditorSlice: StateCreator = (s ? `Commit ${commitCompare.compareRef}: ${subject}` : `Commit ${commitCompare.compareRef}` set((s) => { + const runtimeEnvironmentId = resolveDiffRuntimeEnvironmentId(s, worktreeId, undefined) const existing = s.openFiles.find((f) => f.id === id) if (existing) { return { @@ -3256,7 +3295,8 @@ export const createEditorSlice: StateCreator = (s commitEntriesSnapshot: entries, conflict: undefined, skippedConflicts: undefined, - conflictReview: undefined + conflictReview: undefined, + runtimeEnvironmentId } : f ), @@ -3280,7 +3320,8 @@ export const createEditorSlice: StateCreator = (s commitEntriesSnapshot: entries, conflict: undefined, skippedConflicts: undefined, - conflictReview: undefined + conflictReview: undefined, + runtimeEnvironmentId } return { openFiles: [...s.openFiles, newFile],