From a70afd5582a1cee6ed6a38da41c09f7d95d82b94 Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Sat, 13 Jun 2026 01:53:20 -0700 Subject: [PATCH] Route file explorer requests by worktree owner Co-authored-by: Orca --- .../host-context-implementation-checklist.md | 1 + ...le-explorer-runtime-owner-boundary.test.ts | 34 +++++++++++++++++++ .../file-explorer-runtime-owner.ts | 12 +++++++ .../right-sidebar/useFileDuplicate.ts | 5 ++- .../right-sidebar/useFileExplorerDragDrop.ts | 3 +- .../right-sidebar/useFileExplorerImport.ts | 5 ++- .../useFileExplorerInlineInput.ts | 3 +- .../right-sidebar/useFileExplorerTree.ts | 6 ++-- .../useFileExplorerVisibleRowProjection.ts | 3 +- .../right-sidebar/useGitStatusPolling.test.ts | 15 ++++++-- .../right-sidebar/useGitStatusPolling.ts | 5 +-- 11 files changed, 76 insertions(+), 16 deletions(-) create mode 100644 src/renderer/src/components/right-sidebar/file-explorer-runtime-owner-boundary.test.ts create mode 100644 src/renderer/src/components/right-sidebar/file-explorer-runtime-owner.ts diff --git a/notes/host-context-implementation-checklist.md b/notes/host-context-implementation-checklist.md index f3695a3e8f7..4488039c1b5 100644 --- a/notes/host-context-implementation-checklist.md +++ b/notes/host-context-implementation-checklist.md @@ -289,3 +289,4 @@ Done means: reviewers and future agents can understand the vision, implementatio - [x] 2026-06-13: Routed Delete Workspace dirty-state preloads through the selected worktree runtime owner instead of ambient focused settings, so delete warnings inspect the host that owns the workspace. Verified with focused DeleteWorktreeDialog host-boundary tests, existing worktree-owner tests, targeted oxlint, `git diff --check`, and full `pnpm run typecheck`. - [x] 2026-06-13: Routed editor file-drop stat/import operations through the selected worktree runtime owner instead of ambient focused settings, so local, SSH, and runtime workspace drops use the host that owns the workspace. Verified with focused file-drop owner tests, targeted oxlint, `git diff --check`, and full `pnpm run typecheck`. - [x] 2026-06-13: Routed agent draft-paste readiness probes and paste writes through the tab's worktree runtime owner when available, covering legacy remote PTY ids that do not embed their runtime owner. Verified with focused agent-paste tests, targeted oxlint, `git diff --check`, and full `pnpm run typecheck`. +- [x] 2026-06-13: Routed File Explorer directory reads, stat/import/create/move/duplicate operations, ignored-file checks, and background git/conflict polling through the selected worktree runtime owner instead of ambient focused settings. Verified with focused right-sidebar boundary/projection/polling/drag tests, targeted oxlint, `git diff --check`, and full `pnpm run typecheck`. diff --git a/src/renderer/src/components/right-sidebar/file-explorer-runtime-owner-boundary.test.ts b/src/renderer/src/components/right-sidebar/file-explorer-runtime-owner-boundary.test.ts new file mode 100644 index 00000000000..24f6cd1cc33 --- /dev/null +++ b/src/renderer/src/components/right-sidebar/file-explorer-runtime-owner-boundary.test.ts @@ -0,0 +1,34 @@ +import { readFileSync } from 'node:fs' +import { join } from 'node:path' +import { describe, expect, it } from 'vitest' + +const root = process.cwd() + +function source(path: string): string { + return readFileSync(join(root, path), 'utf8') +} + +describe('right sidebar file/git runtime ownership boundaries', () => { + it.each([ + 'src/renderer/src/components/right-sidebar/useFileExplorerTree.ts', + 'src/renderer/src/components/right-sidebar/useFileExplorerImport.ts', + 'src/renderer/src/components/right-sidebar/useFileExplorerInlineInput.ts', + 'src/renderer/src/components/right-sidebar/useFileExplorerDragDrop.ts', + 'src/renderer/src/components/right-sidebar/useFileDuplicate.ts', + 'src/renderer/src/components/right-sidebar/useFileExplorerVisibleRowProjection.ts', + 'src/renderer/src/components/right-sidebar/useGitStatusPolling.ts' + ])('%s routes file/git requests by the selected worktree owner', (path) => { + const text = source(path) + + expect(text).toContain('getRightSidebarWorktreeRuntimeSettings') + expect(text).not.toContain('settings: useAppStore.getState().settings') + expect(text).not.toContain('const settings = useAppStore.getState().settings') + }) + + it('derives owner settings through the shared worktree runtime owner helper', () => { + const text = source('src/renderer/src/components/right-sidebar/file-explorer-runtime-owner.ts') + + expect(text).toContain('getSettingsForWorktreeRuntimeOwner') + expect(text).toContain('useAppStore.getState()') + }) +}) diff --git a/src/renderer/src/components/right-sidebar/file-explorer-runtime-owner.ts b/src/renderer/src/components/right-sidebar/file-explorer-runtime-owner.ts new file mode 100644 index 00000000000..eddc593d31f --- /dev/null +++ b/src/renderer/src/components/right-sidebar/file-explorer-runtime-owner.ts @@ -0,0 +1,12 @@ +import type { GlobalSettings } from '../../../../shared/types' +import { getSettingsForWorktreeRuntimeOwner } from '@/lib/worktree-runtime-owner' +import { useAppStore } from '@/store' + +export function getRightSidebarWorktreeRuntimeSettings( + worktreeId: string | null | undefined +): Pick { + const store = useAppStore.getState() + // Why: right-sidebar file/git actions operate on the selected workspace. + // Route by that workspace owner so global focused-host changes cannot retarget them. + return getSettingsForWorktreeRuntimeOwner(store, worktreeId) +} diff --git a/src/renderer/src/components/right-sidebar/useFileDuplicate.ts b/src/renderer/src/components/right-sidebar/useFileDuplicate.ts index 82ef96b1353..e7e7737294e 100644 --- a/src/renderer/src/components/right-sidebar/useFileDuplicate.ts +++ b/src/renderer/src/components/right-sidebar/useFileDuplicate.ts @@ -2,9 +2,9 @@ import { useCallback } from 'react' import { toast } from 'sonner' import { basename, dirname, joinPath } from '@/lib/path' import type { TreeNode } from './file-explorer-types' -import { useAppStore } from '@/store' import { copyRuntimePath, runtimePathExists } from '@/runtime/runtime-file-client' import { getConnectionId } from '@/lib/connection-context' +import { getRightSidebarWorktreeRuntimeSettings } from './file-explorer-runtime-owner' /** * Electron's ipcRenderer.invoke wraps errors as: @@ -42,9 +42,8 @@ export function useFileDuplicate({ const ext = dotIndex > 0 ? name.slice(dotIndex) : '' const run = async (): Promise => { - const settings = useAppStore.getState().settings const context = { - settings, + settings: getRightSidebarWorktreeRuntimeSettings(activeWorktreeId), worktreeId: activeWorktreeId, worktreePath, connectionId: getConnectionId(activeWorktreeId) ?? undefined diff --git a/src/renderer/src/components/right-sidebar/useFileExplorerDragDrop.ts b/src/renderer/src/components/right-sidebar/useFileExplorerDragDrop.ts index c4f932cc931..79784b72c01 100644 --- a/src/renderer/src/components/right-sidebar/useFileExplorerDragDrop.ts +++ b/src/renderer/src/components/right-sidebar/useFileExplorerDragDrop.ts @@ -12,6 +12,7 @@ import { remapOpenEditorTabsForPathChange } from '@/lib/remap-open-editor-tabs-f import { requestEditorSaveQuiesce } from '@/components/editor/editor-autosave' import { commitFileExplorerOp } from './fileExplorerUndoRedo' import { renameRuntimePath } from '@/runtime/runtime-file-client' +import { getRightSidebarWorktreeRuntimeSettings } from './file-explorer-runtime-owner' function extractIpcErrorMessage(err: unknown, fallback: string): string { if (!(err instanceof Error)) { @@ -233,7 +234,7 @@ export function useFileExplorerDragDrop({ try { const connectionId = getConnectionId(activeWorktreeId ?? null) ?? undefined const fileContext = { - settings: useAppStore.getState().settings, + settings: getRightSidebarWorktreeRuntimeSettings(activeWorktreeId), worktreeId: activeWorktreeId, worktreePath, connectionId diff --git a/src/renderer/src/components/right-sidebar/useFileExplorerImport.ts b/src/renderer/src/components/right-sidebar/useFileExplorerImport.ts index 2b314c536cf..8e0951cf9bf 100644 --- a/src/renderer/src/components/right-sidebar/useFileExplorerImport.ts +++ b/src/renderer/src/components/right-sidebar/useFileExplorerImport.ts @@ -2,9 +2,9 @@ import { useEffect, useRef } from 'react' import { toast } from 'sonner' import { getConnectionId } from '@/lib/connection-context' import { extractIpcErrorMessage } from '@/lib/ipc-error' -import { useAppStore } from '@/store' import { importExternalPathsToRuntime } from '@/runtime/runtime-file-client' import { translate } from '@/i18n/i18n' +import { getRightSidebarWorktreeRuntimeSettings } from './file-explorer-runtime-owner' type UseFileExplorerImportParams = { worktreePath: string | null @@ -63,10 +63,9 @@ export function useFileExplorerImport({ void (async () => { try { - const settings = useAppStore.getState().settings const { results } = await importExternalPathsToRuntime( { - settings, + settings: getRightSidebarWorktreeRuntimeSettings(wtId), worktreeId: wtId, worktreePath: worktreePathRef.current, connectionId diff --git a/src/renderer/src/components/right-sidebar/useFileExplorerInlineInput.ts b/src/renderer/src/components/right-sidebar/useFileExplorerInlineInput.ts index 480ecca2a54..aae50da62dc 100644 --- a/src/renderer/src/components/right-sidebar/useFileExplorerInlineInput.ts +++ b/src/renderer/src/components/right-sidebar/useFileExplorerInlineInput.ts @@ -11,6 +11,7 @@ import type { TreeNode } from './file-explorer-types' import type { FileExplorerRowProjection } from './file-explorer-row-projection' import { commitFileExplorerOp } from './fileExplorerUndoRedo' import { createRuntimePath, deleteRuntimePath } from '@/runtime/runtime-file-client' +import { getRightSidebarWorktreeRuntimeSettings } from './file-explorer-runtime-owner' type UseFileExplorerInlineInputParams = { activeWorktreeId: string | null @@ -110,7 +111,7 @@ export function useFileExplorerInlineInput({ const run = async (): Promise => { const connectionId = getConnectionId(activeWorktreeId ?? null) ?? undefined const fileContext = { - settings: useAppStore.getState().settings, + settings: getRightSidebarWorktreeRuntimeSettings(activeWorktreeId), worktreeId: activeWorktreeId, worktreePath, connectionId diff --git a/src/renderer/src/components/right-sidebar/useFileExplorerTree.ts b/src/renderer/src/components/right-sidebar/useFileExplorerTree.ts index f9b32378ecf..7481bb4c877 100644 --- a/src/renderer/src/components/right-sidebar/useFileExplorerTree.ts +++ b/src/renderer/src/components/right-sidebar/useFileExplorerTree.ts @@ -6,8 +6,8 @@ import type { DirCache, TreeNode } from './file-explorer-types' import { splitPathSegments } from './path-tree' import { shouldIncludeFileExplorerEntry } from './file-explorer-entries' import { readRuntimeDirectory, statRuntimePath } from '@/runtime/runtime-file-client' -import { useAppStore } from '@/store' import { createFileExplorerDirLoadTracker } from './file-explorer-dir-load-tracker' +import { getRightSidebarWorktreeRuntimeSettings } from './file-explorer-runtime-owner' type UseFileExplorerTreeResult = { dirCache: Record @@ -63,7 +63,7 @@ export function useFileExplorerTree( const connectionId = getConnectionId(activeWorktreeId ?? null) ?? undefined const entries = await readRuntimeDirectory( { - settings: useAppStore.getState().settings, + settings: getRightSidebarWorktreeRuntimeSettings(activeWorktreeId), worktreeId: activeWorktreeId, worktreePath, connectionId @@ -133,7 +133,7 @@ export function useFileExplorerTree( const connectionId = getConnectionId(activeWorktreeId ?? null) ?? undefined return statRuntimePath( { - settings: useAppStore.getState().settings, + settings: getRightSidebarWorktreeRuntimeSettings(activeWorktreeId), worktreeId: activeWorktreeId, worktreePath, connectionId diff --git a/src/renderer/src/components/right-sidebar/useFileExplorerVisibleRowProjection.ts b/src/renderer/src/components/right-sidebar/useFileExplorerVisibleRowProjection.ts index 7839a3b3372..7b87cc5402a 100644 --- a/src/renderer/src/components/right-sidebar/useFileExplorerVisibleRowProjection.ts +++ b/src/renderer/src/components/right-sidebar/useFileExplorerVisibleRowProjection.ts @@ -2,6 +2,7 @@ import { useCallback, useEffect, useMemo, useState } from 'react' import { useAppStore } from '@/store' import { getConnectionId } from '@/lib/connection-context' import { getRuntimeGitIgnoredPaths } from '@/runtime/runtime-git-client' +import { getRightSidebarWorktreeRuntimeSettings } from './file-explorer-runtime-owner' import { isDotfileRelativePath } from './file-explorer-entries' import type { DirCache, TreeNode } from './file-explorer-types' import { @@ -162,7 +163,7 @@ export function useFileExplorerVisibleRowProjection( const connectionId = getConnectionId(activeWorktreeId) ?? undefined void getRuntimeGitIgnoredPaths( { - settings: useAppStore.getState().settings, + settings: getRightSidebarWorktreeRuntimeSettings(activeWorktreeId), worktreeId: activeWorktreeId, worktreePath, connectionId diff --git a/src/renderer/src/components/right-sidebar/useGitStatusPolling.test.ts b/src/renderer/src/components/right-sidebar/useGitStatusPolling.test.ts index a07e9e8042c..9f045022168 100644 --- a/src/renderer/src/components/right-sidebar/useGitStatusPolling.test.ts +++ b/src/renderer/src/components/right-sidebar/useGitStatusPolling.test.ts @@ -163,7 +163,15 @@ describe('useGitStatusPolling', () => { }) expect(state.setUpstreamStatus).not.toHaveBeenCalled() - expect(state.fetchUpstreamStatus).toHaveBeenCalledWith(worktree.id, '/repo', undefined) + expect(state.fetchUpstreamStatus).toHaveBeenCalledWith( + worktree.id, + '/repo', + undefined, + undefined, + { + runtimeTargetSettings: { activeRuntimeEnvironmentId: null } + } + ) }) it('passes the explicit push target to upstream refreshes', async () => { @@ -182,7 +190,10 @@ describe('useGitStatusPolling', () => { worktree.id, '/repo', undefined, - pushTarget + pushTarget, + { + runtimeTargetSettings: { activeRuntimeEnvironmentId: null } + } ) }) diff --git a/src/renderer/src/components/right-sidebar/useGitStatusPolling.ts b/src/renderer/src/components/right-sidebar/useGitStatusPolling.ts index 3e5013bc628..d3e09f6fae0 100644 --- a/src/renderer/src/components/right-sidebar/useGitStatusPolling.ts +++ b/src/renderer/src/components/right-sidebar/useGitStatusPolling.ts @@ -9,6 +9,7 @@ import { refreshGitStatusForWorktree } from './git-status-refresh' import { createCoalescedPollRunner } from './coalesced-poll-runner' import { installWindowVisibilityInterval } from '@/lib/window-visibility-interval' import { shouldPollActiveGitStatus } from '@/lib/passive-macos-app-data-access' +import { getRightSidebarWorktreeRuntimeSettings } from './file-explorer-runtime-owner' const POLL_INTERVAL_MS = 3000 @@ -93,7 +94,7 @@ export function useGitStatusPolling(options: { enabled?: boolean } = {}): void { try { const connectionId = getConnectionId(activeWorktreeId) ?? undefined await refreshGitStatusForWorktree({ - settings: useAppStore.getState().settings, + settings: getRightSidebarWorktreeRuntimeSettings(activeWorktreeId), worktreeId: activeWorktreeId, worktreePath, connectionId, @@ -175,7 +176,7 @@ export function useGitStatusPolling(options: { enabled?: boolean } = {}): void { continue } const op = (await getRuntimeGitConflictOperation({ - settings: useAppStore.getState().settings, + settings: getRightSidebarWorktreeRuntimeSettings(id), worktreeId: id, worktreePath: path, connectionId