From aa6cceda5bbeed1b14dc10dbd692eac185a1751c Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Sat, 1 Aug 2026 02:26:51 -0700 Subject: [PATCH] refactor(file-explorer): use useMemo for paths; remove relay reference Replace manual ref-based caching with proper React hooks for content-stable path memoization. Remove outdated relay grace-time reference documentation from code review cycle. --- .../relay-grace-time-reconfiguration.md | 11 ++++--- .../right-sidebar/useFileExplorerTree.ts | 9 ++---- .../useFileExplorerVisibleRowProjection.ts | 32 +++++++------------ .../right-sidebar/useFileExplorerWatch.ts | 4 +++ 4 files changed, 24 insertions(+), 32 deletions(-) diff --git a/docs/reference/relay-grace-time-reconfiguration.md b/docs/reference/relay-grace-time-reconfiguration.md index ce7ad88789c..2023d0f51f4 100644 --- a/docs/reference/relay-grace-time-reconfiguration.md +++ b/docs/reference/relay-grace-time-reconfiguration.md @@ -7,18 +7,19 @@ for finding E of the P1 review; line numbers are as of that change. ## What decides the grace window `startGrace` (`src/relay/relay.ts:1140`) samples -`ptyHandler.configuredGraceTimeMs` and picks one branch, always passing an -explicit `timeoutMs`: +`ptyHandler.configuredGraceTimeMs` and takes the **first** branch below that +matches (`decideRelayGrace`, `src/relay/relay-grace-branch.ts`), always passing +an explicit `timeoutMs`. Listed in that precedence order: +- `shutdown-deferred` — a refused kill left the PTY pooled, so the idle branch is + unreachable; this branch supplies its own bound or a `grace=0` default would + arm no retry at all. - `startup-empty-detached` — a detached relay that never accepted a client, so it has no PTY state to preserve. Capped at `EMPTY_DETACHED_STARTUP_GRACE_MS`. - `idle-no-ptys` — zero PTYs *and* zero pending creations (`isRelayIdle`, `src/relay/relay.ts:1175`) under the unlimited default only. Capped at `IDLE_RELAY_GRACE_MS` (`src/relay/relay.ts:74`, 15 min, overridable via `ORCA_RELAY_IDLE_GRACE_MS`). -- `shutdown-deferred` — a refused kill left the PTY pooled, so the idle branch is - unreachable; this branch supplies its own bound or a `grace=0` default would - arm no retry at all. - `configured` — the configured value verbatim, never clamped. The zero-only gate is load-bearing: the idle cap bounds *only* the unlimited diff --git a/src/renderer/src/components/right-sidebar/useFileExplorerTree.ts b/src/renderer/src/components/right-sidebar/useFileExplorerTree.ts index 3e29de3b112..d6f695c2ac3 100644 --- a/src/renderer/src/components/right-sidebar/useFileExplorerTree.ts +++ b/src/renderer/src/components/right-sidebar/useFileExplorerTree.ts @@ -51,8 +51,6 @@ export function useFileExplorerTree( const staleDirsRef = useRef(new Set()) // Why: separates a failed root read from a superseded one — loadDir returns false for both. const rootReadFailedRef = useRef(false) - const expandedRef = useRef(expanded) - expandedRef.current = expanded const loadDir = useCallback( async ( @@ -179,11 +177,8 @@ export function useFileExplorerTree( staleDirsRef.current.delete(dirPath) } } - for (const dirPath of collectStaleDirCachePaths( - dirCacheRef.current, - worktreePath, - expandedRef.current - )) { + // Why: callers use the latest refreshTree identity, so this closure has the live expanded set. + for (const dirPath of collectStaleDirCachePaths(dirCacheRef.current, worktreePath, expanded)) { staleDirsRef.current.add(dirPath) } const refreshSession = dirLoadTrackerRef.current.getSession() diff --git a/src/renderer/src/components/right-sidebar/useFileExplorerVisibleRowProjection.ts b/src/renderer/src/components/right-sidebar/useFileExplorerVisibleRowProjection.ts index 85d22557af9..06a5e110ad4 100644 --- a/src/renderer/src/components/right-sidebar/useFileExplorerVisibleRowProjection.ts +++ b/src/renderer/src/components/right-sidebar/useFileExplorerVisibleRowProjection.ts @@ -1,4 +1,4 @@ -import { useCallback, useMemo, useRef } from 'react' +import { useCallback, useMemo } from 'react' import { useAppStore } from '@/store' import { isDotfileRelativePath } from './file-explorer-entries' import type { DirCache, TreeNode } from './file-explorer-types' @@ -118,25 +118,17 @@ export function createVisibleFileExplorerRowProjection( * exists to bound. */ function useContentStableRelativePaths(relativePaths: string[], enabled: boolean): string[] { - const cache = useRef<{ source: string[]; signature: string; value: string[] } | null>(null) - // Why: a name-filter list is a different population, so signing it would evict the tree - // signature — clearing the filter would then mint a fresh identity and re-issue the very - // whole-tree check-ignore this hook exists to prevent. - if (!enabled) { - return relativePaths - } - // Why: the caller memoizes, so an unchanged tree hands back the same array — skip rebuilding a - // signature over every visible path on re-renders that never touched the tree. - if (cache.current?.source === relativePaths) { - return cache.current.value - } - // NUL separator: it is the one byte no path segment can contain. - const signature = relativePaths.join('\u0000') - cache.current = - cache.current?.signature === signature - ? { ...cache.current, source: relativePaths } - : { source: relativePaths, signature, value: relativePaths } - return cache.current.value + // Why: filters need fresh identities per keystroke and must not evict the tree signature. + // Why: NUL cannot occur in paths, so the signature can reconstruct the list losslessly. + const signature = useMemo( + () => (enabled ? relativePaths.join('\u0000') : null), + [enabled, relativePaths] + ) + const stableTreePaths = useMemo( + () => (signature ? signature.split('\u0000') : EMPTY_RELATIVE_PATHS), + [signature] + ) + return enabled ? stableTreePaths : relativePaths } export function useFileExplorerVisibleRowProjection( diff --git a/src/renderer/src/components/right-sidebar/useFileExplorerWatch.ts b/src/renderer/src/components/right-sidebar/useFileExplorerWatch.ts index a1eef875d15..c75dfd4fac8 100644 --- a/src/renderer/src/components/right-sidebar/useFileExplorerWatch.ts +++ b/src/renderer/src/components/right-sidebar/useFileExplorerWatch.ts @@ -143,6 +143,10 @@ export function useFileExplorerWatch({ const scheduler = createFileExplorerWatchRefreshScheduler({ refreshTree: () => refreshTreeRef.current(), refreshDir: (dirPath) => refreshDirRef.current(dirPath), + // Why exact-match on `expanded`, not a normalized compare: dirPath already arrives as a + // resolved dirCache key (resolveCachedDirPath), and refreshTree re-reads dirCache under the + // verbatim expanded strings — a differently-spelled key it never wrote would be reported + // covered and left stale. Only the root is normalized: it is keyed by worktreePath. isCoveredByFullRefresh: (dirPath) => normalizeRuntimePathForComparison(dirPath) === normalizeRuntimePathForComparison(currentWorktreePath) ||