Route file explorer requests by worktree owner

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Jinwoo-H
2026-06-13 01:53:20 -07:00
co-authored by Orca
parent d11de7228b
commit a70afd5582
11 changed files with 76 additions and 16 deletions
@@ -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`.
@@ -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()')
})
})
@@ -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<GlobalSettings, 'activeRuntimeEnvironmentId'> {
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)
}
@@ -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<void> => {
const settings = useAppStore.getState().settings
const context = {
settings,
settings: getRightSidebarWorktreeRuntimeSettings(activeWorktreeId),
worktreeId: activeWorktreeId,
worktreePath,
connectionId: getConnectionId(activeWorktreeId) ?? undefined
@@ -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
@@ -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
@@ -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<void> => {
const connectionId = getConnectionId(activeWorktreeId ?? null) ?? undefined
const fileContext = {
settings: useAppStore.getState().settings,
settings: getRightSidebarWorktreeRuntimeSettings(activeWorktreeId),
worktreeId: activeWorktreeId,
worktreePath,
connectionId
@@ -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<string, DirCache>
@@ -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
@@ -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
@@ -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 }
}
)
})
@@ -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